You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
5. Caches the result to avoid redundant processing
325
325
6. Returns the generated table matching the specified kind
326
326
327
+
## getExtendedTable Helper
328
+
329
+
A table that plugins are attached to gains the fields those plugins return in `extends.fields`, but only in the table `tailor generate` registers — the object exported from the table's source file stays as written. `getExtendedTable()` returns the table with every plugin-added field applied, so tooling that reads the table at runtime sees the same fields `tailor generate` does.
extendedCustomer.fields.deletedAt; // added by a plugin attached with .plugin()
340
+
```
341
+
342
+
**Parameters:**
343
+
344
+
-`configPath`: Path to `tailor.config.ts` (absolute or relative to cwd)
345
+
-`sourceTable`: The TailorDB table as exported from its source file
346
+
347
+
**How it works:**
348
+
349
+
1. Returns `sourceTable` itself when no plugin is attached to it
350
+
2. Loads and caches the config from the given path
351
+
3. Auto-resolves the namespace from config
352
+
4. Calls each attached plugin's `onTableLoaded()` in the order of the `.plugin()` calls, each seeing the fields the plugins before it added
353
+
5. Caches the result per config path and table
354
+
6. Returns a new table with the added fields; `sourceTable` is not changed
355
+
356
+
The seed schema files `tailor generate` writes for tables with plugins attached use this helper, so `tailor seed validate` checks plugin-added fields like the table's own.
357
+
327
358
## Examples
328
359
329
360
### Definition-time Plugin (Soft Delete)
@@ -646,10 +677,11 @@ of whether `.files()` or `.plugin()` was called first. `tailor generate` also re
646
677
collision at runtime, as a backstop for any case a table's static type doesn't otherwise catch.
647
678
648
679
This only affects the table's static type. The corresponding field exists on the table's
649
-
generated schema, and on the table object's own `fields`, only after `tailor generate` actually
650
-
applies `extends.fields`. Before that, reading an injected field directly off the table
651
-
(`table.fields.status`) returns `undefined`, and `pickFields(["status"])` throws — call these only
652
-
with the table's originally declared fields, not ones a plugin injects.
680
+
generated schema, and on the table `tailor generate` registers, only once `extends.fields` is
681
+
applied; the table object exported from the source file never gains it. Reading an injected field
682
+
directly off that object (`table.fields.status`) returns `undefined`, and `pickFields(["status"])`
683
+
throws — call these only with the table's originally declared fields, or load the table with
Copy file name to clipboardExpand all lines: docs/sdk/testing.md
+22-20Lines changed: 22 additions & 20 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -149,26 +149,30 @@ Pass `{ onUnhandled: "error" }` to make an unmatched query fail instead of retur
149
149
150
150
Instead of staging responses, back TailorDB with `@electric-sql/pglite` — an in-memory PostgreSQL (install it as a devDependency) — so the queries a resolver, executor, or workflow job issues through `getDB()` execute against real data. `getDB(namespace)` needs no test-side swap: acquire the mock, and each namespace you list resolves to its PGlite instance.
151
151
152
-
Create the tables the test touches with `CREATE TABLE` statements matching the generated Kysely types — `text` for string and enum fields, `timestamptz` for date/datetime, `jsonb` for nested objects. The schema only has to match what your code reads and writes, not TailorDB's storage; relations are not enforced.
152
+
Let `kyselyTypePlugin` generate the `CREATE TABLE` script for you: set `pgliteSchemaPath` next to `distPath`, and `tailor generate` writes a module exporting one script per namespace, derived from the same table definitions as the Kysely types.
153
+
154
+
```typescript
155
+
// tailor.config.ts
156
+
kyselyTypePlugin({
157
+
distPath: "./generated/db.ts",
158
+
pgliteSchemaPath: "./generated/db.pglite.ts",
159
+
});
160
+
```
161
+
162
+
Run the namespace's script once per PGlite instance. Every statement is `IF NOT EXISTS`, so applying it again to an instance that already has the tables is harmless.
@@ -201,21 +205,19 @@ test("upserts against real rows", async () => {
201
205
});
202
206
```
203
207
204
-
A `.serial()` field is omitted from generated `getDB()` inserts, so its PGlite column must generate a value. Use an identity for an integer serial. For a formatted string serial, create a sequence and reproduce the format in its `DEFAULT` expression:
208
+
The generated columns follow the Kysely types, not TailorDB's storage: `text` for string and enum fields, `timestamptz` for datetime, `date` and `time` for date and time, `numeric` for decimal, rounded to the configured scale and read back with exactly that many fractional digits, `jsonb`for nested objects (and arrays of them), Postgres arrays for other array fields. `id` is a generated `uuid` primary key, `.unique()` fields and unique `.indexes()` are enforced, so `ON CONFLICT` upserts behave, and `.default()` values become column defaults (`"now"` becomes the current time). `.serial()` fields are assigned by the database from the configured `start`, `maxValue`, and format. Relations are not enforced.
205
209
206
-
```sql
207
-
CREATESEQUENCE "invoiceNumberSequence" START WITH 1000;
208
-
CREATETABLE "Invoice" (
209
-
"sequentialId"integer GENERATED BY DEFAULT AS IDENTITY (START WITH 1),
- Hooks, validations, and permissions do not run. A required field whose value only its own field-level create hook supplies is created nullable, so inserts that omit it succeed; give it a `.default()` if the test reads it back. A field filled by a table-level hook stays `NOT NULL`, as its Kysely type still requires it on insert.
213
+
- Serial formats are reproduced for a single `%d`, `%x`, or `%X` specifier with an optional zero-padded width; an octal `%o` format fails generation with an error naming the field.
214
+
- A datetime inside a nested object reads back as a string from `jsonb`, not a `Date`.
215
+
- On a persistent PGlite (`dataDir`), tables created by an earlier run are kept as they were; drop them or start from an empty directory after changing a table definition.
214
216
215
-
PGlite does not apply the TailorDB `.serial()` configuration itself. Match the `start`, `format`, and any limit that the behavior under test relies on.
217
+
To hand-write DDL instead — for a table not in the schema, or to add a constraint — run your own statements after the script, or without it.
216
218
217
219
- The PGlite instance is yours: the mock never closes it, so close it in `afterAll`. Reuse one instance across a suite — creating one per test is slow.
218
-
- Pass the same instance under several namespaces to drive them against one shared database.
220
+
- Pass the same instance under several namespaces to drive them against one shared database. Two namespaces with a same-named table cannot share one instance, because the second script leaves the first table as it is.
219
221
- Seed through `getDB` itself. When a column type rejects a value that only the test must stage, use `createKyselyPGlite<Unmigrated<...>>(pglite)` instead — see [Testing Migrations Locally](services/tailordb-migration.md#testing-migrations-locally). This only affects test setup; it cannot supply a `.serial()` value for an insert issued by the code under test.
220
222
- Transactions on a shared instance are serialized: while one is open, queries from other `getDB` instances wait. Do not use `test.concurrent` with a shared instance, and do not query the same instance through a second `getDB` from inside a transaction — that waits on itself.
221
223
- PGlite runs full PostgreSQL while TailorDB supports a subset of it, and TailorDB hooks, validations, and permissions do not run here — a test passing on PGlite can still behave differently on the platform. Keep [`mockTailordb`](#tailordb-mock) or [`createKyselyMock`](#kysely-layer-mock-createkyselymock) tests for query shape and error paths, and E2E tests for platform behavior.
0 commit comments