feat(tailordb,resolver)!: object-literal descriptor API and record-level hooks/validate#905
feat(tailordb,resolver)!: object-literal descriptor API and record-level hooks/validate#905dqn wants to merge 39 commits into
Conversation
…ver descriptor support Add createTable() and timestampFields() as an alternative to the fluent db.type() API for defining TailorDB types using plain object literals. This is a reworked version of the closed PR #645 (createType), renamed to createTable. Extend createResolver() to accept object-literal field descriptors ({ kind: "string" }) alongside the existing fluent t.string() API in both input and output parameters. Fluent and descriptor styles can be mixed freely.
…date decimal scale - Tighten isResolverFieldDescriptor to check kind is a known string value, preventing false positives when output records contain a field named "kind" - Add decimal scale validation (integer 0-12) in createTable to match db.decimal()
…lverFieldMap, add boundary tests - Export KindToFieldType from descriptor.ts, remove duplicate in resolver.ts - Move isTailorField from closure to module-level function - Replace two-pass iteration in resolveResolverFieldMap with single-pass loop - Add decimal scale boundary value tests (0 and 12) for createTable
Add runtime guards so that untyped callers (JS, JSON-driven schemas) get a clear error instead of silently producing fields with undefined type when passing an invalid kind like "strng".
…hook typing trade-off Reject enum descriptors that omit the required `values` array at runtime, preventing permissive fields from being silently created by untyped callers. Document the accepted trade-off that descriptor hook callbacks receive the base scalar type rather than the final output type adjusted for optional/array.
…sthrough fields ValidateHookTypes now checks against DescriptorBaseOutput (base scalar) instead of DescriptorOutput (with array/optional applied), matching the IndexableOptions typing contract. Also reject plain objects without `kind` or `type` that would silently pass through as TailorDBField.
…and metadata Strengthen the passthrough field check to verify both `type` (string) and `metadata` (object) properties, catching plain objects that are neither descriptors nor real field instances. Apply the same guard to both resolver and tailordb descriptor paths.
…vious comments Delegate field resolution in resolveResolverFieldMap to resolveResolverField instead of inlining the same validation logic. Remove self-evident WHAT comments from createResolver and resolveOutput. Also fix pre-existing import order in processOrder.ts test fixture.
The import-x/order rule changed after merging main, making the original order (date-fns before @tailor-platform/sdk) correct again.
🦋 Changeset detectedLatest commit: a34ce7c The changes in this PR will be included in the next version bump. This PR includes changesets to release 2 packages
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
commit: |
📖 Docs Consistency Check
|
| File | Issue | Suggested Fix |
|---|---|---|
packages/sdk/docs/services/tailordb.md |
New createTable API is not documented |
Add section documenting createTable object-literal syntax as alternative to db.type() |
packages/sdk/docs/services/resolver.md |
New descriptor syntax for resolver fields is not documented | Add section documenting { kind: "string" } syntax as alternative to t.string() |
CLAUDE.md |
Code Patterns section only mentions db.type() |
Update to mention createTable as an alternative |
example/ directory |
No examples demonstrate new APIs | Consider adding example files showing createTable and resolver descriptor usage |
Details
1. TailorDB Documentation (packages/sdk/docs/services/tailordb.md)
What the implementation adds:
createTable(name, { fields })- Object-literal API exported from@tailor-platform/sdk(packages/sdk/src/configure/services/index.ts:4)- Supports field descriptors like
{ kind: "string" },{ kind: "int", optional: true }, etc. - Full feature parity with
db.type()including hooks, validation, relations, indices, permissions
What the docs say:
- Only documents the fluent API:
db.type(),db.string(),db.int(), etc. - No mention of
createTableor descriptor syntax anywhere in the file
Example from PR description:
const order = createTable("Order", {
fields: {
name: { kind: "string" },
quantity: { kind: "int", optional: true, index: true },
status: { kind: "enum", values: ["pending", "shipped"] },
},
permission: unsafeAllowAllTypePermission,
});Suggested section location: After "Type Definition" section (line 17), add a new section titled "Alternative: Object-Literal Syntax (createTable)"
2. Resolver Documentation (packages/sdk/docs/services/resolver.md)
What the implementation adds:
ResolverFieldDescriptortype for resolver input/output fields (packages/sdk/src/configure/services/resolver/descriptor.ts:57-68)- Allows
{ kind: "string" }syntax as alternative tot.string() - Can be mixed with fluent API fields in the same resolver
What the docs say:
- Only documents fluent API:
t.int(),t.string(),t.object(), etc. (lines 84-121) - Section "Input/Output Schemas" (line 104) states: "Define input/output schemas using methods of
tobject" - No mention of descriptor syntax
Example from PR description:
const resolver = createResolver({
name: "addNumbers",
operation: "query",
input: {
a: { kind: "int" },
b: { kind: "int" },
},
output: { kind: "int" },
body: ({ input }) => input.a + input.b,
});Suggested section location: After "Input/Output Schemas" intro (line 106), add subsection "Object-Literal Descriptor Syntax"
3. CLAUDE.md
What the docs say:
- Line 44:
example/tailordb/*.ts- Model definitions withdb.type() - No mention of
createTablealternative
Suggested fix:
Update line 44 to:
- `example/tailordb/*.ts` - Model definitions with `db.type()` or `createTable`Or add to "Non-obvious Rules and Gotchas" section if there are any caveats about the new API.
4. Example Files (example/ directory)
What exists:
- All TailorDB types use fluent API:
db.type(...)(e.g.,example/tailordb/customer.ts) - All resolvers use fluent API:
t.string(),t.int(), etc. (e.g.,example/resolvers/add.ts)
What's missing:
- No example demonstrating
createTable - No example demonstrating descriptor syntax in resolvers
Note from PR description:
"No impact on existing
db.type()/t.*()APIs"
The old APIs still work, but examples showing the new alternatives would help users understand both options.
Recommended Actions
-
Add documentation for
createTableinpackages/sdk/docs/services/tailordb.md:- Show basic usage with field descriptors
- Explain when to use object-literal vs fluent API (e.g., PR notes that hook typing is less precise in descriptors)
- Document all supported descriptor options (optional, array, hooks, validate, index, unique, serial, relation, etc.)
-
Add documentation for descriptor syntax in
packages/sdk/docs/services/resolver.md:- Show that descriptors can be mixed with fluent API
- Document supported descriptor types (string, int, float, bool, uuid, decimal, date, datetime, time, enum, object)
- Note any limitations vs fluent API
-
Update CLAUDE.md to mention both API styles
-
Consider adding example files (optional but recommended):
example/tailordb/descriptor-example.tsusingcreateTableexample/resolvers/descriptor-example.tsusing field descriptors
📖 Docs Consistency Check
|
| File | Issue | Suggested Fix |
|---|---|---|
packages/sdk/docs/services/tailordb.md |
Missing createTable() API documentation |
Add section documenting the object-literal descriptor API as an alternative to db.type() |
packages/sdk/docs/services/tailordb.md |
Missing timestampFields() helper |
Document the timestampFields() helper function and its usage |
packages/sdk/docs/services/resolver.md |
Missing descriptor syntax for input/output fields | Add section showing { kind: "string" } syntax alongside fluent API examples |
CLAUDE.md |
Code Patterns section only mentions db.type() |
Add createTable() as an alternative pattern with reference to examples |
example/ directory |
No examples using new APIs | Add at least one example file demonstrating createTable() and resolver descriptor syntax |
Details
1. TailorDB createTable() API (packages/sdk/docs/services/tailordb.md)
What the implementation does:
packages/sdk/src/configure/services/tailordb/createTable.ts:469-504- ExportscreateTable()function that accepts object-literal field descriptors as an alternative to the fluentdb.type()APIpackages/sdk/src/configure/services/index.ts:4- ExportscreateTablefrom the main SDK package- JSDoc example in the implementation shows:
export const user = createTable("User", { name: { kind: "string" }, email: { kind: "string", unique: true }, role: { kind: "enum", values: ["MANAGER", "STAFF"] }, ...timestampFields(), });
What the documentation says:
- The TailorDB documentation only documents
db.type()with the fluent API - No mention of
createTable()anywhere in the docs - The "Type Definition" section (lines 17-61) only shows the
db.type()pattern
Impact:
Users won't know that the object-literal descriptor API exists as an alternative style for defining TailorDB types.
2. TailorDB timestampFields() helper (packages/sdk/docs/services/tailordb.md)
What the implementation does:
packages/sdk/src/configure/services/tailordb/createTable.ts:516-530- ExportstimestampFields()helper that returns standardcreatedAt/updatedAtfields with auto-hookspackages/sdk/src/configure/services/index.ts:5- ExportstimestampFieldsfrom the main SDK package- Can be used with both
createTable()anddb.type()via spread syntax
What the documentation says:
- The docs show
...db.fields.timestamps()(line 357-358) as the pattern for timestamp fields - No mention of
timestampFields()helper
Impact:
Users using createTable() won't know about the timestampFields() helper designed for the descriptor API. The existing db.fields.timestamps() is for the fluent API.
3. Resolver descriptor syntax (packages/sdk/docs/services/resolver.md)
What the implementation does:
packages/sdk/src/configure/services/resolver/descriptor.ts:1-212- ImplementsResolverFieldDescriptortype system supporting{ kind: "string" }syntaxpackages/sdk/src/configure/services/resolver/resolver.ts:79-82- Documents that input/output fields accept both fluent API and object-literal descriptors, and both can be mixed- JSDoc example in resolver.ts:106-116 shows:
input: { a: { kind: "int", description: "First number" }, b: { kind: "int", description: "Second number" }, }, output: { kind: "int", description: "Sum" },
What the documentation says:
packages/sdk/docs/services/resolver.md:84-142- Only shows fluent API examples witht.string(),t.int(), etc.- No mention that descriptor syntax
{ kind: "int" }is supported - No examples mixing both styles
Impact:
Users won't know they can use descriptor syntax in resolvers, which provides a more concise alternative for simple fields and matches the TailorDB createTable() style.
4. CLAUDE.md Code Patterns section
What the implementation does:
- Adds
createTableas a new exported API for defining TailorDB types
What CLAUDE.md says:
- Lines 43: "Model definitions with
db.type()" - only mentions the fluent API - No mention of
createTable()as an alternative pattern
Impact:
Claude Code won't know about the new API when helping users write TailorDB models, and won't suggest it as an option.
5. Example files
What the implementation does:
- Adds fully functional
createTable()and descriptor APIs
What the examples show:
example/tailordb/*.ts- All examples usedb.type()fluent API onlyexample/resolvers/*.ts- All examples uset.string()fluent API only- No examples demonstrate the new descriptor syntax
Impact:
Users learning from examples won't see the descriptor API in action. The example/ directory is specifically referenced in CLAUDE.md as the source for "working implementations of all patterns."
Recommended Actions
-
Add
createTable()section to TailorDB docs - Document the object-literal API with full examples showing all field descriptor types (scalar, enum, object, relations, hooks, validation, etc.) -
Document
timestampFields()helper - Add to TailorDB docs, likely in a "Common Fields" or "Helper Functions" section, showing it works with bothcreateTable()and spread intodb.type() -
Add descriptor syntax section to Resolver docs - Show examples of
{ kind: "string" }syntax for input/output fields, demonstrate mixing with fluent API -
Update CLAUDE.md - Add
createTableto the Code Patterns section as an alternative todb.type(), update the reference to mention both APIs -
Add example files - Create at least one TailorDB type using
createTable()and one resolver using descriptor syntax in theexample/directory
This comment has been minimized.
This comment has been minimized.
Cover pluralForm (string and tuple), description, features, and gqlPermission options that were missing from the test suite.
This comment has been minimized.
This comment has been minimized.
Document the object-literal API (createTable, timestampFields) in tailordb.md and resolver field descriptors in resolver.md. Update CLAUDE.md code patterns to mention both API styles.
Demonstrate the object-literal descriptor API with a Product model that includes enum, relation, timestamps, and permissions.
This is a major issue... |
Descriptor inline hooks now receive the array output type for array fields (e.g. Hook<unknown, string[]> instead of Hook<unknown, string>). - Introduce ScalarOrArrayHooks<O> discriminated union that narrows hooks to Hook<unknown, O> for scalar and Hook<unknown, O[]> for array - Unify ValidatedDescriptors into a single mapped type to avoid combinatorial type explosion with the doubled descriptor union - Compute DescriptorHookOutput directly from field properties instead of intersecting with the FieldDescriptor union - Keep validate callbacks at base scalar type to preserve contextual typing for inline lambdas
…ping TailorAnyDBField in FieldEntry union prevented TypeScript from narrowing FieldDescriptor during generic inference, causing inline hook callbacks to lose contextual typing (value resolved to any). Add a FieldDescriptor-only overload that TypeScript tries first, restoring correct type resolution for inline scalar, array, and datetime hooks.
…nd tests Add tests showing that inline enum descriptor hooks cannot narrow value to the literal union (TS reverse-inference limitation), and document the two working workarounds: fluent API db.enum().hooks() and type-level options.hooks.<field>.
Status: Inline hook typing improvementsWhat changed
Known limitation: inline enum hooksInline enum descriptor hooks ( Working alternatives (both tested):
createTable(
"Test",
{ role: { kind: "enum", values: ["ADMIN", "USER"] } },
{
hooks: {
role: {
create: ({ value }) => {
// value: "ADMIN" | "USER" | null ✅
return value ?? "USER";
},
},
},
},
);
createTable("Test", {
role: db.enum(["ADMIN", "USER"]).hooks({
create: ({ value }) => {
// value: "ADMIN" | "USER" | null ✅
return value ?? "USER";
},
}),
}); |
The synthetic hook expressions for generated datetime fields (createdAt/updatedAt) are detected as schema changes by the migration system. Generate migration 0003 to account for this.
Record-level hooks are not yet wired to the platform, so the server-side seed insert cannot compute fullAddress automatically. Provide the pre-computed values in the seed data.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
Record-level hooks are not yet platform-supported, so fullAddress must be provided explicitly in GraphQL mutations. Add fullAddress to all createCustomer inputs in E2E tests.
This comment has been minimized.
This comment has been minimized.
…el pipeline Record-level validators (.validate() on db.type) were silently dropped during apply because the Zod schema stripped them and the bundler/parser didn't process them. This wires them through the existing field-level pipeline by: 1. Adding validate/hooks to the Zod type metadata schema 2. Collecting record-level validators in the bundler for precompilation 3. Distributing them to the first field in the type-parser
This comment has been minimized.
This comment has been minimized.
The auto-generated `id` field does not evaluate validators on the platform. Distribute record-level validators to the first user-defined field (skipping `id`) so they are properly evaluated on create/update.
This comment has been minimized.
This comment has been minimized.
|
Status: waiting on the platform side to add record-level |
The platform shipped TailorDBType_TypeHook and TailorDBType_TypeValidate on the TypeConfig message, with CEL exclusivity rules against field-level hooks/validate. Regenerating the proto pulls those new message types into the SDK, alongside other upstream proto updates (workspace, staticwebsite, workflow, application, function, telemetryrouter, service, gateway filter).
…e_validate
Replace the temporary workaround that distributed record-level validators
to the first non-id field with proper end-to-end wiring to the platform's
new `TailorDBType.TypeHook` and `TailorDBType.TypeValidate` proto fields.
- Parser: convert record-level hooks (`{ data, user }`) and validators to
operator-form Script expressions and attach them to `TailorDBType`
rather than splicing them into a field's `validate` list.
- Apply: emit `typeHook` / `typeValidate` on the manifest schema. Multiple
SDK-side validators are combined with `&&` since the platform exposes a
single create/update Script per type.
- Migrate snapshot: extend `SnapshotType` with `hooks` / `validate`, diff
via `type_modified` with explicit reasons, and apply on snapshot replay.
Migration 0004 was generated under the workaround that pushed record-level validators onto the first non-id field. Regenerate from the current SDK so the diff is recorded as a `type_modified` carrying `hooks` and `validate` at the type level, matching the platform's `type_hook` / `type_validate`.
Fixed in 26f1771 — field-level hooks/validate were removed from descriptors entirely, and the surviving record-level callback receives the full output-typed record so this typing limitation no longer applies. |
Fixed in 26f1771 — went one step further and removed field-level hooks/validate from |
…escriptor-api # Conflicts: # CLAUDE.md # packages/sdk/src/configure/services/resolver/resolver.ts # packages/sdk/src/configure/services/tailordb/index.ts # packages/sdk/src/configure/services/tailordb/schema.test.ts # packages/sdk/src/configure/services/tailordb/schema.ts # packages/sdk/src/configure/services/tailordb/types.ts # packages/sdk/src/parser/service/tailordb/field.ts # packages/sdk/src/types/tailordb.ts # packages/sdk/src/types/validation.ts # packages/tailor-proto/src/tailor/v1/service_pb.js
…rd-level hooks The platform rejects schemas defining both `type_hook` and field-level hooks. After we started emitting record-level hooks via `type_hook`, types like Customer (with both `.hooks()` and `db.fields.timestamps()`) hit the validation error on deploy. - Skip auto-generated `new Date()` field hooks when the type carries record-level hooks; the user's hook is responsible for populating the timestamps. - Treat generated fields as optional in seed schema when the type has record-level hooks, since the record hook fills them in.
…mp hooks Customer's record-level hooks now own createdAt/updatedAt population, so the field-level `new Date()` hooks are no longer emitted. Capture that schema change as a migration to keep deploy's schema-check green.
This comment has been minimized.
This comment has been minimized.
Per the Terraform provider docs, type-level hooks receive `_input` (the
record map) and `user`, not the field-level `_value` / `_data` bindings.
The SDK was reusing the field-level expression template, which references
the undefined `_value` symbol on every type_hook evaluation and surfaces
as a generic "internal error" during Kysely seed inserts.
- Emit record-level hook and validator scripts as
`(fn)({ data: _input, user: ... })`, dropping the field-only `_value`.
- Regenerate migration 0005 so deploy's schema-check stays in sync.
This comment has been minimized.
This comment has been minimized.
The Kysely batch insert path used by `tailor-sdk seed` does not appear to invoke type_hook server-side, so Customer rows hit a NOT NULL violation on createdAt now that we no longer emit field-level auto-hooks for types with record-level hooks. Pass explicit timestamps in the seed data.
This comment has been minimized.
This comment has been minimized.
…ide-only)
The platform's `type_hook` and `type_validate` scripts receive `_input`
(the record map) plus `user`, and hooks are expected to return only the
fields to override on the record. The previous SDK contract emitted
`_data` / `_value` bindings borrowed from the field-level pipeline and
required hooks to spread `data` back into a complete record, which made
the platform's hook evaluator produce opaque internal errors on Kysely
inserts.
- `RecordHookFn` now returns `Partial<TData>`; omitted fields keep their
incoming values. JSDoc and changeset updated accordingly.
- Hook/validator script compilation distinguishes `record-hooks` /
`record-validate` from field-level kinds and emits
`({ data: _input, user })` invocations for the record-level variants
(both inline and bundled forms).
- `createTailorDBHook` merges the record hook's overrides onto the
per-field result instead of replacing the whole record, matching the
new contract.
- Example: `Customer` returns only the recomputed fields from its
record-level hooks. Migration 0005 regenerated; the obsolete manual
timestamp entries in `Customer.jsonl` are reverted now that the
record-level hook populates them.
This comment has been minimized.
This comment has been minimized.
The platform's type_validate script must return a map
(`{ key: errorMessage }` on failure, `{}` on success); the SDK was
emitting a chained boolean expression which the runtime rejected with
`should return a map but returned bool: true`, masked downstream as an
opaque "internal error" on Kysely seed inserts.
- `convertRecordValidators` now wraps each predicate so it evaluates to
a `{ _record_<i>: message }` entry on failure and `{}` on success.
- `toProtoTypeValidate` (deploy) and `toProtoSnapshotTypeValidate`
(migration manifest) merge the per-predicate maps with `Object.assign`
so every failing message is surfaced.
- Migration 0005 regenerated to capture the new expressions.
Code Metrics Report (packages/sdk)
Details | | main (b2bd4aa) | #905 (d637ef0) | +/- |
|--------------------|----------------|----------------|-------|
+ | Coverage | 62.3% | 62.4% | +0.0% |
| Files | 364 | 366 | +2 |
| Lines | 12773 | 12976 | +203 |
+ | Covered | 7967 | 8102 | +135 |
+ | Code to Test Ratio | 1:0.4 | 1:0.4 | +0.0 |
| Code | 83913 | 85693 | +1780 |
+ | Test | 35136 | 36003 | +867 |Code coverage of files in pull request scope (67.0% → 67.0%)SDK Configure Bundle Size
Runtime Performance
Type Performance (instantiations)
Reported by octocov |
Add
createTableand resolver field descriptors as an alternative object-literal syntax for defining TailorDB types and resolver input/output fields. Move TailorDB hooks and validators from field level to record level.Usage
Main Changes
createTableobject-literal API for TailorDB type definitions with full descriptor support (all scalar kinds, enum, nested object, serial, relation, permissions, indices)ResolverFieldDescriptorfor resolverinput/outputfields, allowing{ kind: "string" }syntax alongside fluentt.string()fields.hooks()and.validate()from TailorDB field builders and descriptorscreateTabletype-levelhooks/validateoptions now receive({ data, user })with the full record. Hooks must return a complete record (spread incomingdatato keep unchanged fields)db.fields.timestamps()/timestampFields()no longer installs automaticcreate/updatehooks. PopulatecreatedAt/updatedAtvia a record-level hookgeneratedmetadata flag to timestamp fields so the Kysely plugin emitsGenerated<Timestamp>and the seed hook auto-fills valueskindToFieldTypemapping andresolveResolverFieldMapinto a shareddescriptor.tsmoduleTailorDBType.TypeHook/TailorDBType.TypeValidateproto fields, with full snapshot / diff support in the migration systemNotes