diff --git a/.changeset/object-literal-descriptor-api.md b/.changeset/object-literal-descriptor-api.md new file mode 100644 index 000000000..3c1d6dd28 --- /dev/null +++ b/.changeset/object-literal-descriptor-api.md @@ -0,0 +1,12 @@ +--- +"@tailor-platform/sdk": major +--- + +TailorDB API refactor: object-literal descriptor API and record-level hooks/validate + +- **New**: `createTable(name, fields, options?)` accepts object-literal field descriptors alongside the existing fluent API. +- **New**: Resolver fields accept object-literal descriptors. +- **Breaking**: Removed field-level `.hooks()` and `.validate()` from the TailorDB field builder (`db.string().hooks(...)`, `db.int().validate(...)`, etc.) and from field descriptors passed to `createTable`. +- **Breaking**: `createTable` type-level `hooks` / `validate` options are now **record-level** callbacks that receive the full record via `({ data, user }) => ...`. Hooks return an object containing **only the fields to override**; omitted fields keep their incoming values. The SDK statically extracts the override key set from the returned object literal and expands each entry into a field-level hook on the affected field, so the platform-generated GraphQL `CreateInput` treats those fields as optional. `validate` accepts a single function, a `[fn, message]` tuple, or an array of either. + +Migration: move field-level hook/validate logic into record-level callbacks on the type. Record-level hook bodies must end in a static object literal (`({ data }) => ({ k1: v1, k2: v2 })`) so the override keys can be statically resolved; branched or computed return shapes will throw at parse time. diff --git a/.gitignore b/.gitignore index 950c06d03..f6d36bd37 100644 --- a/.gitignore +++ b/.gitignore @@ -55,3 +55,4 @@ CLAUDE.local.md llm-challenge/results/ llm-challenge/problems/*/work .claude/tmp/ +.agent/tmp/ diff --git a/CLAUDE.md b/CLAUDE.md index 5cdf85c14..34964b575 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -41,7 +41,7 @@ Refer to `example/` for working implementations of all patterns (config, models, Key files: - `example/tailor.config.ts` - Configuration with defineConfig, defineAuth, defineIdp, defineStaticWebSite, definePlugins -- `example/tailordb/*.ts` - Model definitions with `db.type()` +- `example/tailordb/*.ts` - Model definitions with `db.type()` or `createTable` - `example/resolvers/*.ts` - Resolver implementations with `createResolver` - `example/executors/*.ts` - Executor implementations with `createExecutor` - `example/workflows/*.ts` - Workflow implementations with `createWorkflow` / `createWorkflowJob` diff --git a/example/e2e/executor.test.ts b/example/e2e/executor.test.ts index 0eec3e402..fcf1972b7 100644 --- a/example/e2e/executor.test.ts +++ b/example/e2e/executor.test.ts @@ -177,6 +177,7 @@ describe("dataplane", () => { email: "customer@example.com" country: "USA" postalCode: "12345" + fullAddress: "12345" state: "California" } ) { diff --git a/example/e2e/tailordb.test.ts b/example/e2e/tailordb.test.ts index 91499ca17..c15d05497 100644 --- a/example/e2e/tailordb.test.ts +++ b/example/e2e/tailordb.test.ts @@ -236,6 +236,7 @@ describe("dataplane", () => { email: "customer-${randomUUID()}@example.com" country: "USA" postalCode: "12345" + fullAddress: "12345" state: "California" } ) { @@ -419,6 +420,7 @@ describe("dataplane", () => { email: "customer@example.com" country: "USA" postalCode: "12345" + fullAddress: "12345" state: "California" } ) { @@ -535,6 +537,8 @@ describe("dataplane", () => { }); }); + // TODO(record-level-hooks): once the platform supports record-level hooks, + // remove the explicit fullAddress input and verify the hook computes it. test("custom hooks execute correctly", async () => { const query = gql` mutation { @@ -546,6 +550,7 @@ describe("dataplane", () => { postalCode: "12345" address: "123 Main St" city: "Los Angeles" + fullAddress: "12345 123 Main St Los Angeles" state: "California" } ) { @@ -577,6 +582,7 @@ describe("dataplane", () => { email: "bob@example.com" country: "USA" postalCode: "12345" + fullAddress: "12345" state: "California" } ) { diff --git a/example/executors/userRecordLog.ts b/example/executors/userRecordLog.ts index 9ee855bda..804506358 100644 --- a/example/executors/userRecordLog.ts +++ b/example/executors/userRecordLog.ts @@ -15,6 +15,8 @@ export default async ({ newRecord }: { newRecord: t.infer }) => { .values({ userID: newRecord.id, message: `User created: ${record?.name} (${record?.email})`, + createdAt: new Date(), + updatedAt: new Date(), }) .execute(); }; diff --git a/example/generated/enums.ts b/example/generated/enums.ts index a2e42e216..ea7a59624 100644 --- a/example/generated/enums.ts +++ b/example/generated/enums.ts @@ -14,6 +14,13 @@ export const InvoiceStatus = { } as const; export type InvoiceStatus = (typeof InvoiceStatus)[keyof typeof InvoiceStatus]; +export const ProductCategory = { + "electronics": "electronics", + "clothing": "clothing", + "food": "food" +} as const; +export type ProductCategory = (typeof ProductCategory)[keyof typeof ProductCategory]; + export const PurchaseOrderAttachedFilesType = { "text": "text", "image": "image" diff --git a/example/generated/tailordb.ts b/example/generated/tailordb.ts index db9ea0e47..dc3b7a876 100644 --- a/example/generated/tailordb.ts +++ b/example/generated/tailordb.ts @@ -27,7 +27,7 @@ export interface Namespace { fullAddress: Generated; state: string; createdAt: Generated; - updatedAt: Timestamp | null; + updatedAt: Generated; } Invoice: { @@ -38,7 +38,7 @@ export interface Namespace { sequentialId: Serial; status: "draft" | "sent" | "paid" | "cancelled" | null; createdAt: Generated; - updatedAt: Timestamp | null; + updatedAt: Generated; } NestedProfile: { @@ -57,7 +57,19 @@ export interface Namespace { }>; archived: boolean | null; createdAt: Generated; - updatedAt: Timestamp | null; + updatedAt: Generated; + } + + Product: { + id: Generated; + name: string; + sku: string; + price: number; + stock: number; + category: "electronics" | "clothing" | "food"; + supplierId: string; + createdAt: Generated; + updatedAt: Generated; } PurchaseOrder: { @@ -73,7 +85,7 @@ export interface Namespace { type: "text" | "image"; }[]; createdAt: Generated; - updatedAt: Timestamp | null; + updatedAt: Generated; } SalesOrder: { @@ -86,7 +98,7 @@ export interface Namespace { cancelReason: string | null; canceledAt: Timestamp | null; createdAt: Generated; - updatedAt: Timestamp | null; + updatedAt: Generated; } SalesOrderCreated: { @@ -115,7 +127,7 @@ export interface Namespace { state: "Alabama" | "Alaska"; city: string; createdAt: Generated; - updatedAt: Timestamp | null; + updatedAt: Generated; } User: { @@ -126,7 +138,7 @@ export interface Namespace { department: string | null; role: "MANAGER" | "STAFF"; createdAt: Generated; - updatedAt: Timestamp | null; + updatedAt: Generated; } UserLog: { @@ -134,7 +146,7 @@ export interface Namespace { userID: string; message: string; createdAt: Generated; - updatedAt: Timestamp | null; + updatedAt: Generated; } UserSetting: { @@ -142,7 +154,7 @@ export interface Namespace { language: "jp" | "en"; userID: string; createdAt: Generated; - updatedAt: Timestamp | null; + updatedAt: Generated; } }, "analyticsdb": { @@ -150,7 +162,7 @@ export interface Namespace { id: Generated; name: "CLICK" | "VIEW" | "PURCHASE"; createdAt: Generated; - updatedAt: Timestamp | null; + updatedAt: Generated; } } } diff --git a/example/migrations/0001/diff.json b/example/migrations/0001/diff.json new file mode 100644 index 000000000..9530ad042 --- /dev/null +++ b/example/migrations/0001/diff.json @@ -0,0 +1,212 @@ +{ + "version": 1, + "namespace": "tailordb", + "createdAt": "2026-04-04T23:52:28.003Z", + "changes": [ + { + "kind": "type_added", + "typeName": "Product", + "after": { + "name": "Product", + "fields": { + "id": { + "type": "uuid", + "required": true + }, + "name": { + "type": "string", + "required": true, + "description": "Product name" + }, + "sku": { + "type": "string", + "required": true, + "index": true, + "unique": true, + "description": "Stock keeping unit" + }, + "price": { + "type": "float", + "required": true + }, + "stock": { + "type": "integer", + "required": true, + "index": true + }, + "category": { + "type": "enum", + "required": true, + "allowedValues": [ + { + "value": "electronics" + }, + { + "value": "clothing" + }, + { + "value": "food" + } + ] + }, + "supplierId": { + "type": "uuid", + "required": true, + "index": true, + "foreignKey": true, + "foreignKeyType": "Supplier", + "foreignKeyField": "id" + }, + "createdAt": { + "type": "datetime", + "required": true, + "description": "Record creation timestamp", + "hooks": { + "create": { + "expr": "(() => /* @__PURE__ */ new Date())({ value: _value, data: _data, user: { id: user.id, type: user.type, workspaceId: user.workspace_id, attributes: user.attribute_map, attributeList: user.attributes } })" + } + } + }, + "updatedAt": { + "type": "datetime", + "required": false, + "description": "Record last update timestamp", + "hooks": { + "update": { + "expr": "(() => /* @__PURE__ */ new Date())({ value: _value, data: _data, user: { id: user.id, type: user.type, workspaceId: user.workspace_id, attributes: user.attribute_map, attributeList: user.attributes } })" + } + } + } + }, + "pluralForm": "Products", + "description": "Product catalog entry", + "settings": {}, + "forwardRelationships": { + "supplier": { + "targetType": "Supplier", + "targetField": "supplierId", + "sourceField": "id", + "isArray": false, + "description": "" + } + }, + "permissions": { + "record": { + "create": [ + { + "conditions": [ + [ + { + "user": "role" + }, + "eq", + "MANAGER" + ] + ], + "permit": "allow" + } + ], + "read": [ + { + "conditions": [ + [ + { + "user": "role" + }, + "eq", + "MANAGER" + ] + ], + "permit": "allow" + }, + { + "conditions": [ + [ + { + "user": "_loggedIn" + }, + "eq", + true + ] + ], + "permit": "allow" + } + ], + "update": [ + { + "conditions": [ + [ + { + "user": "role" + }, + "eq", + "MANAGER" + ] + ], + "permit": "allow" + } + ], + "delete": [ + { + "conditions": [ + [ + { + "user": "role" + }, + "eq", + "MANAGER" + ] + ], + "permit": "allow" + } + ] + }, + "gql": [ + { + "conditions": [ + [ + { + "user": "role" + }, + "eq", + "MANAGER" + ] + ], + "actions": ["create", "read", "update", "delete", "aggregate", "bulkUpsert"], + "permit": "allow" + }, + { + "conditions": [ + [ + { + "user": "_loggedIn" + }, + "eq", + true + ] + ], + "actions": ["read"], + "permit": "allow" + } + ] + } + } + }, + { + "kind": "relationship_added", + "typeName": "Supplier", + "relationshipName": "products", + "relationshipType": "backward", + "after": { + "targetType": "Product", + "targetField": "supplierId", + "sourceField": "id", + "isArray": true, + "description": "Product catalog entry" + } + } + ], + "hasBreakingChanges": false, + "breakingChanges": [], + "requiresMigrationScript": false +} diff --git a/example/migrations/0002/diff.json b/example/migrations/0002/diff.json new file mode 100644 index 000000000..9d3fe0f2f --- /dev/null +++ b/example/migrations/0002/diff.json @@ -0,0 +1,554 @@ +{ + "version": 1, + "namespace": "tailordb", + "createdAt": "2026-04-15T02:51:59.393Z", + "changes": [ + { + "kind": "field_modified", + "typeName": "Customer", + "fieldName": "name", + "before": { + "type": "string", + "required": true, + "validate": [ + { + "script": { + "expr": "(({value})=>value.length>5)({ value: _value, data: _data, user: { id: user.id, type: user.type, workspaceId: user.workspace_id, attributes: user.attribute_map, attributeList: user.attributes } })" + }, + "errorMessage": "Name must be longer than 5 characters" + } + ] + }, + "after": { + "type": "string", + "required": true + } + }, + { + "kind": "field_modified", + "typeName": "Customer", + "fieldName": "city", + "before": { + "type": "string", + "required": false, + "validate": [ + { + "script": { + "expr": "(({value})=>value?value.length>1:true)({ value: _value, data: _data, user: { id: user.id, type: user.type, workspaceId: user.workspace_id, attributes: user.attribute_map, attributeList: user.attributes } })" + }, + "errorMessage": "failed by `({value})=>value?value.length>1:true`" + }, + { + "script": { + "expr": "(({value})=>value?value.length<100:true)({ value: _value, data: _data, user: { id: user.id, type: user.type, workspaceId: user.workspace_id, attributes: user.attribute_map, attributeList: user.attributes } })" + }, + "errorMessage": "failed by `({value})=>value?value.length<100:true`" + } + ] + }, + "after": { + "type": "string", + "required": false + } + }, + { + "kind": "field_modified", + "typeName": "Customer", + "fieldName": "fullAddress", + "before": { + "type": "string", + "required": true, + "hooks": { + "create": { + "expr": "(({data})=>`${data.postalCode} ${data.address} ${data.city}`)({ value: _value, data: _data, user: { id: user.id, type: user.type, workspaceId: user.workspace_id, attributes: user.attribute_map, attributeList: user.attributes } })" + }, + "update": { + "expr": "(({data})=>`${data.postalCode} ${data.address} ${data.city}`)({ value: _value, data: _data, user: { id: user.id, type: user.type, workspaceId: user.workspace_id, attributes: user.attribute_map, attributeList: user.attributes } })" + } + } + }, + "after": { + "type": "string", + "required": true + } + }, + { + "kind": "field_modified", + "typeName": "Customer", + "fieldName": "createdAt", + "before": { + "type": "datetime", + "required": true, + "description": "Record creation timestamp", + "hooks": { + "create": { + "expr": "(() => /* @__PURE__ */ new Date())({ value: _value, data: _data, user: { id: user.id, type: user.type, workspaceId: user.workspace_id, attributes: user.attribute_map, attributeList: user.attributes } })" + } + } + }, + "after": { + "type": "datetime", + "required": true, + "description": "Record creation timestamp" + } + }, + { + "kind": "field_modified", + "typeName": "Customer", + "fieldName": "updatedAt", + "before": { + "type": "datetime", + "required": false, + "description": "Record last update timestamp", + "hooks": { + "update": { + "expr": "(() => /* @__PURE__ */ new Date())({ value: _value, data: _data, user: { id: user.id, type: user.type, workspaceId: user.workspace_id, attributes: user.attribute_map, attributeList: user.attributes } })" + } + } + }, + "after": { + "type": "datetime", + "required": false, + "description": "Record last update timestamp" + } + }, + { + "kind": "field_modified", + "typeName": "Invoice", + "fieldName": "createdAt", + "before": { + "type": "datetime", + "required": true, + "description": "Record creation timestamp", + "hooks": { + "create": { + "expr": "(() => /* @__PURE__ */ new Date())({ value: _value, data: _data, user: { id: user.id, type: user.type, workspaceId: user.workspace_id, attributes: user.attribute_map, attributeList: user.attributes } })" + } + } + }, + "after": { + "type": "datetime", + "required": true, + "description": "Record creation timestamp" + } + }, + { + "kind": "field_modified", + "typeName": "Invoice", + "fieldName": "updatedAt", + "before": { + "type": "datetime", + "required": false, + "description": "Record last update timestamp", + "hooks": { + "update": { + "expr": "(() => /* @__PURE__ */ new Date())({ value: _value, data: _data, user: { id: user.id, type: user.type, workspaceId: user.workspace_id, attributes: user.attribute_map, attributeList: user.attributes } })" + } + } + }, + "after": { + "type": "datetime", + "required": false, + "description": "Record last update timestamp" + } + }, + { + "kind": "field_modified", + "typeName": "NestedProfile", + "fieldName": "createdAt", + "before": { + "type": "datetime", + "required": true, + "description": "Record creation timestamp", + "hooks": { + "create": { + "expr": "(() => /* @__PURE__ */ new Date())({ value: _value, data: _data, user: { id: user.id, type: user.type, workspaceId: user.workspace_id, attributes: user.attribute_map, attributeList: user.attributes } })" + } + } + }, + "after": { + "type": "datetime", + "required": true, + "description": "Record creation timestamp" + } + }, + { + "kind": "field_modified", + "typeName": "NestedProfile", + "fieldName": "updatedAt", + "before": { + "type": "datetime", + "required": false, + "description": "Record last update timestamp", + "hooks": { + "update": { + "expr": "(() => /* @__PURE__ */ new Date())({ value: _value, data: _data, user: { id: user.id, type: user.type, workspaceId: user.workspace_id, attributes: user.attribute_map, attributeList: user.attributes } })" + } + } + }, + "after": { + "type": "datetime", + "required": false, + "description": "Record last update timestamp" + } + }, + { + "kind": "field_modified", + "typeName": "Product", + "fieldName": "createdAt", + "before": { + "type": "datetime", + "required": true, + "description": "Record creation timestamp", + "hooks": { + "create": { + "expr": "(() => /* @__PURE__ */ new Date())({ value: _value, data: _data, user: { id: user.id, type: user.type, workspaceId: user.workspace_id, attributes: user.attribute_map, attributeList: user.attributes } })" + } + } + }, + "after": { + "type": "datetime", + "required": true, + "description": "Record creation timestamp" + } + }, + { + "kind": "field_modified", + "typeName": "Product", + "fieldName": "updatedAt", + "before": { + "type": "datetime", + "required": false, + "description": "Record last update timestamp", + "hooks": { + "update": { + "expr": "(() => /* @__PURE__ */ new Date())({ value: _value, data: _data, user: { id: user.id, type: user.type, workspaceId: user.workspace_id, attributes: user.attribute_map, attributeList: user.attributes } })" + } + } + }, + "after": { + "type": "datetime", + "required": false, + "description": "Record last update timestamp" + } + }, + { + "kind": "field_modified", + "typeName": "PurchaseOrder", + "fieldName": "attachedFiles", + "before": { + "type": "nested", + "required": true, + "array": true, + "fields": { + "id": { + "type": "uuid", + "required": true + }, + "name": { + "type": "string", + "required": true + }, + "size": { + "type": "integer", + "required": true, + "validate": [ + { + "script": { + "expr": "(({value})=>value>0)({ value: _value, data: _data, user: { id: user.id, type: user.type, workspaceId: user.workspace_id, attributes: user.attribute_map, attributeList: user.attributes } })" + }, + "errorMessage": "failed by `({value})=>value>0`" + } + ] + }, + "type": { + "type": "enum", + "required": true, + "allowedValues": [ + { + "value": "text" + }, + { + "value": "image" + } + ] + } + } + }, + "after": { + "type": "nested", + "required": true, + "array": true, + "fields": { + "id": { + "type": "uuid", + "required": true + }, + "name": { + "type": "string", + "required": true + }, + "size": { + "type": "integer", + "required": true + }, + "type": { + "type": "enum", + "required": true, + "allowedValues": [ + { + "value": "text" + }, + { + "value": "image" + } + ] + } + } + } + }, + { + "kind": "field_modified", + "typeName": "PurchaseOrder", + "fieldName": "createdAt", + "before": { + "type": "datetime", + "required": true, + "description": "Record creation timestamp", + "hooks": { + "create": { + "expr": "(() => /* @__PURE__ */ new Date())({ value: _value, data: _data, user: { id: user.id, type: user.type, workspaceId: user.workspace_id, attributes: user.attribute_map, attributeList: user.attributes } })" + } + } + }, + "after": { + "type": "datetime", + "required": true, + "description": "Record creation timestamp" + } + }, + { + "kind": "field_modified", + "typeName": "PurchaseOrder", + "fieldName": "updatedAt", + "before": { + "type": "datetime", + "required": false, + "description": "Record last update timestamp", + "hooks": { + "update": { + "expr": "(() => /* @__PURE__ */ new Date())({ value: _value, data: _data, user: { id: user.id, type: user.type, workspaceId: user.workspace_id, attributes: user.attribute_map, attributeList: user.attributes } })" + } + } + }, + "after": { + "type": "datetime", + "required": false, + "description": "Record last update timestamp" + } + }, + { + "kind": "field_modified", + "typeName": "SalesOrder", + "fieldName": "createdAt", + "before": { + "type": "datetime", + "required": true, + "description": "Record creation timestamp", + "hooks": { + "create": { + "expr": "(() => /* @__PURE__ */ new Date())({ value: _value, data: _data, user: { id: user.id, type: user.type, workspaceId: user.workspace_id, attributes: user.attribute_map, attributeList: user.attributes } })" + } + } + }, + "after": { + "type": "datetime", + "required": true, + "description": "Record creation timestamp" + } + }, + { + "kind": "field_modified", + "typeName": "SalesOrder", + "fieldName": "updatedAt", + "before": { + "type": "datetime", + "required": false, + "description": "Record last update timestamp", + "hooks": { + "update": { + "expr": "(() => /* @__PURE__ */ new Date())({ value: _value, data: _data, user: { id: user.id, type: user.type, workspaceId: user.workspace_id, attributes: user.attribute_map, attributeList: user.attributes } })" + } + } + }, + "after": { + "type": "datetime", + "required": false, + "description": "Record last update timestamp" + } + }, + { + "kind": "field_modified", + "typeName": "Supplier", + "fieldName": "createdAt", + "before": { + "type": "datetime", + "required": true, + "description": "Record creation timestamp", + "hooks": { + "create": { + "expr": "(() => /* @__PURE__ */ new Date())({ value: _value, data: _data, user: { id: user.id, type: user.type, workspaceId: user.workspace_id, attributes: user.attribute_map, attributeList: user.attributes } })" + } + } + }, + "after": { + "type": "datetime", + "required": true, + "description": "Record creation timestamp" + } + }, + { + "kind": "field_modified", + "typeName": "Supplier", + "fieldName": "updatedAt", + "before": { + "type": "datetime", + "required": false, + "description": "Record last update timestamp", + "hooks": { + "update": { + "expr": "(() => /* @__PURE__ */ new Date())({ value: _value, data: _data, user: { id: user.id, type: user.type, workspaceId: user.workspace_id, attributes: user.attribute_map, attributeList: user.attributes } })" + } + } + }, + "after": { + "type": "datetime", + "required": false, + "description": "Record last update timestamp" + } + }, + { + "kind": "field_modified", + "typeName": "User", + "fieldName": "createdAt", + "before": { + "type": "datetime", + "required": true, + "description": "Record creation timestamp", + "hooks": { + "create": { + "expr": "(() => /* @__PURE__ */ new Date())({ value: _value, data: _data, user: { id: user.id, type: user.type, workspaceId: user.workspace_id, attributes: user.attribute_map, attributeList: user.attributes } })" + } + } + }, + "after": { + "type": "datetime", + "required": true, + "description": "Record creation timestamp" + } + }, + { + "kind": "field_modified", + "typeName": "User", + "fieldName": "updatedAt", + "before": { + "type": "datetime", + "required": false, + "description": "Record last update timestamp", + "hooks": { + "update": { + "expr": "(() => /* @__PURE__ */ new Date())({ value: _value, data: _data, user: { id: user.id, type: user.type, workspaceId: user.workspace_id, attributes: user.attribute_map, attributeList: user.attributes } })" + } + } + }, + "after": { + "type": "datetime", + "required": false, + "description": "Record last update timestamp" + } + }, + { + "kind": "field_modified", + "typeName": "UserLog", + "fieldName": "createdAt", + "before": { + "type": "datetime", + "required": true, + "description": "Record creation timestamp", + "hooks": { + "create": { + "expr": "(() => /* @__PURE__ */ new Date())({ value: _value, data: _data, user: { id: user.id, type: user.type, workspaceId: user.workspace_id, attributes: user.attribute_map, attributeList: user.attributes } })" + } + } + }, + "after": { + "type": "datetime", + "required": true, + "description": "Record creation timestamp" + } + }, + { + "kind": "field_modified", + "typeName": "UserLog", + "fieldName": "updatedAt", + "before": { + "type": "datetime", + "required": false, + "description": "Record last update timestamp", + "hooks": { + "update": { + "expr": "(() => /* @__PURE__ */ new Date())({ value: _value, data: _data, user: { id: user.id, type: user.type, workspaceId: user.workspace_id, attributes: user.attribute_map, attributeList: user.attributes } })" + } + } + }, + "after": { + "type": "datetime", + "required": false, + "description": "Record last update timestamp" + } + }, + { + "kind": "field_modified", + "typeName": "UserSetting", + "fieldName": "createdAt", + "before": { + "type": "datetime", + "required": true, + "description": "Record creation timestamp", + "hooks": { + "create": { + "expr": "(() => /* @__PURE__ */ new Date())({ value: _value, data: _data, user: { id: user.id, type: user.type, workspaceId: user.workspace_id, attributes: user.attribute_map, attributeList: user.attributes } })" + } + } + }, + "after": { + "type": "datetime", + "required": true, + "description": "Record creation timestamp" + } + }, + { + "kind": "field_modified", + "typeName": "UserSetting", + "fieldName": "updatedAt", + "before": { + "type": "datetime", + "required": false, + "description": "Record last update timestamp", + "hooks": { + "update": { + "expr": "(() => /* @__PURE__ */ new Date())({ value: _value, data: _data, user: { id: user.id, type: user.type, workspaceId: user.workspace_id, attributes: user.attribute_map, attributeList: user.attributes } })" + } + } + }, + "after": { + "type": "datetime", + "required": false, + "description": "Record last update timestamp" + } + } + ], + "hasBreakingChanges": false, + "breakingChanges": [], + "requiresMigrationScript": false +} diff --git a/example/migrations/0003/diff.json b/example/migrations/0003/diff.json new file mode 100644 index 000000000..a0fdfa1e3 --- /dev/null +++ b/example/migrations/0003/diff.json @@ -0,0 +1,410 @@ +{ + "version": 1, + "namespace": "tailordb", + "createdAt": "2026-04-15T03:25:31.444Z", + "changes": [ + { + "kind": "field_modified", + "typeName": "Customer", + "fieldName": "createdAt", + "before": { + "type": "datetime", + "required": true, + "description": "Record creation timestamp" + }, + "after": { + "type": "datetime", + "required": true, + "description": "Record creation timestamp", + "hooks": { + "create": { + "expr": "new Date()" + } + } + } + }, + { + "kind": "field_modified", + "typeName": "Customer", + "fieldName": "updatedAt", + "before": { + "type": "datetime", + "required": false, + "description": "Record last update timestamp" + }, + "after": { + "type": "datetime", + "required": false, + "description": "Record last update timestamp", + "hooks": { + "update": { + "expr": "new Date()" + } + } + } + }, + { + "kind": "field_modified", + "typeName": "Invoice", + "fieldName": "createdAt", + "before": { + "type": "datetime", + "required": true, + "description": "Record creation timestamp" + }, + "after": { + "type": "datetime", + "required": true, + "description": "Record creation timestamp", + "hooks": { + "create": { + "expr": "new Date()" + } + } + } + }, + { + "kind": "field_modified", + "typeName": "Invoice", + "fieldName": "updatedAt", + "before": { + "type": "datetime", + "required": false, + "description": "Record last update timestamp" + }, + "after": { + "type": "datetime", + "required": false, + "description": "Record last update timestamp", + "hooks": { + "update": { + "expr": "new Date()" + } + } + } + }, + { + "kind": "field_modified", + "typeName": "NestedProfile", + "fieldName": "createdAt", + "before": { + "type": "datetime", + "required": true, + "description": "Record creation timestamp" + }, + "after": { + "type": "datetime", + "required": true, + "description": "Record creation timestamp", + "hooks": { + "create": { + "expr": "new Date()" + } + } + } + }, + { + "kind": "field_modified", + "typeName": "NestedProfile", + "fieldName": "updatedAt", + "before": { + "type": "datetime", + "required": false, + "description": "Record last update timestamp" + }, + "after": { + "type": "datetime", + "required": false, + "description": "Record last update timestamp", + "hooks": { + "update": { + "expr": "new Date()" + } + } + } + }, + { + "kind": "field_modified", + "typeName": "Product", + "fieldName": "createdAt", + "before": { + "type": "datetime", + "required": true, + "description": "Record creation timestamp" + }, + "after": { + "type": "datetime", + "required": true, + "description": "Record creation timestamp", + "hooks": { + "create": { + "expr": "new Date()" + } + } + } + }, + { + "kind": "field_modified", + "typeName": "Product", + "fieldName": "updatedAt", + "before": { + "type": "datetime", + "required": false, + "description": "Record last update timestamp" + }, + "after": { + "type": "datetime", + "required": false, + "description": "Record last update timestamp", + "hooks": { + "update": { + "expr": "new Date()" + } + } + } + }, + { + "kind": "field_modified", + "typeName": "PurchaseOrder", + "fieldName": "createdAt", + "before": { + "type": "datetime", + "required": true, + "description": "Record creation timestamp" + }, + "after": { + "type": "datetime", + "required": true, + "description": "Record creation timestamp", + "hooks": { + "create": { + "expr": "new Date()" + } + } + } + }, + { + "kind": "field_modified", + "typeName": "PurchaseOrder", + "fieldName": "updatedAt", + "before": { + "type": "datetime", + "required": false, + "description": "Record last update timestamp" + }, + "after": { + "type": "datetime", + "required": false, + "description": "Record last update timestamp", + "hooks": { + "update": { + "expr": "new Date()" + } + } + } + }, + { + "kind": "field_modified", + "typeName": "SalesOrder", + "fieldName": "createdAt", + "before": { + "type": "datetime", + "required": true, + "description": "Record creation timestamp" + }, + "after": { + "type": "datetime", + "required": true, + "description": "Record creation timestamp", + "hooks": { + "create": { + "expr": "new Date()" + } + } + } + }, + { + "kind": "field_modified", + "typeName": "SalesOrder", + "fieldName": "updatedAt", + "before": { + "type": "datetime", + "required": false, + "description": "Record last update timestamp" + }, + "after": { + "type": "datetime", + "required": false, + "description": "Record last update timestamp", + "hooks": { + "update": { + "expr": "new Date()" + } + } + } + }, + { + "kind": "field_modified", + "typeName": "Supplier", + "fieldName": "createdAt", + "before": { + "type": "datetime", + "required": true, + "description": "Record creation timestamp" + }, + "after": { + "type": "datetime", + "required": true, + "description": "Record creation timestamp", + "hooks": { + "create": { + "expr": "new Date()" + } + } + } + }, + { + "kind": "field_modified", + "typeName": "Supplier", + "fieldName": "updatedAt", + "before": { + "type": "datetime", + "required": false, + "description": "Record last update timestamp" + }, + "after": { + "type": "datetime", + "required": false, + "description": "Record last update timestamp", + "hooks": { + "update": { + "expr": "new Date()" + } + } + } + }, + { + "kind": "field_modified", + "typeName": "User", + "fieldName": "createdAt", + "before": { + "type": "datetime", + "required": true, + "description": "Record creation timestamp" + }, + "after": { + "type": "datetime", + "required": true, + "description": "Record creation timestamp", + "hooks": { + "create": { + "expr": "new Date()" + } + } + } + }, + { + "kind": "field_modified", + "typeName": "User", + "fieldName": "updatedAt", + "before": { + "type": "datetime", + "required": false, + "description": "Record last update timestamp" + }, + "after": { + "type": "datetime", + "required": false, + "description": "Record last update timestamp", + "hooks": { + "update": { + "expr": "new Date()" + } + } + } + }, + { + "kind": "field_modified", + "typeName": "UserLog", + "fieldName": "createdAt", + "before": { + "type": "datetime", + "required": true, + "description": "Record creation timestamp" + }, + "after": { + "type": "datetime", + "required": true, + "description": "Record creation timestamp", + "hooks": { + "create": { + "expr": "new Date()" + } + } + } + }, + { + "kind": "field_modified", + "typeName": "UserLog", + "fieldName": "updatedAt", + "before": { + "type": "datetime", + "required": false, + "description": "Record last update timestamp" + }, + "after": { + "type": "datetime", + "required": false, + "description": "Record last update timestamp", + "hooks": { + "update": { + "expr": "new Date()" + } + } + } + }, + { + "kind": "field_modified", + "typeName": "UserSetting", + "fieldName": "createdAt", + "before": { + "type": "datetime", + "required": true, + "description": "Record creation timestamp" + }, + "after": { + "type": "datetime", + "required": true, + "description": "Record creation timestamp", + "hooks": { + "create": { + "expr": "new Date()" + } + } + } + }, + { + "kind": "field_modified", + "typeName": "UserSetting", + "fieldName": "updatedAt", + "before": { + "type": "datetime", + "required": false, + "description": "Record last update timestamp" + }, + "after": { + "type": "datetime", + "required": false, + "description": "Record last update timestamp", + "hooks": { + "update": { + "expr": "new Date()" + } + } + } + } + ], + "hasBreakingChanges": false, + "breakingChanges": [], + "requiresMigrationScript": false +} diff --git a/example/migrations/0004/diff.json b/example/migrations/0004/diff.json new file mode 100644 index 000000000..b5933b7b1 --- /dev/null +++ b/example/migrations/0004/diff.json @@ -0,0 +1,40 @@ +{ + "version": 1, + "namespace": "tailordb", + "createdAt": "2026-05-20T19:18:42.305Z", + "changes": [ + { + "kind": "type_modified", + "typeName": "Customer", + "reason": "record-level hooks changed, record-level validators changed", + "before": {}, + "after": { + "hooks": { + "create": { + "expr": "(({data})=>({...data,fullAddress:`${data.postalCode} ${data.address??\"\"} ${data.city??\"\"}`,createdAt:new Date}))({ value: _value, data: _data, user: { id: user.id, type: user.type, workspaceId: user.workspace_id, attributes: user.attribute_map, attributeList: user.attributes } })" + }, + "update": { + "expr": "(({data})=>({...data,fullAddress:`${data.postalCode} ${data.address??\"\"} ${data.city??\"\"}`,updatedAt:new Date}))({ value: _value, data: _data, user: { id: user.id, type: user.type, workspaceId: user.workspace_id, attributes: user.attribute_map, attributeList: user.attributes } })" + } + }, + "validate": [ + { + "script": { + "expr": "(({data})=>data.name.length>5)({ value: _value, data: _data, user: { id: user.id, type: user.type, workspaceId: user.workspace_id, attributes: user.attribute_map, attributeList: user.attributes } })" + }, + "errorMessage": "Name must be longer than 5 characters" + }, + { + "script": { + "expr": "(({data})=>data.city?data.city.length>1&&data.city.length<100:true)({ value: _value, data: _data, user: { id: user.id, type: user.type, workspaceId: user.workspace_id, attributes: user.attribute_map, attributeList: user.attributes } })" + }, + "errorMessage": "failed by `({data})=>data.city?data.city.length>1&&data.city.length<100:true`" + } + ] + } + } + ], + "hasBreakingChanges": false, + "breakingChanges": [], + "requiresMigrationScript": false +} diff --git a/example/migrations/0005/diff.json b/example/migrations/0005/diff.json new file mode 100644 index 000000000..df0696c98 --- /dev/null +++ b/example/migrations/0005/diff.json @@ -0,0 +1,120 @@ +{ + "version": 1, + "namespace": "tailordb", + "createdAt": "2026-05-21T09:41:38.882Z", + "changes": [ + { + "kind": "field_modified", + "typeName": "Customer", + "fieldName": "fullAddress", + "before": { + "type": "string", + "required": true + }, + "after": { + "type": "string", + "required": true, + "hooks": { + "create": { + "expr": "((({data})=>({fullAddress:`${data.postalCode} ${data.address??\"\"} ${data.city??\"\"}`,createdAt:new Date}))({ data: _data, user: { id: user.id, type: user.type, workspaceId: user.workspace_id, attributes: user.attribute_map, attributeList: user.attributes } }))[\"fullAddress\"]" + }, + "update": { + "expr": "((({data})=>({fullAddress:`${data.postalCode} ${data.address??\"\"} ${data.city??\"\"}`,updatedAt:new Date}))({ data: _data, user: { id: user.id, type: user.type, workspaceId: user.workspace_id, attributes: user.attribute_map, attributeList: user.attributes } }))[\"fullAddress\"]" + } + } + } + }, + { + "kind": "field_modified", + "typeName": "Customer", + "fieldName": "createdAt", + "before": { + "type": "datetime", + "required": true, + "description": "Record creation timestamp", + "hooks": { + "create": { + "expr": "new Date()" + } + } + }, + "after": { + "type": "datetime", + "required": true, + "description": "Record creation timestamp", + "hooks": { + "create": { + "expr": "((({data})=>({fullAddress:`${data.postalCode} ${data.address??\"\"} ${data.city??\"\"}`,createdAt:new Date}))({ data: _data, user: { id: user.id, type: user.type, workspaceId: user.workspace_id, attributes: user.attribute_map, attributeList: user.attributes } }))[\"createdAt\"]" + } + } + } + }, + { + "kind": "field_modified", + "typeName": "Customer", + "fieldName": "updatedAt", + "before": { + "type": "datetime", + "required": false, + "description": "Record last update timestamp", + "hooks": { + "update": { + "expr": "new Date()" + } + } + }, + "after": { + "type": "datetime", + "required": false, + "description": "Record last update timestamp", + "hooks": { + "update": { + "expr": "((({data})=>({fullAddress:`${data.postalCode} ${data.address??\"\"} ${data.city??\"\"}`,updatedAt:new Date}))({ data: _data, user: { id: user.id, type: user.type, workspaceId: user.workspace_id, attributes: user.attribute_map, attributeList: user.attributes } }))[\"updatedAt\"]" + } + } + } + }, + { + "kind": "type_modified", + "typeName": "Customer", + "reason": "record-level validators changed", + "before": { + "validate": [ + { + "script": { + "expr": "(({data})=>data.name.length>5)({ value: _value, data: _data, user: { id: user.id, type: user.type, workspaceId: user.workspace_id, attributes: user.attribute_map, attributeList: user.attributes } })" + }, + "errorMessage": "Name must be longer than 5 characters" + }, + { + "script": { + "expr": "(({data})=>data.city?data.city.length>1&&data.city.length<100:true)({ value: _value, data: _data, user: { id: user.id, type: user.type, workspaceId: user.workspace_id, attributes: user.attribute_map, attributeList: user.attributes } })" + }, + "errorMessage": "failed by `({data})=>data.city?data.city.length>1&&data.city.length<100:true`" + } + ] + }, + "after": { + "validate": [ + { + "script": { + "expr": "(((({data})=>data.name.length>5)({ data: _input, user: { id: user.id, type: user.type, workspaceId: user.workspace_id, attributes: user.attribute_map, attributeList: user.attributes } })) ? {} : { \"_record_0\": \"Name must be longer than 5 characters\" })" + }, + "errorMessage": "Name must be longer than 5 characters" + }, + { + "script": { + "expr": "(((({data})=>data.city?data.city.length>1&&data.city.length<100:true)({ data: _input, user: { id: user.id, type: user.type, workspaceId: user.workspace_id, attributes: user.attribute_map, attributeList: user.attributes } })) ? {} : { \"_record_1\": \"failed by `({data})=>data.city?data.city.length>1&&data.city.length<100:true`\" })" + }, + "errorMessage": "failed by `({data})=>data.city?data.city.length>1&&data.city.length<100:true`" + } + ] + } + } + ], + "hasBreakingChanges": false, + "breakingChanges": [], + "hasWarnings": false, + "warnings": [], + "requiresMigrationScript": false +} diff --git a/example/resolvers/insertNestedProfileWithDate.ts b/example/resolvers/insertNestedProfileWithDate.ts index 8ca0e1101..4add73cce 100644 --- a/example/resolvers/insertNestedProfileWithDate.ts +++ b/example/resolvers/insertNestedProfileWithDate.ts @@ -30,6 +30,8 @@ export default createResolver({ created: new Date(), version: 1, }, + createdAt: new Date(), + updatedAt: new Date(), }) .returning("id") .executeTakeFirstOrThrow(); diff --git a/example/seed/data/Customer.jsonl b/example/seed/data/Customer.jsonl index bbb6aacc5..1d7f20fcb 100644 --- a/example/seed/data/Customer.jsonl +++ b/example/seed/data/Customer.jsonl @@ -1,5 +1,5 @@ -{"id":"aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa","name":"Acme Corporation","email":"contact@acme.com","phone":"03-1234-5678","country":"Japan","postalCode":"100-0001","address":"Chiyoda-ku","city":"Tokyo","state":"Tokyo"} -{"id":"bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb","name":"Global Tech Inc","email":"info@globaltech.com","phone":"03-9876-5432","country":"Japan","postalCode":"150-0002","address":"Shibuya-ku","city":"Tokyo","state":"Tokyo"} -{"id":"cccccccc-cccc-cccc-cccc-cccccccccccc","name":"Enterprise Solutions Ltd","email":"sales@enterprise.com","phone":"06-1111-2222","country":"Japan","postalCode":"530-0001","address":"Kita-ku","city":"Osaka","state":"Osaka"} -{"id":"dddddddd-dddd-dddd-dddd-dddddddddddd","name":"Digital Services Co","email":"hello@digital.com","country":"Japan","postalCode":"810-0001","address":"Chuo-ku","city":"Fukuoka","state":"Fukuoka"} -{"id":"eeeeeeee-eeee-eeee-eeee-eeeeeeeeeeee","name":"Innovation Partners","email":"contact@innovation.com","phone":"011-3333-4444","country":"Japan","postalCode":"060-0001","address":"Chuo-ku","city":"Sapporo","state":"Hokkaido"} +{"id":"aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa","name":"Acme Corporation","email":"contact@acme.com","phone":"03-1234-5678","country":"Japan","postalCode":"100-0001","address":"Chiyoda-ku","city":"Tokyo","state":"Tokyo","fullAddress":"100-0001 Chiyoda-ku Tokyo"} +{"id":"bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb","name":"Global Tech Inc","email":"info@globaltech.com","phone":"03-9876-5432","country":"Japan","postalCode":"150-0002","address":"Shibuya-ku","city":"Tokyo","state":"Tokyo","fullAddress":"150-0002 Shibuya-ku Tokyo"} +{"id":"cccccccc-cccc-cccc-cccc-cccccccccccc","name":"Enterprise Solutions Ltd","email":"sales@enterprise.com","phone":"06-1111-2222","country":"Japan","postalCode":"530-0001","address":"Kita-ku","city":"Osaka","state":"Osaka","fullAddress":"530-0001 Kita-ku Osaka"} +{"id":"dddddddd-dddd-dddd-dddd-dddddddddddd","name":"Digital Services Co","email":"hello@digital.com","country":"Japan","postalCode":"810-0001","address":"Chuo-ku","city":"Fukuoka","state":"Fukuoka","fullAddress":"810-0001 Chuo-ku Fukuoka"} +{"id":"eeeeeeee-eeee-eeee-eeee-eeeeeeeeeeee","name":"Innovation Partners","email":"contact@innovation.com","phone":"011-3333-4444","country":"Japan","postalCode":"060-0001","address":"Chuo-ku","city":"Sapporo","state":"Hokkaido","fullAddress":"060-0001 Chuo-ku Sapporo"} diff --git a/example/seed/data/Customer.schema.ts b/example/seed/data/Customer.schema.ts index 756c2f29e..82c648fdf 100644 --- a/example/seed/data/Customer.schema.ts +++ b/example/seed/data/Customer.schema.ts @@ -11,5 +11,5 @@ const schemaType = t.object({ const hook = createTailorDBHook(customer); export const schema = defineSchema( - createStandardSchema(schemaType, hook), + createStandardSchema(schemaType, hook, customer.metadata?.validate), ); diff --git a/example/seed/data/Event.schema.ts b/example/seed/data/Event.schema.ts index 0bc3d8691..45d283ef0 100644 --- a/example/seed/data/Event.schema.ts +++ b/example/seed/data/Event.schema.ts @@ -11,5 +11,5 @@ const schemaType = t.object({ const hook = createTailorDBHook(event); export const schema = defineSchema( - createStandardSchema(schemaType, hook), + createStandardSchema(schemaType, hook, event.metadata?.validate), ); diff --git a/example/seed/data/Invoice.schema.ts b/example/seed/data/Invoice.schema.ts index b25da906f..c2ea4369d 100644 --- a/example/seed/data/Invoice.schema.ts +++ b/example/seed/data/Invoice.schema.ts @@ -11,7 +11,7 @@ const schemaType = t.object({ const hook = createTailorDBHook(invoice); export const schema = defineSchema( - createStandardSchema(schemaType, hook), + createStandardSchema(schemaType, hook, invoice.metadata?.validate), { foreignKeys: [ {"column":"salesOrderID","references":{"table":"SalesOrder","column":"id"}}, diff --git a/example/seed/data/NestedProfile.schema.ts b/example/seed/data/NestedProfile.schema.ts index 2c52ea377..21ed32fa4 100644 --- a/example/seed/data/NestedProfile.schema.ts +++ b/example/seed/data/NestedProfile.schema.ts @@ -11,5 +11,5 @@ const schemaType = t.object({ const hook = createTailorDBHook(nestedProfile); export const schema = defineSchema( - createStandardSchema(schemaType, hook), + createStandardSchema(schemaType, hook, nestedProfile.metadata?.validate), ); diff --git a/example/seed/data/Product.jsonl b/example/seed/data/Product.jsonl new file mode 100644 index 000000000..e69de29bb diff --git a/example/seed/data/Product.schema.ts b/example/seed/data/Product.schema.ts new file mode 100644 index 000000000..a82dac774 --- /dev/null +++ b/example/seed/data/Product.schema.ts @@ -0,0 +1,23 @@ +import { t } from "@tailor-platform/sdk"; +import { defineSchema } from "@tailor-platform/sdk/seed"; +import { createTailorDBHook, createStandardSchema } from "@tailor-platform/sdk/test"; +import { product } from "../../tailordb/product"; + +const schemaType = t.object({ + ...product.pickFields(["id","createdAt"], { optional: true }), + ...product.omitFields(["id","createdAt"]), +}); + +const hook = createTailorDBHook(product); + +export const schema = defineSchema( + createStandardSchema(schemaType, hook, product.metadata?.validate), + { + foreignKeys: [ + {"column":"supplierId","references":{"table":"Supplier","column":"id"}}, + ], + indexes: [ + {"name":"product_sku_unique_idx","columns":["sku"],"unique":true}, + ], + } +); diff --git a/example/seed/data/PurchaseOrder.schema.ts b/example/seed/data/PurchaseOrder.schema.ts index 3a26ef3a3..c1392ee5f 100644 --- a/example/seed/data/PurchaseOrder.schema.ts +++ b/example/seed/data/PurchaseOrder.schema.ts @@ -11,7 +11,7 @@ const schemaType = t.object({ const hook = createTailorDBHook(purchaseOrder); export const schema = defineSchema( - createStandardSchema(schemaType, hook), + createStandardSchema(schemaType, hook, purchaseOrder.metadata?.validate), { foreignKeys: [ {"column":"supplierID","references":{"table":"Supplier","column":"id"}}, diff --git a/example/seed/data/SalesOrder.schema.ts b/example/seed/data/SalesOrder.schema.ts index 3f2533204..0f609a697 100644 --- a/example/seed/data/SalesOrder.schema.ts +++ b/example/seed/data/SalesOrder.schema.ts @@ -11,7 +11,7 @@ const schemaType = t.object({ const hook = createTailorDBHook(salesOrder); export const schema = defineSchema( - createStandardSchema(schemaType, hook), + createStandardSchema(schemaType, hook, salesOrder.metadata?.validate), { foreignKeys: [ {"column":"customerID","references":{"table":"Customer","column":"id"}}, diff --git a/example/seed/data/SalesOrderCreated.schema.ts b/example/seed/data/SalesOrderCreated.schema.ts index fe91cb3d5..a64f22500 100644 --- a/example/seed/data/SalesOrderCreated.schema.ts +++ b/example/seed/data/SalesOrderCreated.schema.ts @@ -11,5 +11,5 @@ const schemaType = t.object({ const hook = createTailorDBHook(salesOrderCreated); export const schema = defineSchema( - createStandardSchema(schemaType, hook), + createStandardSchema(schemaType, hook, salesOrderCreated.metadata?.validate), ); diff --git a/example/seed/data/Selfie.schema.ts b/example/seed/data/Selfie.schema.ts index 8563f8aaf..dba16cabc 100644 --- a/example/seed/data/Selfie.schema.ts +++ b/example/seed/data/Selfie.schema.ts @@ -11,7 +11,7 @@ const schemaType = t.object({ const hook = createTailorDBHook(selfie); export const schema = defineSchema( - createStandardSchema(schemaType, hook), + createStandardSchema(schemaType, hook, selfie.metadata?.validate), { foreignKeys: [ {"column":"parentID","references":{"table":"Selfie","column":"id"}}, diff --git a/example/seed/data/Supplier.schema.ts b/example/seed/data/Supplier.schema.ts index bac16337c..58502859d 100644 --- a/example/seed/data/Supplier.schema.ts +++ b/example/seed/data/Supplier.schema.ts @@ -11,5 +11,5 @@ const schemaType = t.object({ const hook = createTailorDBHook(supplier); export const schema = defineSchema( - createStandardSchema(schemaType, hook), + createStandardSchema(schemaType, hook, supplier.metadata?.validate), ); diff --git a/example/seed/data/User.schema.ts b/example/seed/data/User.schema.ts index 6c5a84d86..1e83d58bb 100644 --- a/example/seed/data/User.schema.ts +++ b/example/seed/data/User.schema.ts @@ -11,7 +11,7 @@ const schemaType = t.object({ const hook = createTailorDBHook(user); export const schema = defineSchema( - createStandardSchema(schemaType, hook), + createStandardSchema(schemaType, hook, user.metadata?.validate), { foreignKeys: [ {"column":"email","references":{"table":"_User","column":"name"}}, diff --git a/example/seed/data/UserLog.schema.ts b/example/seed/data/UserLog.schema.ts index 32dfc98fa..727c06e09 100644 --- a/example/seed/data/UserLog.schema.ts +++ b/example/seed/data/UserLog.schema.ts @@ -11,7 +11,7 @@ const schemaType = t.object({ const hook = createTailorDBHook(userLog); export const schema = defineSchema( - createStandardSchema(schemaType, hook), + createStandardSchema(schemaType, hook, userLog.metadata?.validate), { foreignKeys: [ {"column":"userID","references":{"table":"User","column":"id"}}, diff --git a/example/seed/data/UserSetting.schema.ts b/example/seed/data/UserSetting.schema.ts index 553d42c9e..503259c07 100644 --- a/example/seed/data/UserSetting.schema.ts +++ b/example/seed/data/UserSetting.schema.ts @@ -11,7 +11,7 @@ const schemaType = t.object({ const hook = createTailorDBHook(userSetting); export const schema = defineSchema( - createStandardSchema(schemaType, hook), + createStandardSchema(schemaType, hook, userSetting.metadata?.validate), { foreignKeys: [ {"column":"userID","references":{"table":"User","column":"id"}}, diff --git a/example/seed/exec.mjs b/example/seed/exec.mjs index 5daf85641..0a37435a4 100644 --- a/example/seed/exec.mjs +++ b/example/seed/exec.mjs @@ -144,6 +144,7 @@ const namespaceEntities = { "Customer", "Invoice", "NestedProfile", + "Product", "PurchaseOrder", "SalesOrder", "SalesOrderCreated", @@ -162,6 +163,7 @@ const namespaceDeps = { "Customer": [], "Invoice": ["SalesOrder"], "NestedProfile": [], + "Product": ["Supplier"], "PurchaseOrder": ["Supplier"], "SalesOrder": ["Customer", "User"], "SalesOrderCreated": [], diff --git a/example/tailordb/customer.ts b/example/tailordb/customer.ts index efde41cb1..698f6e1f0 100644 --- a/example/tailordb/customer.ts +++ b/example/tailordb/customer.ts @@ -9,22 +9,24 @@ export const customer = db country: db.string(), postalCode: db.string(), address: db.string({ optional: true }), - city: db.string({ optional: true }).validate( - ({ value }) => (value ? value.length > 1 : true), - ({ value }) => (value ? value.length < 100 : true), - ), + city: db.string({ optional: true }), fullAddress: db.string(), state: db.string(), ...db.fields.timestamps(), }) .hooks({ - fullAddress: { - create: ({ data }) => `${data.postalCode} ${data.address} ${data.city}`, - update: ({ data }) => `${data.postalCode} ${data.address} ${data.city}`, - }, - }) - .validate({ - name: [({ value }) => value.length > 5, "Name must be longer than 5 characters"], + create: ({ data }) => ({ + fullAddress: `${data.postalCode} ${data.address ?? ""} ${data.city ?? ""}`, + createdAt: new Date(), + }), + update: ({ data }) => ({ + fullAddress: `${data.postalCode} ${data.address ?? ""} ${data.city ?? ""}`, + updatedAt: new Date(), + }), }) + .validate([ + [({ data }) => data.name.length > 5, "Name must be longer than 5 characters"], + ({ data }) => (data.city ? data.city.length > 1 && data.city.length < 100 : true), + ]) .permission(defaultPermission) .gqlPermission(defaultGqlPermission); diff --git a/example/tailordb/file.ts b/example/tailordb/file.ts index 38726367c..5c21c6458 100644 --- a/example/tailordb/file.ts +++ b/example/tailordb/file.ts @@ -1,10 +1,14 @@ import { db } from "@tailor-platform/sdk"; +// NOTE: field-level `.validate()` has been removed from the public API. +// Nested object sub-fields can no longer carry inline validators; enforce +// constraints at the record level on the enclosing type via +// `db.type(...).validate(...)` instead. export const attachedFiles = db.object( { id: db.uuid(), name: db.string(), - size: db.int().validate(({ value }) => value > 0), + size: db.int(), type: db.enum(["text", "image"]), }, { array: true }, diff --git a/example/tailordb/product.ts b/example/tailordb/product.ts new file mode 100644 index 000000000..05dc3af19 --- /dev/null +++ b/example/tailordb/product.ts @@ -0,0 +1,28 @@ +import { createTable, timestampFields } from "@tailor-platform/sdk"; +import { defaultGqlPermission, defaultPermission } from "./permissions"; +import { supplier } from "./supplier"; + +export const product = createTable( + "Product", + { + name: { kind: "string", description: "Product name" }, + sku: { kind: "string", unique: true, description: "Stock keeping unit" }, + price: { kind: "float" }, + stock: { kind: "int", index: true }, + category: { kind: "enum", values: ["electronics", "clothing", "food"] }, + supplierId: { + kind: "uuid", + relation: { + type: "n-1", + toward: { type: supplier }, + }, + }, + ...timestampFields(), + }, + { + description: "Product catalog entry", + permission: defaultPermission, + gqlPermission: defaultGqlPermission, + }, +); +export type product = typeof product; diff --git a/example/tests/bundled_execution.test.ts b/example/tests/bundled_execution.test.ts index e8c3f4808..a279ef58b 100644 --- a/example/tests/bundled_execution.test.ts +++ b/example/tests/bundled_execution.test.ts @@ -181,8 +181,14 @@ describe("bundled execution tests", () => { expect(tailordbMock.executedQueries).toEqual([ { query: 'select * from "User" where "id" = $1', params: ["user-1"] }, { - query: 'insert into "UserLog" ("userID", "message") values ($1, $2)', - params: ["user-1", "User created: undefined (undefined)"], + query: + 'insert into "UserLog" ("userID", "message", "createdAt", "updatedAt") values ($1, $2, $3, $4)', + params: [ + "user-1", + "User created: undefined (undefined)", + new Date("2025-10-06T12:34:56.000Z"), + new Date("2025-10-06T12:34:56.000Z"), + ], }, ]); expect(tailordbMock.createdClients).toMatchObject([{ namespace: "tailordb" }]); diff --git a/example/tests/fixtures/expected/db.ts b/example/tests/fixtures/expected/db.ts index db9ea0e47..dc3b7a876 100644 --- a/example/tests/fixtures/expected/db.ts +++ b/example/tests/fixtures/expected/db.ts @@ -27,7 +27,7 @@ export interface Namespace { fullAddress: Generated; state: string; createdAt: Generated; - updatedAt: Timestamp | null; + updatedAt: Generated; } Invoice: { @@ -38,7 +38,7 @@ export interface Namespace { sequentialId: Serial; status: "draft" | "sent" | "paid" | "cancelled" | null; createdAt: Generated; - updatedAt: Timestamp | null; + updatedAt: Generated; } NestedProfile: { @@ -57,7 +57,19 @@ export interface Namespace { }>; archived: boolean | null; createdAt: Generated; - updatedAt: Timestamp | null; + updatedAt: Generated; + } + + Product: { + id: Generated; + name: string; + sku: string; + price: number; + stock: number; + category: "electronics" | "clothing" | "food"; + supplierId: string; + createdAt: Generated; + updatedAt: Generated; } PurchaseOrder: { @@ -73,7 +85,7 @@ export interface Namespace { type: "text" | "image"; }[]; createdAt: Generated; - updatedAt: Timestamp | null; + updatedAt: Generated; } SalesOrder: { @@ -86,7 +98,7 @@ export interface Namespace { cancelReason: string | null; canceledAt: Timestamp | null; createdAt: Generated; - updatedAt: Timestamp | null; + updatedAt: Generated; } SalesOrderCreated: { @@ -115,7 +127,7 @@ export interface Namespace { state: "Alabama" | "Alaska"; city: string; createdAt: Generated; - updatedAt: Timestamp | null; + updatedAt: Generated; } User: { @@ -126,7 +138,7 @@ export interface Namespace { department: string | null; role: "MANAGER" | "STAFF"; createdAt: Generated; - updatedAt: Timestamp | null; + updatedAt: Generated; } UserLog: { @@ -134,7 +146,7 @@ export interface Namespace { userID: string; message: string; createdAt: Generated; - updatedAt: Timestamp | null; + updatedAt: Generated; } UserSetting: { @@ -142,7 +154,7 @@ export interface Namespace { language: "jp" | "en"; userID: string; createdAt: Generated; - updatedAt: Timestamp | null; + updatedAt: Generated; } }, "analyticsdb": { @@ -150,7 +162,7 @@ export interface Namespace { id: Generated; name: "CLICK" | "VIEW" | "PURCHASE"; createdAt: Generated; - updatedAt: Timestamp | null; + updatedAt: Generated; } } } diff --git a/example/tests/fixtures/expected/enums.ts b/example/tests/fixtures/expected/enums.ts index a2e42e216..ea7a59624 100644 --- a/example/tests/fixtures/expected/enums.ts +++ b/example/tests/fixtures/expected/enums.ts @@ -14,6 +14,13 @@ export const InvoiceStatus = { } as const; export type InvoiceStatus = (typeof InvoiceStatus)[keyof typeof InvoiceStatus]; +export const ProductCategory = { + "electronics": "electronics", + "clothing": "clothing", + "food": "food" +} as const; +export type ProductCategory = (typeof ProductCategory)[keyof typeof ProductCategory]; + export const PurchaseOrderAttachedFilesType = { "text": "text", "image": "image" diff --git a/example/tests/fixtures/expected/seed/data/Customer.schema.ts b/example/tests/fixtures/expected/seed/data/Customer.schema.ts index 145333a65..dbdc7b011 100644 --- a/example/tests/fixtures/expected/seed/data/Customer.schema.ts +++ b/example/tests/fixtures/expected/seed/data/Customer.schema.ts @@ -11,5 +11,5 @@ const schemaType = t.object({ const hook = createTailorDBHook(customer); export const schema = defineSchema( - createStandardSchema(schemaType, hook), + createStandardSchema(schemaType, hook, customer.metadata?.validate), ); diff --git a/example/tests/fixtures/expected/seed/data/Event.schema.ts b/example/tests/fixtures/expected/seed/data/Event.schema.ts index 2c9f19fc4..6cc237fac 100644 --- a/example/tests/fixtures/expected/seed/data/Event.schema.ts +++ b/example/tests/fixtures/expected/seed/data/Event.schema.ts @@ -11,5 +11,5 @@ const schemaType = t.object({ const hook = createTailorDBHook(event); export const schema = defineSchema( - createStandardSchema(schemaType, hook), + createStandardSchema(schemaType, hook, event.metadata?.validate), ); diff --git a/example/tests/fixtures/expected/seed/data/Invoice.schema.ts b/example/tests/fixtures/expected/seed/data/Invoice.schema.ts index 020f3b52e..70d06db52 100644 --- a/example/tests/fixtures/expected/seed/data/Invoice.schema.ts +++ b/example/tests/fixtures/expected/seed/data/Invoice.schema.ts @@ -11,7 +11,7 @@ const schemaType = t.object({ const hook = createTailorDBHook(invoice); export const schema = defineSchema( - createStandardSchema(schemaType, hook), + createStandardSchema(schemaType, hook, invoice.metadata?.validate), { foreignKeys: [ {"column":"salesOrderID","references":{"table":"SalesOrder","column":"id"}}, diff --git a/example/tests/fixtures/expected/seed/data/NestedProfile.schema.ts b/example/tests/fixtures/expected/seed/data/NestedProfile.schema.ts index 38dc3b4e1..c4864bb15 100644 --- a/example/tests/fixtures/expected/seed/data/NestedProfile.schema.ts +++ b/example/tests/fixtures/expected/seed/data/NestedProfile.schema.ts @@ -11,5 +11,5 @@ const schemaType = t.object({ const hook = createTailorDBHook(nestedProfile); export const schema = defineSchema( - createStandardSchema(schemaType, hook), + createStandardSchema(schemaType, hook, nestedProfile.metadata?.validate), ); diff --git a/example/tests/fixtures/expected/seed/data/Product.jsonl b/example/tests/fixtures/expected/seed/data/Product.jsonl new file mode 100644 index 000000000..e69de29bb diff --git a/example/tests/fixtures/expected/seed/data/Product.schema.ts b/example/tests/fixtures/expected/seed/data/Product.schema.ts new file mode 100644 index 000000000..6fe9537f2 --- /dev/null +++ b/example/tests/fixtures/expected/seed/data/Product.schema.ts @@ -0,0 +1,23 @@ +import { t } from "@tailor-platform/sdk"; +import { defineSchema } from "@tailor-platform/sdk/seed"; +import { createTailorDBHook, createStandardSchema } from "@tailor-platform/sdk/test"; +import { product } from "../../../../../tailordb/product"; + +const schemaType = t.object({ + ...product.pickFields(["id","createdAt"], { optional: true }), + ...product.omitFields(["id","createdAt"]), +}); + +const hook = createTailorDBHook(product); + +export const schema = defineSchema( + createStandardSchema(schemaType, hook, product.metadata?.validate), + { + foreignKeys: [ + {"column":"supplierId","references":{"table":"Supplier","column":"id"}}, + ], + indexes: [ + {"name":"product_sku_unique_idx","columns":["sku"],"unique":true}, + ], + } +); diff --git a/example/tests/fixtures/expected/seed/data/PurchaseOrder.schema.ts b/example/tests/fixtures/expected/seed/data/PurchaseOrder.schema.ts index 4e4f269a4..28139dceb 100644 --- a/example/tests/fixtures/expected/seed/data/PurchaseOrder.schema.ts +++ b/example/tests/fixtures/expected/seed/data/PurchaseOrder.schema.ts @@ -11,7 +11,7 @@ const schemaType = t.object({ const hook = createTailorDBHook(purchaseOrder); export const schema = defineSchema( - createStandardSchema(schemaType, hook), + createStandardSchema(schemaType, hook, purchaseOrder.metadata?.validate), { foreignKeys: [ {"column":"supplierID","references":{"table":"Supplier","column":"id"}}, diff --git a/example/tests/fixtures/expected/seed/data/SalesOrder.schema.ts b/example/tests/fixtures/expected/seed/data/SalesOrder.schema.ts index db0543332..4f7fdc9ce 100644 --- a/example/tests/fixtures/expected/seed/data/SalesOrder.schema.ts +++ b/example/tests/fixtures/expected/seed/data/SalesOrder.schema.ts @@ -11,7 +11,7 @@ const schemaType = t.object({ const hook = createTailorDBHook(salesOrder); export const schema = defineSchema( - createStandardSchema(schemaType, hook), + createStandardSchema(schemaType, hook, salesOrder.metadata?.validate), { foreignKeys: [ {"column":"customerID","references":{"table":"Customer","column":"id"}}, diff --git a/example/tests/fixtures/expected/seed/data/SalesOrderCreated.schema.ts b/example/tests/fixtures/expected/seed/data/SalesOrderCreated.schema.ts index 61aac30f0..acc0f8255 100644 --- a/example/tests/fixtures/expected/seed/data/SalesOrderCreated.schema.ts +++ b/example/tests/fixtures/expected/seed/data/SalesOrderCreated.schema.ts @@ -11,5 +11,5 @@ const schemaType = t.object({ const hook = createTailorDBHook(salesOrderCreated); export const schema = defineSchema( - createStandardSchema(schemaType, hook), + createStandardSchema(schemaType, hook, salesOrderCreated.metadata?.validate), ); diff --git a/example/tests/fixtures/expected/seed/data/Selfie.schema.ts b/example/tests/fixtures/expected/seed/data/Selfie.schema.ts index d272c3337..9091bf667 100644 --- a/example/tests/fixtures/expected/seed/data/Selfie.schema.ts +++ b/example/tests/fixtures/expected/seed/data/Selfie.schema.ts @@ -11,7 +11,7 @@ const schemaType = t.object({ const hook = createTailorDBHook(selfie); export const schema = defineSchema( - createStandardSchema(schemaType, hook), + createStandardSchema(schemaType, hook, selfie.metadata?.validate), { foreignKeys: [ {"column":"parentID","references":{"table":"Selfie","column":"id"}}, diff --git a/example/tests/fixtures/expected/seed/data/Supplier.schema.ts b/example/tests/fixtures/expected/seed/data/Supplier.schema.ts index 3fb6af855..f4c159e75 100644 --- a/example/tests/fixtures/expected/seed/data/Supplier.schema.ts +++ b/example/tests/fixtures/expected/seed/data/Supplier.schema.ts @@ -11,5 +11,5 @@ const schemaType = t.object({ const hook = createTailorDBHook(supplier); export const schema = defineSchema( - createStandardSchema(schemaType, hook), + createStandardSchema(schemaType, hook, supplier.metadata?.validate), ); diff --git a/example/tests/fixtures/expected/seed/data/User.schema.ts b/example/tests/fixtures/expected/seed/data/User.schema.ts index 5c400f2a0..29078d56a 100644 --- a/example/tests/fixtures/expected/seed/data/User.schema.ts +++ b/example/tests/fixtures/expected/seed/data/User.schema.ts @@ -11,8 +11,11 @@ const schemaType = t.object({ const hook = createTailorDBHook(user); export const schema = defineSchema( - createStandardSchema(schemaType, hook), + createStandardSchema(schemaType, hook, user.metadata?.validate), { + foreignKeys: [ + {"column":"email","references":{"table":"_User","column":"name"}}, + ], indexes: [ {"name":"user_email_unique_idx","columns":["email"],"unique":true}, {"name":"idx_name_department","columns":["name","department"],"unique":false}, diff --git a/example/tests/fixtures/expected/seed/data/UserLog.schema.ts b/example/tests/fixtures/expected/seed/data/UserLog.schema.ts index fd3b3c52b..98581af42 100644 --- a/example/tests/fixtures/expected/seed/data/UserLog.schema.ts +++ b/example/tests/fixtures/expected/seed/data/UserLog.schema.ts @@ -11,7 +11,7 @@ const schemaType = t.object({ const hook = createTailorDBHook(userLog); export const schema = defineSchema( - createStandardSchema(schemaType, hook), + createStandardSchema(schemaType, hook, userLog.metadata?.validate), { foreignKeys: [ {"column":"userID","references":{"table":"User","column":"id"}}, diff --git a/example/tests/fixtures/expected/seed/data/UserSetting.schema.ts b/example/tests/fixtures/expected/seed/data/UserSetting.schema.ts index d5f2fb052..882b0e848 100644 --- a/example/tests/fixtures/expected/seed/data/UserSetting.schema.ts +++ b/example/tests/fixtures/expected/seed/data/UserSetting.schema.ts @@ -11,7 +11,7 @@ const schemaType = t.object({ const hook = createTailorDBHook(userSetting); export const schema = defineSchema( - createStandardSchema(schemaType, hook), + createStandardSchema(schemaType, hook, userSetting.metadata?.validate), { foreignKeys: [ {"column":"userID","references":{"table":"User","column":"id"}}, diff --git a/example/tests/fixtures/expected/seed/exec.mjs b/example/tests/fixtures/expected/seed/exec.mjs index b751f1d27..98ef21329 100644 --- a/example/tests/fixtures/expected/seed/exec.mjs +++ b/example/tests/fixtures/expected/seed/exec.mjs @@ -144,6 +144,7 @@ const namespaceEntities = { "Customer", "Invoice", "NestedProfile", + "Product", "PurchaseOrder", "SalesOrder", "SalesOrderCreated", @@ -162,6 +163,7 @@ const namespaceDeps = { "Customer": [], "Invoice": ["SalesOrder"], "NestedProfile": [], + "Product": ["Supplier"], "PurchaseOrder": ["Supplier"], "SalesOrder": ["Customer", "User"], "SalesOrderCreated": [], diff --git a/lefthook.yml b/lefthook.yml index 625d09e5e..1fe4f7441 100644 --- a/lefthook.yml +++ b/lefthook.yml @@ -25,6 +25,8 @@ pre-commit: - "example/seed/**" - "packages/tailor-proto/**" - "packages/sdk-codemod/codemods/**/tests/**" + - "packages/create-sdk/templates/*/src/generated/**" + - "packages/create-sdk/templates/*/src/seed/**" run: pnpm oxfmt --check {staged_files} checks: diff --git a/packages/create-sdk/templates/executor/src/executor/shared.ts b/packages/create-sdk/templates/executor/src/executor/shared.ts index 0483a944b..0a90f8159 100644 --- a/packages/create-sdk/templates/executor/src/executor/shared.ts +++ b/packages/create-sdk/templates/executor/src/executor/shared.ts @@ -9,5 +9,8 @@ interface AuditLogInput { export async function createAuditLog(input: AuditLogInput): Promise { const db = getDB("main-db"); - await db.insertInto("AuditLog").values(input).execute(); + await db + .insertInto("AuditLog") + .values({ ...input, createdAt: new Date(), updatedAt: new Date() }) + .execute(); } diff --git a/packages/create-sdk/templates/executor/src/generated/db.ts b/packages/create-sdk/templates/executor/src/generated/db.ts index 1bdcc0d1f..637fef2a7 100644 --- a/packages/create-sdk/templates/executor/src/generated/db.ts +++ b/packages/create-sdk/templates/executor/src/generated/db.ts @@ -20,7 +20,7 @@ export interface Namespace { entityId: string; message: string; createdAt: Generated; - updatedAt: Timestamp | null; + updatedAt: Generated; } Notification: { @@ -30,7 +30,7 @@ export interface Namespace { body: string; isRead: boolean; createdAt: Generated; - updatedAt: Timestamp | null; + updatedAt: Generated; } User: { @@ -39,7 +39,7 @@ export interface Namespace { email: string; role: "ADMIN" | "MEMBER"; createdAt: Generated; - updatedAt: Timestamp | null; + updatedAt: Generated; } } } diff --git a/packages/create-sdk/templates/generators/src/generated/db.ts b/packages/create-sdk/templates/generators/src/generated/db.ts index 0da558bfe..7db4b82e5 100644 --- a/packages/create-sdk/templates/generators/src/generated/db.ts +++ b/packages/create-sdk/templates/generators/src/generated/db.ts @@ -28,7 +28,7 @@ export interface Namespace { totalPrice: number; status: "PENDING" | "CONFIRMED" | "SHIPPED" | "DELIVERED" | "CANCELLED"; createdAt: Generated; - updatedAt: Timestamp | null; + updatedAt: Generated; } Product: { @@ -39,7 +39,7 @@ export interface Namespace { status: "DRAFT" | "ACTIVE" | "DISCONTINUED"; categoryId: string | null; createdAt: Generated; - updatedAt: Timestamp | null; + updatedAt: Generated; } User: { @@ -48,7 +48,7 @@ export interface Namespace { email: string; role: "ADMIN" | "MEMBER" | "VIEWER"; createdAt: Generated; - updatedAt: Timestamp | null; + updatedAt: Generated; } } } diff --git a/packages/create-sdk/templates/generators/src/seed/data/Category.schema.ts b/packages/create-sdk/templates/generators/src/seed/data/Category.schema.ts index 7ed5c6a9f..d6b8d3aea 100644 --- a/packages/create-sdk/templates/generators/src/seed/data/Category.schema.ts +++ b/packages/create-sdk/templates/generators/src/seed/data/Category.schema.ts @@ -11,7 +11,7 @@ const schemaType = t.object({ const hook = createTailorDBHook(category); export const schema = defineSchema( - createStandardSchema(schemaType, hook), + createStandardSchema(schemaType, hook, category.metadata?.validate), { foreignKeys: [ {"column":"parentCategoryId","references":{"table":"Category","column":"id"}}, diff --git a/packages/create-sdk/templates/generators/src/seed/data/Order.schema.ts b/packages/create-sdk/templates/generators/src/seed/data/Order.schema.ts index dffeb95f3..3386aa294 100644 --- a/packages/create-sdk/templates/generators/src/seed/data/Order.schema.ts +++ b/packages/create-sdk/templates/generators/src/seed/data/Order.schema.ts @@ -11,7 +11,7 @@ const schemaType = t.object({ const hook = createTailorDBHook(order); export const schema = defineSchema( - createStandardSchema(schemaType, hook), + createStandardSchema(schemaType, hook, order.metadata?.validate), { foreignKeys: [ {"column":"productId","references":{"table":"Product","column":"id"}}, diff --git a/packages/create-sdk/templates/generators/src/seed/data/Product.schema.ts b/packages/create-sdk/templates/generators/src/seed/data/Product.schema.ts index 2bf00829c..2bff1a8e8 100644 --- a/packages/create-sdk/templates/generators/src/seed/data/Product.schema.ts +++ b/packages/create-sdk/templates/generators/src/seed/data/Product.schema.ts @@ -11,7 +11,7 @@ const schemaType = t.object({ const hook = createTailorDBHook(product); export const schema = defineSchema( - createStandardSchema(schemaType, hook), + createStandardSchema(schemaType, hook, product.metadata?.validate), { foreignKeys: [ {"column":"categoryId","references":{"table":"Category","column":"id"}}, diff --git a/packages/create-sdk/templates/generators/src/seed/data/User.schema.ts b/packages/create-sdk/templates/generators/src/seed/data/User.schema.ts index 2cbbdf2c5..b774e8695 100644 --- a/packages/create-sdk/templates/generators/src/seed/data/User.schema.ts +++ b/packages/create-sdk/templates/generators/src/seed/data/User.schema.ts @@ -11,7 +11,7 @@ const schemaType = t.object({ const hook = createTailorDBHook(user); export const schema = defineSchema( - createStandardSchema(schemaType, hook), + createStandardSchema(schemaType, hook, user.metadata?.validate), { indexes: [ {"name":"user_email_unique_idx","columns":["email"],"unique":true}, diff --git a/packages/create-sdk/templates/hello-world/src/generated/kysely-tailordb.ts b/packages/create-sdk/templates/hello-world/src/generated/kysely-tailordb.ts index c42a19651..b4ea6f9d6 100644 --- a/packages/create-sdk/templates/hello-world/src/generated/kysely-tailordb.ts +++ b/packages/create-sdk/templates/hello-world/src/generated/kysely-tailordb.ts @@ -19,7 +19,7 @@ export interface Namespace { email: string; role: "MANAGER" | "STAFF"; createdAt: Generated; - updatedAt: Timestamp | null; + updatedAt: Generated; } } } diff --git a/packages/create-sdk/templates/inventory-management/src/db/inventory.ts b/packages/create-sdk/templates/inventory-management/src/db/inventory.ts index 6deb5d780..77e0ddd9e 100644 --- a/packages/create-sdk/templates/inventory-management/src/db/inventory.ts +++ b/packages/create-sdk/templates/inventory-management/src/db/inventory.ts @@ -8,11 +8,9 @@ export const inventory = db .uuid() .description("ID of the product") .relation({ type: "1-1", toward: { type: product } }), - quantity: db - .int() - .description("Quantity of the product in inventory") - .validate(({ value }) => value >= 0), + quantity: db.int().description("Quantity of the product in inventory"), ...db.fields.timestamps(), }) + .validate(({ data }) => data.quantity >= 0) .permission(permissionLoggedIn) .gqlPermission(gqlPermissionLoggedIn); diff --git a/packages/create-sdk/templates/inventory-management/src/db/orderItem.ts b/packages/create-sdk/templates/inventory-management/src/db/orderItem.ts index 2fc8c572e..97fb4ee9d 100644 --- a/packages/create-sdk/templates/inventory-management/src/db/orderItem.ts +++ b/packages/create-sdk/templates/inventory-management/src/db/orderItem.ts @@ -13,22 +13,22 @@ export const orderItem = db .uuid() .description("ID of the product") .relation({ type: "n-1", toward: { type: product } }), - quantity: db - .int() - .description("Quantity of the product") - .validate(({ value }) => value >= 0), - unitPrice: db - .float() - .description("Unit price of the product") - .validate(({ value }) => value >= 0), + quantity: db.int().description("Quantity of the product"), + unitPrice: db.float().description("Unit price of the product"), totalPrice: db.float({ optional: true }).description("Total price of the order item"), ...db.fields.timestamps(), }) .hooks({ - totalPrice: { - create: ({ data }) => (data?.quantity ?? 0) * (data.unitPrice ?? 0), - update: ({ data }) => (data?.quantity ?? 0) * (data.unitPrice ?? 0), - }, + create: ({ data }) => ({ + totalPrice: data.quantity * data.unitPrice, + createdAt: new Date(), + updatedAt: new Date(), + }), + update: ({ data }) => ({ + totalPrice: data.quantity * data.unitPrice, + updatedAt: new Date(), + }), }) + .validate([({ data }) => data.quantity >= 0, ({ data }) => data.unitPrice >= 0]) .permission(permissionLoggedIn) .gqlPermission(gqlPermissionLoggedIn); diff --git a/packages/create-sdk/templates/inventory-management/src/executor/checkInventory.ts b/packages/create-sdk/templates/inventory-management/src/executor/checkInventory.ts index 93bc9d1cf..78ff705da 100644 --- a/packages/create-sdk/templates/inventory-management/src/executor/checkInventory.ts +++ b/packages/create-sdk/templates/inventory-management/src/executor/checkInventory.ts @@ -19,6 +19,8 @@ export default createExecutor({ .insertInto("Notification") .values({ message: `Inventory for product ${newRecord.productId} is below threshold. Current quantity: ${newRecord.quantity}`, + createdAt: new Date(), + updatedAt: new Date(), }) .execute(); }, diff --git a/packages/create-sdk/templates/inventory-management/src/generated/kysely-tailordb.ts b/packages/create-sdk/templates/inventory-management/src/generated/kysely-tailordb.ts index 001ea023e..a9db53ba6 100644 --- a/packages/create-sdk/templates/inventory-management/src/generated/kysely-tailordb.ts +++ b/packages/create-sdk/templates/inventory-management/src/generated/kysely-tailordb.ts @@ -18,7 +18,7 @@ export interface Namespace { name: string; description: string | null; createdAt: Generated; - updatedAt: Timestamp | null; + updatedAt: Generated; } Contact: { @@ -28,7 +28,7 @@ export interface Namespace { phone: string | null; address: string | null; createdAt: Generated; - updatedAt: Timestamp | null; + updatedAt: Generated; } Inventory: { @@ -36,14 +36,14 @@ export interface Namespace { productId: string; quantity: number; createdAt: Generated; - updatedAt: Timestamp | null; + updatedAt: Generated; } Notification: { id: Generated; message: string; createdAt: Generated; - updatedAt: Timestamp | null; + updatedAt: Generated; } Order: { @@ -54,7 +54,7 @@ export interface Namespace { orderType: "PURCHASE" | "SALES"; contactId: string; createdAt: Generated; - updatedAt: Timestamp | null; + updatedAt: Generated; } OrderItem: { @@ -65,7 +65,7 @@ export interface Namespace { unitPrice: number; totalPrice: Generated; createdAt: Generated; - updatedAt: Timestamp | null; + updatedAt: Generated; } Product: { @@ -74,7 +74,7 @@ export interface Namespace { description: string | null; categoryId: string; createdAt: Generated; - updatedAt: Timestamp | null; + updatedAt: Generated; } User: { @@ -83,7 +83,7 @@ export interface Namespace { email: string; role: "MANAGER" | "STAFF"; createdAt: Generated; - updatedAt: Timestamp | null; + updatedAt: Generated; } } } diff --git a/packages/create-sdk/templates/inventory-management/src/resolver/registerOrder.ts b/packages/create-sdk/templates/inventory-management/src/resolver/registerOrder.ts index ee1f332a7..416d9febd 100644 --- a/packages/create-sdk/templates/inventory-management/src/resolver/registerOrder.ts +++ b/packages/create-sdk/templates/inventory-management/src/resolver/registerOrder.ts @@ -16,7 +16,7 @@ const insertOrder = async (db: DB<"main-db">, input: Input) => { // Insert Order const order = await db .insertInto("Order") - .values(input.order) + .values({ ...input.order, createdAt: new Date() }) .returning("id") .executeTakeFirstOrThrow(); @@ -27,6 +27,7 @@ const insertOrder = async (db: DB<"main-db">, input: Input) => { input.items.map((item) => ({ ...item, orderId: order.id, + createdAt: new Date(), })), ) .execute(); @@ -63,6 +64,8 @@ const updateInventory = async (db: DB<"main-db">, input: Input) => { .values({ productId: item.productId, quantity: item.quantity, + createdAt: new Date(), + updatedAt: new Date(), }) .execute(); } else { diff --git a/packages/create-sdk/templates/multi-application/apps/admin/db/adminNote.ts b/packages/create-sdk/templates/multi-application/apps/admin/db/adminNote.ts index b3f5997c3..b8bbae5cb 100644 --- a/packages/create-sdk/templates/multi-application/apps/admin/db/adminNote.ts +++ b/packages/create-sdk/templates/multi-application/apps/admin/db/adminNote.ts @@ -8,9 +8,19 @@ export const adminNote = db .type("AdminNote", { title: db.string(), content: db.string(), - authorId: db.uuid().hooks({ create: ({ user }) => user.id }), + authorId: db.uuid(), ...db.fields.timestamps(), }) + .hooks({ + create: ({ user }) => ({ + authorId: user.id, + createdAt: new Date(), + updatedAt: new Date(), + }), + update: () => ({ + updatedAt: new Date(), + }), + }) // NOTE: This permits all operations for simplicity. // In production, configure proper permissions based on your requirements. .permission(unsafeAllowAllTypePermission) diff --git a/packages/create-sdk/templates/resolver/src/generated/db.ts b/packages/create-sdk/templates/resolver/src/generated/db.ts index e36ba8aa9..b1467d05b 100644 --- a/packages/create-sdk/templates/resolver/src/generated/db.ts +++ b/packages/create-sdk/templates/resolver/src/generated/db.ts @@ -19,7 +19,7 @@ export interface Namespace { email: string; age: number; createdAt: Generated; - updatedAt: Timestamp | null; + updatedAt: Generated; } } } diff --git a/packages/create-sdk/templates/tailordb/src/db/comment.ts b/packages/create-sdk/templates/tailordb/src/db/comment.ts index 5f8f18067..f88168120 100644 --- a/packages/create-sdk/templates/tailordb/src/db/comment.ts +++ b/packages/create-sdk/templates/tailordb/src/db/comment.ts @@ -5,7 +5,7 @@ import { user } from "./user"; export const comment = db .type("Comment", "A comment on a task", { - body: db.string().validate([({ value }) => value.length >= 1, "Comment must not be empty"]), + body: db.string(), taskId: db.uuid().relation({ type: "n-1", toward: { type: task }, @@ -22,5 +22,6 @@ export const comment = db ...db.fields.timestamps(), }) .indexes({ fields: ["taskId", "createdAt"], unique: false }) + .validate([({ data }) => data.body.length >= 1, "Comment must not be empty"]) .permission(allPermission) .gqlPermission(allGqlPermission); diff --git a/packages/create-sdk/templates/tailordb/src/db/task.ts b/packages/create-sdk/templates/tailordb/src/db/task.ts index 4e4a56143..75f7e02e3 100644 --- a/packages/create-sdk/templates/tailordb/src/db/task.ts +++ b/packages/create-sdk/templates/tailordb/src/db/task.ts @@ -5,12 +5,7 @@ import { user } from "./user"; export const task = db .type("Task", "A task with comprehensive features", { - title: db - .string() - .validate( - [({ value }) => value.length >= 3, "Title must be at least 3 characters"], - [({ value }) => value.length <= 200, "Title must be at most 200 characters"], - ), + title: db.string(), description: db.string({ optional: true }), status: db.enum([ { value: "TODO", description: "Not started" }, @@ -18,12 +13,7 @@ export const task = db { value: "DONE", description: "Completed" }, { value: "CANCELLED", description: "No longer needed" }, ]), - priority: db - .int() - .validate( - [({ value }) => value >= 0, "Priority must be non-negative"], - [({ value }) => value <= 4, "Priority must be at most 4"], - ), + priority: db.int(), dueDate: db.datetime({ optional: true }), assigneeId: db.uuid({ optional: true }).relation({ type: "n-1", @@ -37,22 +27,28 @@ export const task = db ...db.fields.timestamps(), }) .hooks({ - isArchived: { - create: () => false, - }, + create: () => ({ + isArchived: false, + createdAt: new Date(), + updatedAt: new Date(), + }), + update: () => ({ + updatedAt: new Date(), + }), }) .indexes( { fields: ["status", "priority"], unique: false }, { fields: ["assigneeId", "status"], unique: false, name: "task_assignee_status_idx" }, ) - .validate({ - status: [ - ({ value, data }) => { - const d = data as { dueDate: string | null }; - return !(value === "DONE" && d.dueDate === null); - }, + .validate([ + [({ data }) => data.title.length >= 3, "Title must be at least 3 characters"], + [({ data }) => data.title.length <= 200, "Title must be at most 200 characters"], + [({ data }) => data.priority >= 0, "Priority must be non-negative"], + [({ data }) => data.priority <= 4, "Priority must be at most 4"], + [ + ({ data }) => !(data.status === "DONE" && data.dueDate === null), "Completed tasks must have a due date", ], - }) + ]) .permission(rolePermission) .gqlPermission(roleGqlPermission); diff --git a/packages/create-sdk/templates/tailordb/src/generated/db.ts b/packages/create-sdk/templates/tailordb/src/generated/db.ts index f68627d27..66a199fb4 100644 --- a/packages/create-sdk/templates/tailordb/src/generated/db.ts +++ b/packages/create-sdk/templates/tailordb/src/generated/db.ts @@ -32,7 +32,7 @@ export interface Namespace { isInternal: boolean; }>; createdAt: Generated; - updatedAt: Timestamp | null; + updatedAt: Generated; } Task: { @@ -46,7 +46,7 @@ export interface Namespace { categoryId: string | null; isArchived: Generated; createdAt: Generated; - updatedAt: Timestamp | null; + updatedAt: Generated; } User: { @@ -56,7 +56,7 @@ export interface Namespace { role: "ADMIN" | "MEMBER" | "VIEWER"; bio: string | null; createdAt: Generated; - updatedAt: Timestamp | null; + updatedAt: Generated; } } } diff --git a/packages/create-sdk/templates/workflow/src/generated/db.ts b/packages/create-sdk/templates/workflow/src/generated/db.ts index 51e0e14cb..f01175f35 100644 --- a/packages/create-sdk/templates/workflow/src/generated/db.ts +++ b/packages/create-sdk/templates/workflow/src/generated/db.ts @@ -19,7 +19,7 @@ export interface Namespace { amount: number; status: "PENDING" | "PROCESSING" | "COMPLETED" | "FAILED"; createdAt: Generated; - updatedAt: Timestamp | null; + updatedAt: Generated; } User: { @@ -28,7 +28,7 @@ export interface Namespace { email: string; age: number; createdAt: Generated; - updatedAt: Timestamp | null; + updatedAt: Generated; } } } diff --git a/packages/create-sdk/templates/workflow/src/workflow/sync-profile.ts b/packages/create-sdk/templates/workflow/src/workflow/sync-profile.ts index f0825f133..94603a3be 100644 --- a/packages/create-sdk/templates/workflow/src/workflow/sync-profile.ts +++ b/packages/create-sdk/templates/workflow/src/workflow/sync-profile.ts @@ -29,7 +29,7 @@ function createDbOperations(db: DB<"main-db">): DbOperations { createUser: async (input: UserProfile) => { return await db .insertInto("User") - .values(input) + .values({ ...input, createdAt: new Date(), updatedAt: new Date() }) .returning(["id", "name", "email", "age", "createdAt", "updatedAt"]) .executeTakeFirstOrThrow(); }, diff --git a/packages/sdk/docs/services/resolver.md b/packages/sdk/docs/services/resolver.md index cdc6ac61f..193bc7f74 100644 --- a/packages/sdk/docs/services/resolver.md +++ b/packages/sdk/docs/services/resolver.md @@ -103,7 +103,54 @@ export default createResolver({ ## Input/Output Schemas -Define input/output schemas using methods of `t` object. Basic usage and supported field types are the same as TailorDB. TailorDB-specific options (e.g., index, relation) are not supported. +Define input/output schemas using methods of `t` object or object-literal descriptors (`{ kind: "..." }`). Both styles can be mixed in the same resolver. + +### Fluent API (`t.*()`) + +```typescript +createResolver({ + input: { + name: t.string(), + age: t.int(), + }, + output: t.object({ name: t.string(), age: t.int() }), + // ... +}); +``` + +### Object-Literal Descriptors + +Use `{ kind: "..." }` syntax as a concise alternative. Supported options: `optional`, `array`, `description`, `validate`, and `typeName` (for enum/object). + +```typescript +createResolver({ + name: "addNumbers", + operation: "query", + input: { + a: { kind: "int", description: "First number" }, + b: { kind: "int", description: "Second number" }, + }, + output: { kind: "int", description: "Sum" }, + body: ({ input }) => input.a + input.b, +}); +``` + +### Mixing Styles + +Fluent and descriptor fields can be freely combined: + +```typescript +createResolver({ + input: { + name: t.string(), + status: { kind: "enum", values: ["active", "inactive"] }, + }, + output: t.object({ result: t.bool() }), + // ... +}); +``` + +### Reusing TailorDB Fields You can reuse fields defined with `db` object, but note that unsupported options will be ignored: diff --git a/packages/sdk/docs/services/tailordb.md b/packages/sdk/docs/services/tailordb.md index 5d464fe4c..f7d3112be 100644 --- a/packages/sdk/docs/services/tailordb.md +++ b/packages/sdk/docs/services/tailordb.md @@ -10,7 +10,7 @@ TailorDB provides: - Automatic GraphQL API generation (CRUD operations) - Relations between types with automatic index and foreign key constraints - Permission system for access control -- Field-level hooks and validations +- Record-level hooks and validations For the official Tailor Platform documentation, see [TailorDB Guide](https://docs.tailor.tech/guides/tailordb/overview). @@ -25,6 +25,8 @@ Define TailorDB Types in files matching glob patterns specified in `tailor.confi - **Export both value and type**: Always export both the runtime value and TypeScript type - **Uniqueness**: Type names must be unique across all TailorDB files +### Fluent API (`db.type()`) + ```typescript import { db } from "@tailor-platform/sdk"; @@ -44,6 +46,50 @@ export const role = db.type("Role", { export type role = typeof role; ``` +### Object-Literal API (`createTable`) + +`createTable` provides an alternative syntax using plain object descriptors instead of method chaining. Each field is described with a `{ kind, ...options }` object. + +```typescript +import { createTable, timestampFields, unsafeAllowAllTypePermission } from "@tailor-platform/sdk"; + +export const order = createTable( + "Order", + { + name: { kind: "string" }, + quantity: { kind: "int", optional: true, index: true }, + status: { kind: "enum", values: ["pending", "shipped"] }, + address: { + kind: "object", + fields: { + city: { kind: "string" }, + zip: { kind: "string" }, + }, + }, + ...timestampFields(), + }, + { + permission: unsafeAllowAllTypePermission, + }, +); +export type order = typeof order; +``` + +**Signature:** `createTable(name, descriptors, options?)` + +- `name` - Type name (`string`) or `[name, pluralForm]` tuple +- `descriptors` - Field descriptors as `{ fieldName: { kind, ...options } }`. You can also mix in `db.*()` fields +- `options` - Optional type-level settings: `description`, `pluralForm`, `features`, `indexes`, `files`, `permission`, `gqlPermission`, `plugins`, `hooks`, `validate` + +Descriptor fields accept the same per-field options as the fluent API: `optional`, `array`, `description`, `index`, `unique`, `serial`, `vector`, `relation`, plus kind-specific options such as `scale` (decimal) and `values` (enum). Hooks and validators are no longer per-field — configure them at the record level via the third `options` argument (`{ hooks, validate }`) or via `db.type(...).hooks(...).validate(...)`. + +**`timestampFields()` helper:** Returns `createdAt` (datetime, set on create) and `updatedAt` (optional datetime, set on update) descriptors. Equivalent to `db.fields.timestamps()` for the fluent API. + +**When to use which:** + +- Use `db.type()` when you need precise hook callback typing (the fluent API infers exact types for `optional`/`array` combinations) +- Use `createTable` for a more concise, declarative style when hook typing precision is not critical + Specify plural form by passing an array as first argument: ```typescript @@ -256,28 +302,7 @@ type User { ### Hooks -Add hooks to execute functions during data creation or update. Hooks receive three arguments: - -- `value`: User input if provided, otherwise existing value on update or null on create -- `data`: Entire record data (for accessing other field values) -- `user`: User performing the operation - -#### Field-level Hooks - -Set hooks directly on individual fields: - -```typescript -db.string().hooks({ - create: ({ user }) => user.id, - update: ({ value }) => value, -}); -``` - -**Note:** When setting hooks at the field level, the `data` argument type is `unknown` since the field doesn't know about other fields in the type. Use type-level hooks if you need to access other fields with type safety. - -#### Type-level Hooks - -Set hooks for multiple fields at once using `db.type().hooks()`: +Attach record-level hooks with `.hooks({ create, update })` on `db.type(...)` or via the third `options` argument of `createTable`. Each hook receives the full record and returns an **object containing only the fields to override** — omitted fields keep their incoming values. ```typescript export const customer = db @@ -287,60 +312,60 @@ export const customer = db fullName: db.string(), }) .hooks({ - fullName: { - create: ({ data }) => `${data.firstName} ${data.lastName}`, - update: ({ data }) => `${data.firstName} ${data.lastName}`, - }, + create: ({ data, user }) => ({ + fullName: `${data.firstName} ${data.lastName}`, + }), + update: ({ data, user }) => ({ + fullName: `${data.firstName} ${data.lastName}`, + }), }); ``` -**Important:** Field-level and type-level hooks cannot coexist on the same field. TypeScript will prevent this at compile time: +Hook callback arguments: -```typescript -// Compile error - cannot set hooks on the same field twice -export const user = db - .type("User", { - name: db.string().hooks({ create: ({ data }) => data.firstName }), // Field-level - }) - .hooks({ - name: { create: ({ data }) => data.lastName }, // Type-level - ERROR - }); +- `data`: the full incoming record (typed from the type definition) +- `user`: the authenticated user performing the operation -// OK - set hooks on different fields -export const user = db - .type("User", { - firstName: db.string().hooks({ create: () => "John" }), // Field-level on firstName - lastName: db.string(), - }) - .hooks({ - lastName: { create: () => "Doe" }, // Type-level on lastName - }); -``` +#### Override-only return shape -### Validation +The SDK statically extracts the set of overridden keys from the returned object literal at deploy time and emits a per-field hook for each one. Because the extraction is static, the return value must be a single object literal whose keys can be read without executing the function. The parser rejects the following shapes: + +```typescript +// ❌ Spread is not allowed — list overridden keys explicitly. +create: ({ data }) => ({ ...data, fullName: data.firstName }), -Add validation rules to fields. Validators receive three arguments (executed after hooks): +// ❌ Computed keys cannot be resolved statically. +create: ({ data }) => ({ [computeKey()]: data.firstName }), -- `value`: Field value after hook transformation -- `data`: Entire record data after hook transformations (for accessing other field values) -- `user`: User performing the operation +// ❌ Branched / multiple returns produce ambiguous override sets. +create: ({ data }) => { + if (data.firstName) return { fullName: data.firstName }; + return { fullName: data.lastName }; +}, -Validators return `true` for success, `false` for failure. Use array form `[validator, errorMessage]` for custom error messages. +// ❌ Getter / setter / method-shorthand properties are not supported. +create: () => ({ get fullName() { return "x"; } }), +``` + +Use a plain `({ key1: ..., key2: ... })` return — and `return` early outside the callback if you need branching logic that produces the override values. -#### Field-level Validation +#### Re-evaluation per overridden key -Set validators directly on individual fields: +The returned object literal is expanded into one independent hook expression per key. Side-effecting calls such as `crypto.randomUUID()` or `new Date()` therefore evaluate **once per key**, not once per record: ```typescript -db.string().validate( - ({ value }) => value.includes("@"), - [({ value }) => value.length >= 5, "Email must be at least 5 characters"], -); +.hooks({ + // `new Date()` runs twice — once for createdAt, once for updatedAt — and + // each call returns its own Date value. + create: () => ({ createdAt: new Date(), updatedAt: new Date() }), +}) ``` -#### Type-level Validation +If you need a single shared value, compute it before the write at the call site. -Set validators for multiple fields at once using `db.type().validate()`: +### Validation + +Attach record-level validators with `.validate(...)` on `db.type(...)` or via `options.validate` on `createTable`. Each validator receives the entire record and returns `true` for success or `false` for failure. ```typescript export const user = db @@ -348,37 +373,20 @@ export const user = db name: db.string(), email: db.string(), }) - .validate({ - name: [({ value }) => value.length > 5, "Name must be longer than 5 characters"], - email: [ - ({ value }) => value.includes("@"), - [({ value }) => value.length >= 5, "Email must be at least 5 characters"], - ], - }); + .validate([ + [({ data }) => data.name.length > 5, "Name must be longer than 5 characters"], + ({ data }) => data.email.includes("@"), + [({ data }) => data.email.length >= 5, "Email must be at least 5 characters"], + ]); ``` -**Important:** Field-level and type-level validation cannot coexist on the same field. TypeScript will prevent this at compile time: +`.validate(...)` accepts any of: -```typescript -// Compile error - cannot set validation on the same field twice -export const user = db - .type("User", { - name: db.string().validate(({ value }) => value.length > 0), // Field-level - }) - .validate({ - name: [({ value }) => value.length < 100, "Too long"], // Type-level - ERROR - }); +- a single function — `({ data }) => boolean` +- a `[fn, errorMessage]` tuple for a custom message +- an array combining either of the above -// OK - set validation on different fields -export const user = db - .type("User", { - name: db.string().validate(({ value }) => value.length > 0), // Field-level on name - email: db.string(), - }) - .validate({ - email: [({ value }) => value.includes("@"), "Invalid email"], // Type-level on email - }); -``` +Validators run after hooks have produced their overrides, so they see the post-hook record. They receive `{ data, user }` — there is no per-field `value` argument. ### Vector Search diff --git a/packages/sdk/knip.json b/packages/sdk/knip.json index 2373a918e..1ee26d70b 100644 --- a/packages/sdk/knip.json +++ b/packages/sdk/knip.json @@ -7,6 +7,7 @@ "e2e/fixtures/**", "eslint-rules/__tests__/fixtures/**", "src/cli/commands/deploy/__test_fixtures__/**", + "src/cli/commands/apply/__test_fixtures__/**-compat-out/**", "src/types/*.ts", "src/vitest/integration/vitest.config.ts", "zinfer.config.ts" diff --git a/packages/sdk/scripts/perf/features/parse-smoke.test.ts b/packages/sdk/scripts/perf/features/parse-smoke.test.ts new file mode 100644 index 000000000..e0f04bddd --- /dev/null +++ b/packages/sdk/scripts/perf/features/parse-smoke.test.ts @@ -0,0 +1,30 @@ +import { describe, expect, it } from "vitest"; +import { parseTypes } from "@/parser/service/tailordb"; +import { toSchemaOutputs } from "@/utils/test/internal"; +import * as basicFeatures from "./tailordb-basic"; +import * as enumFeatures from "./tailordb-enum"; +import * as hooksFeatures from "./tailordb-hooks"; +import * as objectFeatures from "./tailordb-object"; +import * as optionalFeatures from "./tailordb-optional"; +import * as relationFeatures from "./tailordb-relation"; +import * as validateFeatures from "./tailordb-validate"; + +const featureModules = { + "tailordb-basic": basicFeatures, + "tailordb-enum": enumFeatures, + "tailordb-hooks": hooksFeatures, + "tailordb-object": objectFeatures, + "tailordb-optional": optionalFeatures, + "tailordb-relation": relationFeatures, + "tailordb-validate": validateFeatures, +}; + +describe("perf feature scripts pass through parseTypes", () => { + for (const [name, mod] of Object.entries(featureModules)) { + it(`${name}: every exported type parses`, () => { + const types = Object.values(mod) as { name: string }[]; + const rawTypes = toSchemaOutputs(Object.fromEntries(types.map((t) => [t.name, t]))); + expect(() => parseTypes(rawTypes, "perf", {})).not.toThrow(); + }); + } +}); diff --git a/packages/sdk/scripts/perf/features/tailordb-hooks.ts b/packages/sdk/scripts/perf/features/tailordb-hooks.ts index 4247896b3..c31d18bc4 100644 --- a/packages/sdk/scripts/perf/features/tailordb-hooks.ts +++ b/packages/sdk/scripts/perf/features/tailordb-hooks.ts @@ -1,66 +1,116 @@ /** * TailorDB Hooks Performance Test * - * Tests type inference cost for field hooks (create, update) + * Tests type inference cost for record-level hooks (create, update) */ import { db } from "../../../src/configure"; -export const type0 = db.type("Type0", { - name: db.string().hooks({ create: () => "default" }), - createdAt: db.datetime().hooks({ create: () => new Date() }), - updatedAt: db.datetime({ optional: true }).hooks({ update: () => new Date() }), -}); +export const type0 = db + .type("Type0", { + name: db.string(), + createdAt: db.datetime(), + updatedAt: db.datetime({ optional: true }), + }) + .hooks({ + create: () => ({ createdAt: new Date() }), + update: () => ({ updatedAt: new Date() }), + }); -export const type1 = db.type("Type1", { - name: db.string().hooks({ create: () => "default" }), - createdAt: db.datetime().hooks({ create: () => new Date() }), - updatedAt: db.datetime({ optional: true }).hooks({ update: () => new Date() }), -}); +export const type1 = db + .type("Type1", { + name: db.string(), + createdAt: db.datetime(), + updatedAt: db.datetime({ optional: true }), + }) + .hooks({ + create: () => ({ createdAt: new Date() }), + update: () => ({ updatedAt: new Date() }), + }); -export const type2 = db.type("Type2", { - name: db.string().hooks({ create: () => "default" }), - createdAt: db.datetime().hooks({ create: () => new Date() }), - updatedAt: db.datetime({ optional: true }).hooks({ update: () => new Date() }), -}); +export const type2 = db + .type("Type2", { + name: db.string(), + createdAt: db.datetime(), + updatedAt: db.datetime({ optional: true }), + }) + .hooks({ + create: () => ({ createdAt: new Date() }), + update: () => ({ updatedAt: new Date() }), + }); -export const type3 = db.type("Type3", { - name: db.string().hooks({ create: () => "default" }), - createdAt: db.datetime().hooks({ create: () => new Date() }), - updatedAt: db.datetime({ optional: true }).hooks({ update: () => new Date() }), -}); +export const type3 = db + .type("Type3", { + name: db.string(), + createdAt: db.datetime(), + updatedAt: db.datetime({ optional: true }), + }) + .hooks({ + create: () => ({ createdAt: new Date() }), + update: () => ({ updatedAt: new Date() }), + }); -export const type4 = db.type("Type4", { - name: db.string().hooks({ create: () => "default" }), - createdAt: db.datetime().hooks({ create: () => new Date() }), - updatedAt: db.datetime({ optional: true }).hooks({ update: () => new Date() }), -}); +export const type4 = db + .type("Type4", { + name: db.string(), + createdAt: db.datetime(), + updatedAt: db.datetime({ optional: true }), + }) + .hooks({ + create: () => ({ createdAt: new Date() }), + update: () => ({ updatedAt: new Date() }), + }); -export const type5 = db.type("Type5", { - name: db.string().hooks({ create: () => "default" }), - createdAt: db.datetime().hooks({ create: () => new Date() }), - updatedAt: db.datetime({ optional: true }).hooks({ update: () => new Date() }), -}); +export const type5 = db + .type("Type5", { + name: db.string(), + createdAt: db.datetime(), + updatedAt: db.datetime({ optional: true }), + }) + .hooks({ + create: () => ({ createdAt: new Date() }), + update: () => ({ updatedAt: new Date() }), + }); -export const type6 = db.type("Type6", { - name: db.string().hooks({ create: () => "default" }), - createdAt: db.datetime().hooks({ create: () => new Date() }), - updatedAt: db.datetime({ optional: true }).hooks({ update: () => new Date() }), -}); +export const type6 = db + .type("Type6", { + name: db.string(), + createdAt: db.datetime(), + updatedAt: db.datetime({ optional: true }), + }) + .hooks({ + create: () => ({ createdAt: new Date() }), + update: () => ({ updatedAt: new Date() }), + }); -export const type7 = db.type("Type7", { - name: db.string().hooks({ create: () => "default" }), - createdAt: db.datetime().hooks({ create: () => new Date() }), - updatedAt: db.datetime({ optional: true }).hooks({ update: () => new Date() }), -}); +export const type7 = db + .type("Type7", { + name: db.string(), + createdAt: db.datetime(), + updatedAt: db.datetime({ optional: true }), + }) + .hooks({ + create: () => ({ createdAt: new Date() }), + update: () => ({ updatedAt: new Date() }), + }); -export const type8 = db.type("Type8", { - name: db.string().hooks({ create: () => "default" }), - createdAt: db.datetime().hooks({ create: () => new Date() }), - updatedAt: db.datetime({ optional: true }).hooks({ update: () => new Date() }), -}); +export const type8 = db + .type("Type8", { + name: db.string(), + createdAt: db.datetime(), + updatedAt: db.datetime({ optional: true }), + }) + .hooks({ + create: () => ({ createdAt: new Date() }), + update: () => ({ updatedAt: new Date() }), + }); -export const type9 = db.type("Type9", { - name: db.string().hooks({ create: () => "default" }), - createdAt: db.datetime().hooks({ create: () => new Date() }), - updatedAt: db.datetime({ optional: true }).hooks({ update: () => new Date() }), -}); +export const type9 = db + .type("Type9", { + name: db.string(), + createdAt: db.datetime(), + updatedAt: db.datetime({ optional: true }), + }) + .hooks({ + create: () => ({ createdAt: new Date() }), + update: () => ({ updatedAt: new Date() }), + }); diff --git a/packages/sdk/scripts/perf/features/tailordb-validate.ts b/packages/sdk/scripts/perf/features/tailordb-validate.ts index e4eacf505..e531ab994 100644 --- a/packages/sdk/scripts/perf/features/tailordb-validate.ts +++ b/packages/sdk/scripts/perf/features/tailordb-validate.ts @@ -1,66 +1,126 @@ /** * TailorDB Validation Rules Performance Test * - * Tests type inference cost for field validation + * Tests type inference cost for record-level validation */ import { db } from "../../../src/configure"; -export const type0 = db.type("Type0", { - name: db.string().validate(({ value }) => value.length > 0), - email: db.string().validate([({ value }) => value.includes("@"), "Must be valid email"]), - age: db.int().validate(({ value }) => value >= 0), -}); +export const type0 = db + .type("Type0", { + name: db.string(), + email: db.string(), + age: db.int(), + }) + .validate([ + ({ data }) => data.name.length > 0, + [({ data }) => data.email.includes("@"), "Must be valid email"], + ({ data }) => data.age >= 0, + ]); -export const type1 = db.type("Type1", { - name: db.string().validate(({ value }) => value.length > 0), - email: db.string().validate([({ value }) => value.includes("@"), "Must be valid email"]), - age: db.int().validate(({ value }) => value >= 0), -}); +export const type1 = db + .type("Type1", { + name: db.string(), + email: db.string(), + age: db.int(), + }) + .validate([ + ({ data }) => data.name.length > 0, + [({ data }) => data.email.includes("@"), "Must be valid email"], + ({ data }) => data.age >= 0, + ]); -export const type2 = db.type("Type2", { - name: db.string().validate(({ value }) => value.length > 0), - email: db.string().validate([({ value }) => value.includes("@"), "Must be valid email"]), - age: db.int().validate(({ value }) => value >= 0), -}); +export const type2 = db + .type("Type2", { + name: db.string(), + email: db.string(), + age: db.int(), + }) + .validate([ + ({ data }) => data.name.length > 0, + [({ data }) => data.email.includes("@"), "Must be valid email"], + ({ data }) => data.age >= 0, + ]); -export const type3 = db.type("Type3", { - name: db.string().validate(({ value }) => value.length > 0), - email: db.string().validate([({ value }) => value.includes("@"), "Must be valid email"]), - age: db.int().validate(({ value }) => value >= 0), -}); +export const type3 = db + .type("Type3", { + name: db.string(), + email: db.string(), + age: db.int(), + }) + .validate([ + ({ data }) => data.name.length > 0, + [({ data }) => data.email.includes("@"), "Must be valid email"], + ({ data }) => data.age >= 0, + ]); -export const type4 = db.type("Type4", { - name: db.string().validate(({ value }) => value.length > 0), - email: db.string().validate([({ value }) => value.includes("@"), "Must be valid email"]), - age: db.int().validate(({ value }) => value >= 0), -}); +export const type4 = db + .type("Type4", { + name: db.string(), + email: db.string(), + age: db.int(), + }) + .validate([ + ({ data }) => data.name.length > 0, + [({ data }) => data.email.includes("@"), "Must be valid email"], + ({ data }) => data.age >= 0, + ]); -export const type5 = db.type("Type5", { - name: db.string().validate(({ value }) => value.length > 0), - email: db.string().validate([({ value }) => value.includes("@"), "Must be valid email"]), - age: db.int().validate(({ value }) => value >= 0), -}); +export const type5 = db + .type("Type5", { + name: db.string(), + email: db.string(), + age: db.int(), + }) + .validate([ + ({ data }) => data.name.length > 0, + [({ data }) => data.email.includes("@"), "Must be valid email"], + ({ data }) => data.age >= 0, + ]); -export const type6 = db.type("Type6", { - name: db.string().validate(({ value }) => value.length > 0), - email: db.string().validate([({ value }) => value.includes("@"), "Must be valid email"]), - age: db.int().validate(({ value }) => value >= 0), -}); +export const type6 = db + .type("Type6", { + name: db.string(), + email: db.string(), + age: db.int(), + }) + .validate([ + ({ data }) => data.name.length > 0, + [({ data }) => data.email.includes("@"), "Must be valid email"], + ({ data }) => data.age >= 0, + ]); -export const type7 = db.type("Type7", { - name: db.string().validate(({ value }) => value.length > 0), - email: db.string().validate([({ value }) => value.includes("@"), "Must be valid email"]), - age: db.int().validate(({ value }) => value >= 0), -}); +export const type7 = db + .type("Type7", { + name: db.string(), + email: db.string(), + age: db.int(), + }) + .validate([ + ({ data }) => data.name.length > 0, + [({ data }) => data.email.includes("@"), "Must be valid email"], + ({ data }) => data.age >= 0, + ]); -export const type8 = db.type("Type8", { - name: db.string().validate(({ value }) => value.length > 0), - email: db.string().validate([({ value }) => value.includes("@"), "Must be valid email"]), - age: db.int().validate(({ value }) => value >= 0), -}); +export const type8 = db + .type("Type8", { + name: db.string(), + email: db.string(), + age: db.int(), + }) + .validate([ + ({ data }) => data.name.length > 0, + [({ data }) => data.email.includes("@"), "Must be valid email"], + ({ data }) => data.age >= 0, + ]); -export const type9 = db.type("Type9", { - name: db.string().validate(({ value }) => value.length > 0), - email: db.string().validate([({ value }) => value.includes("@"), "Must be valid email"]), - age: db.int().validate(({ value }) => value >= 0), -}); +export const type9 = db + .type("Type9", { + name: db.string(), + email: db.string(), + age: db.int(), + }) + .validate([ + ({ data }) => data.name.length > 0, + [({ data }) => data.email.includes("@"), "Must be valid email"], + ({ data }) => data.age >= 0, + ]); diff --git a/packages/sdk/src/cli/commands/apply/__test_fixtures__/generators-compat-out/db.ts b/packages/sdk/src/cli/commands/apply/__test_fixtures__/generators-compat-out/db.ts new file mode 100644 index 000000000..760dcd5ad --- /dev/null +++ b/packages/sdk/src/cli/commands/apply/__test_fixtures__/generators-compat-out/db.ts @@ -0,0 +1,50 @@ +import { + createGetDB, + type Generated, + type Timestamp, + type NamespaceDB, + type NamespaceInsertable, + type NamespaceSelectable, + type NamespaceTable, + type NamespaceTableName, + type NamespaceTransaction, + type NamespaceUpdateable, +} from "@tailor-platform/sdk/kysely"; + +export interface Namespace { + testdb: { + Order: { + id: Generated; + title: string; + amount: number; + userID: string; + createdAt: Generated; + updatedAt: Generated; + }; + + User: { + id: Generated; + name: string; + email: string; + role: "ADMIN" | "MEMBER"; + createdAt: Generated; + updatedAt: Generated; + }; + }; +} + +export const getDB = createGetDB(); + +export type DB = NamespaceDB; + +export type Transaction = NamespaceTransaction< + Namespace, + K +>; + +type TableName = NamespaceTableName; +export type Table = NamespaceTable; + +export type Insertable = NamespaceInsertable; +export type Selectable = NamespaceSelectable; +export type Updateable = NamespaceUpdateable; diff --git a/packages/sdk/src/cli/commands/apply/__test_fixtures__/generators-compat-out/enums.ts b/packages/sdk/src/cli/commands/apply/__test_fixtures__/generators-compat-out/enums.ts new file mode 100644 index 000000000..fff14103d --- /dev/null +++ b/packages/sdk/src/cli/commands/apply/__test_fixtures__/generators-compat-out/enums.ts @@ -0,0 +1,5 @@ +export const UserRole = { + ADMIN: "ADMIN", + MEMBER: "MEMBER", +} as const; +export type UserRole = (typeof UserRole)[keyof typeof UserRole]; diff --git a/packages/sdk/src/cli/commands/apply/__test_fixtures__/plugins-compat-out/db.ts b/packages/sdk/src/cli/commands/apply/__test_fixtures__/plugins-compat-out/db.ts new file mode 100644 index 000000000..760dcd5ad --- /dev/null +++ b/packages/sdk/src/cli/commands/apply/__test_fixtures__/plugins-compat-out/db.ts @@ -0,0 +1,50 @@ +import { + createGetDB, + type Generated, + type Timestamp, + type NamespaceDB, + type NamespaceInsertable, + type NamespaceSelectable, + type NamespaceTable, + type NamespaceTableName, + type NamespaceTransaction, + type NamespaceUpdateable, +} from "@tailor-platform/sdk/kysely"; + +export interface Namespace { + testdb: { + Order: { + id: Generated; + title: string; + amount: number; + userID: string; + createdAt: Generated; + updatedAt: Generated; + }; + + User: { + id: Generated; + name: string; + email: string; + role: "ADMIN" | "MEMBER"; + createdAt: Generated; + updatedAt: Generated; + }; + }; +} + +export const getDB = createGetDB(); + +export type DB = NamespaceDB; + +export type Transaction = NamespaceTransaction< + Namespace, + K +>; + +type TableName = NamespaceTableName; +export type Table = NamespaceTable; + +export type Insertable = NamespaceInsertable; +export type Selectable = NamespaceSelectable; +export type Updateable = NamespaceUpdateable; diff --git a/packages/sdk/src/cli/commands/apply/__test_fixtures__/plugins-compat-out/enums.ts b/packages/sdk/src/cli/commands/apply/__test_fixtures__/plugins-compat-out/enums.ts new file mode 100644 index 000000000..fff14103d --- /dev/null +++ b/packages/sdk/src/cli/commands/apply/__test_fixtures__/plugins-compat-out/enums.ts @@ -0,0 +1,5 @@ +export const UserRole = { + ADMIN: "ADMIN", + MEMBER: "MEMBER", +} as const; +export type UserRole = (typeof UserRole)[keyof typeof UserRole]; diff --git a/packages/sdk/src/cli/commands/deploy/tailordb/index.test.ts b/packages/sdk/src/cli/commands/deploy/tailordb/index.test.ts index cf1c48869..e94f9b1c6 100644 --- a/packages/sdk/src/cli/commands/deploy/tailordb/index.test.ts +++ b/packages/sdk/src/cli/commands/deploy/tailordb/index.test.ts @@ -491,6 +491,123 @@ describe("planTailorDB (service level)", () => { expect(result.changeSet.type.updates).toHaveLength(0); }); + test("detects record-level validator-only diff as update", async () => { + const tailordbType: TailorDBType = { + name: "Invoice", + pluralForm: "Invoices", + description: "Invoice type", + fields: { + code: { + name: "code", + config: { + type: "string", + required: true, + }, + }, + }, + forwardRelationships: {}, + backwardRelationships: {}, + settings: {}, + permissions: {}, + files: {}, + validate: [ + { + script: { expr: "({ _record_0: 'code required' })" }, + errorMessage: "code required", + }, + ], + }; + + const tailorDBService = createMockTailorDBService("test-tailordb"); + Object.defineProperty(tailorDBService, "types", { + value: { [tailordbType.name]: tailordbType }, + }); + + const client = { + listTailorDBServices: vi.fn().mockResolvedValue({ + tailordbServices: [{ namespace: { name: "test-tailordb" } }], + nextPageToken: "", + }), + listTailorDBTypes: vi.fn().mockResolvedValue({ + tailordbTypes: [ + { + name: "Invoice", + schema: { + description: "Invoice type", + fields: { + code: { + type: "string", + required: true, + allowedValues: [], + description: "", + validate: [], + array: false, + index: false, + unique: false, + foreignKey: false, + vector: false, + fields: {}, + }, + }, + relationships: {}, + settings: { + aggregation: false, + bulkUpsert: false, + draft: false, + defaultQueryLimitSize: "100", + maxBulkUpsertSize: "1000", + pluralForm: "invoices", + publishRecordEvents: false, + disableGqlOperations: { + create: false, + update: false, + delete: false, + read: false, + }, + }, + extends: false, + directives: [], + indexes: {}, + files: {}, + permission: { + create: [], + read: [], + update: [], + delete: [], + }, + }, + }, + ], + nextPageToken: "", + }), + getMetadata: vi.fn().mockResolvedValue({ + metadata: { + labels: { [sdkNameLabelKey]: appName, "sdk-version": "v1-0-0" }, + }, + }), + listTailorDBGQLPermissions: vi.fn().mockResolvedValue({ + permissions: [], + nextPageToken: "", + }), + } as unknown as OperatorClient; + + const application = createMockApplication([tailorDBService]); + const ctx: PlanContext = { + client, + workspaceId, + application, + forRemoval: false, + config: mockConfig, + noSchemaCheck: true, + }; + + const result = await planTailorDB(ctx); + + expect(result.changeSet.type.updates).toHaveLength(1); + expect(result.changeSet.type.updates[0].name).toBe("Invoice"); + expect(result.changeSet.type.unchanged).toHaveLength(0); + }); + test("updates matching type when forceApplyAll is enabled", async () => { const tailordbType: TailorDBType = { name: "Invoice", diff --git a/packages/sdk/src/cli/commands/deploy/tailordb/index.ts b/packages/sdk/src/cli/commands/deploy/tailordb/index.ts index 17db9a399..164f3e238 100644 --- a/packages/sdk/src/cli/commands/deploy/tailordb/index.ts +++ b/packages/sdk/src/cli/commands/deploy/tailordb/index.ts @@ -20,20 +20,8 @@ import { type TailorDBGQLPermission_PolicySchema, type TailorDBGQLPermissionSchema, type TailorDBType as ProtoTailorDBType, - type TailorDBType_FieldConfigSchema, - type TailorDBType_FileConfigSchema, - type TailorDBType_IndexSchema, - type TailorDBType_Permission_ConditionSchema, - type TailorDBType_Permission_OperandSchema, - TailorDBType_Permission_Operator, - TailorDBType_Permission_Permit, - type TailorDBType_Permission_PolicySchema, - type TailorDBType_PermissionSchema, - TailorDBType_PermitAction, - type TailorDBType_RelationshipConfigSchema, type TailorDBTypeSchema, } from "@tailor-proto/tailor/v1/tailordb_resource_pb"; -import * as inflection from "inflection"; import * as path from "pathe"; import { getNamespacesWithMigrations, @@ -47,7 +35,9 @@ import { } from "@/cli/commands/tailordb/migrate/diff-calculator"; import { applyPreMigrationFieldAdjustments, + applyPreMigrationRelationshipAdjustments, buildPreMigrationChangesMap, + buildPreMigrationRelationshipChangesMap, } from "@/cli/commands/tailordb/migrate/pre-migration-schema"; import { reconstructSnapshotFromMigrations, @@ -60,15 +50,13 @@ import { getLatestMigrationNumber, isSnapshotFieldRefOperand, type SchemaSnapshot, - type SnapshotFieldConfig, type TailorDBSnapshotType, - type SnapshotRecordPermission, - type SnapshotActionPermission, type SnapshotPermissionCondition, type SnapshotPermissionOperand, type SnapshotGqlPermission, type SnapshotGqlPermissionPolicy, } from "@/cli/commands/tailordb/migrate/snapshot"; +import { generateTailorDBTypeManifestFromSnapshot } from "@/cli/commands/tailordb/migrate/snapshot-manifest"; import { type TailorDBService } from "@/cli/services/tailordb/service"; import { fetchAll, type OperatorClient } from "@/cli/shared/client"; import { logger } from "@/cli/shared/logger"; @@ -385,16 +373,16 @@ async function reconcileMigrationLabels( } /** - * Build migration execution context for script-based migrations. + * Build migration execution context for migrations that have a migrate.ts on disk. * @param client - Operator client instance * @param migrationContext - Planned TailorDB context - * @param migrationsRequiringScripts - Migrations that require scripts + * @param migrationsWithScripts - Migrations whose migrate.ts file is present * @returns Migration context for script execution */ function buildMigrationContextForScripts( client: OperatorClient, migrationContext: Awaited>["context"], - migrationsRequiringScripts: PendingMigration[], + migrationsWithScripts: PendingMigration[], ): MigrationContext { const authService = migrationContext.application.authService; if (!authService) { @@ -402,7 +390,7 @@ function buildMigrationContextForScripts( } const dbConfigMap: Record = {}; - for (const migration of migrationsRequiringScripts) { + for (const migration of migrationsWithScripts) { if (!(migration.namespace in dbConfigMap)) { dbConfigMap[migration.namespace] = migrationContext.config.db?.[migration.namespace] as | TailorDBServiceConfig @@ -462,17 +450,28 @@ export async function applyTailorDB( // Step 1: Create/update services once at the beginning (services don't need per-migration handling) await executeServicesCreation(client, changeSet); - const migrationsRequiringScripts = pendingMigrations.filter((m) => m.hasScript); + const migrationsWithScripts = pendingMigrations.filter((m) => m.hasScript); // Step 2: Build migration context for script execution (if any migrations require scripts) const migrationCtx = - migrationsRequiringScripts.length > 0 - ? buildMigrationContextForScripts(client, migrationContext, migrationsRequiringScripts) + migrationsWithScripts.length > 0 + ? buildMigrationContextForScripts(client, migrationContext, migrationsWithScripts) : undefined; + // Surface migrations whose diff does not require a script but where the + // user-authored migrate.ts will still run. Without this, a stray + // migrate.ts file silently re-applies on every deploy. + for (const migration of migrationsWithScripts) { + if (!migration.diff.requiresMigrationScript) { + logger.info( + `Migration ${formatMigrationNumber(migration.number)} (${migration.namespace}) will execute migrate.ts even though its diff does not require one.`, + ); + } + } + // Step 3: Execute each migration sequentially: pre -> script -> post - if (migrationsRequiringScripts.length > 0) { - logger.info(`Executing ${migrationsRequiringScripts.length} data migration(s)...`); + if (migrationsWithScripts.length > 0) { + logger.info(`Executing ${migrationsWithScripts.length} data migration(s)...`); logger.newline(); } @@ -509,7 +508,7 @@ export async function applyTailorDB( ); } - if (migrationsRequiringScripts.length > 0) { + if (migrationsWithScripts.length > 0) { logger.newline(); logger.success(`All data migrations completed successfully.`); } @@ -765,9 +764,45 @@ async function executeSingleMigrationPrePhase( // breaking changes (required-add, unique-add, enum value removal) and the // warning-tier field_removed, since the Pre-phase relaxes both. const preMigrationChanges = buildPreMigrationChangesMap([migration]); + const preMigrationRelationshipChanges = buildPreMigrationRelationshipChangesMap([migration]); const affectedTypes = getAffectedTypeNames(migration); const createdBeforeMigration = new Set(processedTypes.created); + // Build a cloned request whose schema has the Pre-phase relaxations applied. + // Returns the original request when nothing needs adjusting so identity + // checks on the changeset still work. + type AnyTailorDBRequest = { + tailordbType?: { name?: string; schema?: { fields?: object; relationships?: object } }; + }; + const adjustForPreMigration = (request: T): T => { + const typeName = request.tailordbType?.name; + const fieldChanges = typeName ? preMigrationChanges.get(typeName) : undefined; + const relChanges = typeName ? preMigrationRelationshipChanges.get(typeName) : undefined; + if ((!fieldChanges || fieldChanges.size === 0) && (!relChanges || relChanges.size === 0)) { + return request; + } + const cloned = structuredClone(request); + const schema = cloned.tailordbType?.schema; + if (schema && fieldChanges && fieldChanges.size > 0 && schema.fields) { + applyPreMigrationFieldAdjustments( + schema.fields as Parameters[0], + fieldChanges, + ); + } + if (schema && relChanges && relChanges.size > 0) { + // structuredClone preserves an empty `relationships` map even when the + // type had no original relationships, but defensive-init keeps the + // helper independent from that detail. + schema.relationships ??= {}; + applyPreMigrationRelationshipAdjustments( + schema.relationships as Parameters[0], + relChanges, + ); + } + return cloned; + }; + + // Types - create/update only types affected by this migration await Promise.all([ ...changeSet.type.creates .filter((create) => { @@ -782,15 +817,12 @@ async function executeSingleMigrationPrePhase( if (!snapshotType) return undefined; if (typeName) processedTypes.created.add(typeName); + // Use the per-migration snapshot manifest as the request body, then + // run it through the Pre-phase adjuster so removed-field / removed- + // relationship relaxations apply uniformly. const clonedRequest = structuredClone(create.request); clonedRequest.tailordbType = snapshotType; - - const typeChanges = typeName ? preMigrationChanges.get(typeName) : undefined; - if (typeChanges && typeChanges.size > 0 && clonedRequest.tailordbType?.schema?.fields) { - applyPreMigrationFieldAdjustments(clonedRequest.tailordbType.schema.fields, typeChanges); - } - - return client.createTailorDBType(clonedRequest); + return client.createTailorDBType(adjustForPreMigration(clonedRequest)); }), // Update types already created in previous migrations (from create list) ...changeSet.type.creates @@ -806,16 +838,14 @@ async function executeSingleMigrationPrePhase( if (!snapshotType) return undefined; if (typeName) processedTypes.updated.add(typeName); - const clonedTypeRequest = structuredClone(snapshotType); - const typeChanges = typeName ? preMigrationChanges.get(typeName) : undefined; - if (typeChanges && typeChanges.size > 0 && clonedTypeRequest.schema?.fields) { - applyPreMigrationFieldAdjustments(clonedTypeRequest.schema.fields, typeChanges); - } + const clonedRequest = structuredClone(create.request); + clonedRequest.tailordbType = snapshotType; + const adjusted = adjustForPreMigration(clonedRequest); return client.updateTailorDBType({ - workspaceId: create.request.workspaceId, - namespaceName: create.request.namespaceName, - tailordbType: clonedTypeRequest, + workspaceId: adjusted.workspaceId, + namespaceName: adjusted.namespaceName, + tailordbType: adjusted.tailordbType, }); }), // Update types that are affected by this migration @@ -834,13 +864,7 @@ async function executeSingleMigrationPrePhase( const clonedRequest = structuredClone(update.request); clonedRequest.tailordbType = snapshotType; - - const typeChanges = typeName ? preMigrationChanges.get(typeName) : undefined; - if (typeChanges && typeChanges.size > 0 && clonedRequest.tailordbType?.schema?.fields) { - applyPreMigrationFieldAdjustments(clonedRequest.tailordbType.schema.fields, typeChanges); - } - - return client.updateTailorDBType(clonedRequest); + return client.updateTailorDBType(adjustForPreMigration(clonedRequest)); }), ]); @@ -916,7 +940,14 @@ async function executeSingleMigrationPostPhase( ): Promise { // Re-use the pre-migration changes map to know which types were touched in // this migration (so we send the post-phase final-schema update for them). + // Relationship-only adjustments also need a Post-phase final update so the + // Pre-phase restoration of removed relationships does not leak through. const preMigrationChanges = buildPreMigrationChangesMap([migration]); + const preMigrationRelationshipChanges = buildPreMigrationRelationshipChangesMap([migration]); + const preMigrationTypes = new Set([ + ...preMigrationChanges.keys(), + ...preMigrationRelationshipChanges.keys(), + ]); const affectedTypes = getAffectedTypeNames(migration); const deletedTypeNames = getDeletedTypeNames(migration); @@ -930,7 +961,7 @@ async function executeSingleMigrationPostPhase( ...changeSet.type.creates .filter((create) => { const typeName = create.request.tailordbType?.name; - return typeName && affectedTypes.has(typeName) && preMigrationChanges.has(typeName); + return typeName && affectedTypes.has(typeName) && preMigrationTypes.has(typeName); }) .map((create) => { const typeName = create.request.tailordbType?.name; @@ -948,7 +979,7 @@ async function executeSingleMigrationPostPhase( ...changeSet.type.updates .filter((update) => { const typeName = update.request.tailordbType?.name; - return typeName && affectedTypes.has(typeName) && preMigrationChanges.has(typeName); + return typeName && affectedTypes.has(typeName) && preMigrationTypes.has(typeName); }) .map((update) => { const typeName = update.request.tailordbType?.name; @@ -1522,6 +1553,7 @@ function normalizeComparableTailorDBType(type: unknown) { indexes?: Record; files?: Record; permission?: Record; + typeValidate?: Record; }; } | null; return normalizeTailorDBCompareValue( @@ -1535,6 +1567,7 @@ function normalizeComparableTailorDBType(type: unknown) { indexes: normalized?.schema?.indexes ?? {}, files: normalized?.schema?.files ?? {}, permission: normalized?.schema?.permission ?? {}, + typeValidate: normalized?.schema?.typeValidate ?? {}, }, }, [], @@ -1608,10 +1641,12 @@ function isNumericLikeValue(value: string | number | bigint): boolean { return typeof value === "number" || typeof value === "bigint" || /^-?\d+$/.test(value); } -// TODO(remiposo): Copied the type-processor / aggregator processing almost as-is. -// This will need refactoring later. /** - * Generate a TailorDB type manifest from snapshot-shaped type + * Generate a TailorDB type manifest from snapshot-shaped type. + * + * Delegates to `generateTailorDBTypeManifestFromSnapshot` (the single source of + * truth for snapshot→proto conversion shared with migration apply) after + * resolving `publishRecordEvents` from the executor set. * @param {TailorDBSnapshotType} type - Snapshot-shaped TailorDB type * @param {ReadonlySet} executorUsedTypes - Set of types used by executors * @param {GqlOperations} [namespaceGqlOperations] - Default gqlOperations for the namespace (already normalized) @@ -1622,357 +1657,22 @@ function generateTailorDBTypeManifest( executorUsedTypes: ReadonlySet, namespaceGqlOperations?: GqlOperations, ): MessageInitShape { - // Ensures that explicitly provided pluralForm like "PurchaseOrderList" becomes "purchaseOrderList". - const pluralForm = inflection.camelize(type.pluralForm, true); - - const defaultSettings: { - aggregation: boolean; - bulkUpsert: boolean; - draft: boolean; - defaultQueryLimitSize: bigint; - maxBulkUpsertSize: bigint; - pluralForm: string; - publishRecordEvents: boolean; - disableGqlOperations?: { - create: boolean; - update: boolean; - delete: boolean; - read: boolean; - }; - } = { - aggregation: type.settings?.aggregation || false, - bulkUpsert: type.settings?.bulkUpsert || false, - draft: false, - defaultQueryLimitSize: 100n, - maxBulkUpsertSize: 1000n, - pluralForm, - publishRecordEvents: false, - }; - // Determine publishRecordEvents (user-facing name: publishEvents): - // - If user explicitly sets a value (true or false), respect that (validation already ensures no executor conflict) - // - If not set, use executor detection (true if executor uses this type) + // - If user explicitly sets a value (true or false), respect that + // (validation in planTypes ensures no executor conflict). + // - If not set, use executor detection (true if executor uses this type). + // - Otherwise default to false inside the snapshot-manifest converter. + let publishRecordEvents: boolean | undefined; if (type.settings?.publishEvents !== undefined) { - defaultSettings.publishRecordEvents = type.settings.publishEvents; + publishRecordEvents = type.settings.publishEvents; } else if (executorUsedTypes.has(type.name)) { - defaultSettings.publishRecordEvents = true; - } - - // Both type.settings.gqlOperations and namespaceGqlOperations are already normalized by schema - const ops = type.settings?.gqlOperations ?? namespaceGqlOperations; - if (ops) { - defaultSettings.disableGqlOperations = { - create: ops.create === false, - update: ops.update === false, - delete: ops.delete === false, - read: ops.read === false, - }; - } - - const fields: Record> = {}; - - Object.keys(type.fields) - .filter((fieldName) => fieldName !== "id") - .forEach((fieldName) => { - const fieldConfig = type.fields[fieldName]; - const fieldType = fieldConfig.type; - const fieldEntry: MessageInitShape = { - type: fieldType, - allowedValues: fieldType === "enum" ? fieldConfig.allowedValues || [] : [], - description: fieldConfig.description || "", - validate: toProtoFieldValidate(fieldConfig), - array: fieldConfig.array || false, - index: fieldConfig.index || false, - unique: fieldConfig.unique || false, - foreignKey: fieldConfig.foreignKey || false, - foreignKeyType: fieldConfig.foreignKeyType, - foreignKeyField: fieldConfig.foreignKeyField, - required: fieldConfig.required, - vector: fieldConfig.vector || false, - ...toProtoFieldHooks(fieldConfig), - ...(fieldConfig.serial && { - serial: { - start: fieldConfig.serial.start as unknown as bigint, - ...(fieldConfig.serial.maxValue && { - maxValue: fieldConfig.serial.maxValue as unknown as bigint, - }), - ...(fieldConfig.serial.format && { - format: fieldConfig.serial.format, - }), - }, - }), - ...(fieldConfig.scale !== undefined && { scale: fieldConfig.scale }), - }; - - // Handle nested fields - if (fieldConfig.type === "nested" && fieldConfig.fields) { - fieldEntry.fields = processNestedFields(fieldConfig.fields); - } - - fields[fieldName] = fieldEntry; - }); - - const relationships: Record< - string, - MessageInitShape - > = {}; - - for (const [relationName, rel] of Object.entries(type.forwardRelationships ?? {})) { - relationships[relationName] = { - refType: rel.targetType, - refField: rel.sourceField, - srcField: rel.targetField, - array: rel.isArray, - description: rel.description, - }; - } - - for (const [relationName, rel] of Object.entries(type.backwardRelationships ?? {})) { - relationships[relationName] = { - refType: rel.targetType, - refField: rel.targetField, - srcField: rel.sourceField, - array: rel.isArray, - description: rel.description, - }; - } - - // Process indexes from metadata - const indexes: Record> = {}; - if (type.indexes) { - Object.entries(type.indexes).forEach(([key, index]) => { - indexes[key] = { - fieldNames: index.fields, - unique: index.unique || false, - }; - }); - } - - // Process files from metadata - const files: Record> = {}; - if (type.files) { - Object.entries(type.files).forEach(([key, description]) => { - files[key] = { description: description || "" }; - }); - } - - // To be secure by default, add Permission settings that reject everyone - // when Permission/RecordPermission is not configured. - const defaultPermission: MessageInitShape = { - create: [], - read: [], - update: [], - delete: [], - }; - const permission = type.permissions?.record - ? protoPermission(type.permissions.record) - : defaultPermission; - - return { - name: type.name, - schema: { - description: type.description || "", - fields, - relationships: relationships, - settings: defaultSettings, - extends: false, - directives: [], - indexes, - files, - permission, - }, - }; -} - -function toProtoFieldValidate( - fieldConfig: SnapshotFieldConfig, -): MessageInitShape["validate"] { - return (fieldConfig.validate || []).map((val) => ({ - action: TailorDBType_PermitAction.DENY, - errorMessage: val.errorMessage || "", - ...(val.script && { - script: { - expr: val.script.expr ? `!${val.script.expr}` : "", - }, - }), - })); -} - -function toProtoFieldHooks( - fieldConfig: SnapshotFieldConfig, -): Pick, "hooks"> | Record { - if (!fieldConfig.hooks) { - return {}; + publishRecordEvents = true; } - return { - hooks: { - create: fieldConfig.hooks.create - ? { - expr: fieldConfig.hooks.create.expr || "", - } - : undefined, - update: fieldConfig.hooks.update - ? { - expr: fieldConfig.hooks.update.expr || "", - } - : undefined, - }, - }; -} -function processNestedFields( - fields: Record, -): Record> { - const nestedFields: Record> = {}; - - Object.entries(fields).forEach(([nestedFieldName, nestedFieldConfig]) => { - const nestedType = nestedFieldConfig.type; - - if (nestedType === "nested" && nestedFieldConfig.fields) { - const deepNestedFields = processNestedFields(nestedFieldConfig.fields); - nestedFields[nestedFieldName] = { - type: "nested", - allowedValues: nestedFieldConfig.allowedValues || [], - description: nestedFieldConfig.description || "", - validate: toProtoFieldValidate(nestedFieldConfig), - required: nestedFieldConfig.required, - array: nestedFieldConfig.array ?? false, - index: false, - unique: false, - foreignKey: false, - vector: false, - ...toProtoFieldHooks(nestedFieldConfig), - fields: deepNestedFields, - ...(nestedFieldConfig.scale !== undefined && { - scale: nestedFieldConfig.scale, - }), - }; - } else { - nestedFields[nestedFieldName] = { - type: nestedType, - allowedValues: nestedType === "enum" ? nestedFieldConfig.allowedValues || [] : [], - description: nestedFieldConfig.description || "", - validate: toProtoFieldValidate(nestedFieldConfig), - required: nestedFieldConfig.required, - array: nestedFieldConfig.array ?? false, - index: false, - unique: false, - foreignKey: false, - vector: false, - ...toProtoFieldHooks(nestedFieldConfig), - ...(nestedFieldConfig.serial && { - serial: { - start: nestedFieldConfig.serial.start as unknown as bigint, - ...(nestedFieldConfig.serial.maxValue && { - maxValue: nestedFieldConfig.serial.maxValue as unknown as bigint, - }), - ...(nestedFieldConfig.serial.format && { - format: nestedFieldConfig.serial.format, - }), - }, - }), - ...(nestedFieldConfig.scale !== undefined && { - scale: nestedFieldConfig.scale, - }), - }; - } + return generateTailorDBTypeManifestFromSnapshot(type, { + publishRecordEvents, + namespaceGqlOperations, }); - - return nestedFields; -} - -function protoPermission( - permission: SnapshotRecordPermission, -): MessageInitShape { - return { - create: permission.create.map((policy) => protoPolicy(policy)), - read: permission.read.map((policy) => protoPolicy(policy)), - update: permission.update.map((policy) => protoPolicy(policy)), - delete: permission.delete.map((policy) => protoPolicy(policy)), - }; -} - -function protoPolicy( - policy: SnapshotActionPermission, -): MessageInitShape { - let permit: TailorDBType_Permission_Permit; - switch (policy.permit) { - case "allow": - permit = TailorDBType_Permission_Permit.ALLOW; - break; - case "deny": - permit = TailorDBType_Permission_Permit.DENY; - break; - default: - throw new Error(`Unknown permission: ${policy.permit satisfies never}`); - } - return { - conditions: policy.conditions.map((cond) => protoCondition(cond)), - permit, - description: policy.description, - }; -} - -function protoCondition( - condition: SnapshotPermissionCondition, -): MessageInitShape { - const [left, operator, right] = condition; - - const l = protoOperand(left); - const r = protoOperand(right); - let op: TailorDBType_Permission_Operator; - switch (operator) { - case "eq": - op = TailorDBType_Permission_Operator.EQ; - break; - case "ne": - op = TailorDBType_Permission_Operator.NE; - break; - case "in": - op = TailorDBType_Permission_Operator.IN; - break; - case "nin": - op = TailorDBType_Permission_Operator.NIN; - break; - case "hasAny": - op = TailorDBType_Permission_Operator.HAS_ANY; - break; - case "nhasAny": - op = TailorDBType_Permission_Operator.NHAS_ANY; - break; - default: - throw new Error(`Unknown operator: ${operator satisfies never}`); - } - return { - left: l, - operator: op, - right: r, - }; -} - -function protoOperand( - operand: SnapshotPermissionOperand, -): MessageInitShape { - if (isSnapshotFieldRefOperand(operand)) { - if ("user" in operand) { - return { kind: { case: "userField", value: operand.user } }; - } - if ("record" in operand) { - return { kind: { case: "recordField", value: operand.record } }; - } - if ("newRecord" in operand) { - return { kind: { case: "newRecordField", value: operand.newRecord } }; - } - if ("oldRecord" in operand) { - return { kind: { case: "oldRecordField", value: operand.oldRecord } }; - } - operand satisfies never; - throw new Error(`Unknown field-ref operand shape: ${JSON.stringify(operand)}`); - } - - return { - kind: { case: "value", value: fromJson(ValueSchema, operand) }, - }; } type CreateGqlPermission = { diff --git a/packages/sdk/src/cli/commands/tailordb/migrate/pre-migration-schema.test.ts b/packages/sdk/src/cli/commands/tailordb/migrate/pre-migration-schema.test.ts new file mode 100644 index 000000000..a022f9538 --- /dev/null +++ b/packages/sdk/src/cli/commands/tailordb/migrate/pre-migration-schema.test.ts @@ -0,0 +1,224 @@ +import { describe, expect, it } from "vitest"; +import { SCHEMA_SNAPSHOT_VERSION, type DiffChange, type MigrationDiff } from "./diff-calculator"; +import { + applyPreMigrationRelationshipAdjustments, + buildPreMigrationRelationshipChangesMap, +} from "./pre-migration-schema"; +import type { PendingMigration } from "./types"; + +function makeMigration(changes: DiffChange[]): PendingMigration { + const diff: MigrationDiff = { + version: SCHEMA_SNAPSHOT_VERSION, + namespace: "ns", + createdAt: "2026-01-01T00:00:00Z", + changes, + hasBreakingChanges: false, + breakingChanges: [], + hasWarnings: false, + warnings: [], + requiresMigrationScript: false, + }; + return { + number: 1, + scriptPath: "/tmp/migrate.ts", + hasScript: false, + diffPath: "/tmp/diff.json", + namespace: "ns", + migrationsDir: "/tmp", + diff, + }; +} + +describe("buildPreMigrationRelationshipChangesMap", () => { + it("collects relationship_removed entries keyed by typeName/relationshipName", () => { + const migration = makeMigration([ + { + kind: "relationship_removed", + typeName: "Order", + relationshipName: "user", + relationshipType: "forward", + before: { + targetType: "User", + targetField: "userId", + sourceField: "userId", + isArray: false, + description: "", + }, + }, + { + kind: "relationship_added", + typeName: "Order", + relationshipName: "newRel", + relationshipType: "forward", + after: { + targetType: "Other", + targetField: "otherId", + sourceField: "otherId", + isArray: false, + description: "", + }, + }, + ]); + + const map = buildPreMigrationRelationshipChangesMap([migration]); + + expect(map.size).toBe(1); + const orderChanges = map.get("Order"); + expect(orderChanges).toBeDefined(); + expect(orderChanges?.size).toBe(1); + expect(orderChanges?.get("user")?.kind).toBe("relationship_removed"); + }); + + it("ignores other change kinds and entries missing a relationshipName", () => { + const migration = makeMigration([ + { + kind: "field_removed", + typeName: "Order", + fieldName: "userId", + before: { type: "uuid", required: true }, + }, + { + kind: "relationship_modified", + typeName: "Order", + relationshipName: "stillThere", + relationshipType: "forward", + reason: "targetField changed", + before: { + targetType: "User", + targetField: "userId", + sourceField: "userId", + isArray: false, + description: "", + }, + after: { + targetType: "User", + targetField: "userId", + sourceField: "userId", + isArray: true, + description: "", + }, + }, + ]); + + const map = buildPreMigrationRelationshipChangesMap([migration]); + expect(map.size).toBe(0); + }); +}); + +describe("applyPreMigrationRelationshipAdjustments", () => { + it("restores a forward relationship using sourceField as the proto refField", () => { + const relationships: Record< + string, + { refType: string; refField: string; srcField: string; array: boolean; description: string } + > = {}; + const typeChanges = new Map([ + [ + "user", + { + kind: "relationship_removed", + typeName: "Order", + relationshipName: "user", + relationshipType: "forward", + before: { + targetType: "User", + targetField: "userId", + sourceField: "userId", + isArray: false, + description: "ref to user", + }, + }, + ], + ]); + + applyPreMigrationRelationshipAdjustments(relationships, typeChanges); + + expect(relationships.user).toEqual({ + refType: "User", + refField: "userId", + srcField: "userId", + array: false, + description: "ref to user", + }); + }); + + it("restores a backward relationship using targetField as the proto refField", () => { + const relationships: Record< + string, + { refType: string; refField: string; srcField: string; array: boolean; description: string } + > = {}; + const typeChanges = new Map([ + [ + "orders", + { + kind: "relationship_removed", + typeName: "User", + relationshipName: "orders", + relationshipType: "backward", + before: { + targetType: "Order", + targetField: "userId", + sourceField: "id", + isArray: true, + description: "all orders for this user", + }, + }, + ], + ]); + + applyPreMigrationRelationshipAdjustments(relationships, typeChanges); + + expect(relationships.orders).toEqual({ + refType: "Order", + refField: "userId", + srcField: "id", + array: true, + description: "all orders for this user", + }); + }); + + it("skips non-removed change kinds and entries without a before snapshot", () => { + const relationships: Record = {}; + const typeChanges = new Map([ + [ + "withoutBefore", + { + kind: "relationship_removed", + typeName: "Order", + relationshipName: "withoutBefore", + relationshipType: "forward", + }, + ], + [ + "modified", + { + kind: "relationship_modified", + typeName: "Order", + relationshipName: "modified", + relationshipType: "forward", + reason: "targetField changed", + before: { + targetType: "User", + targetField: "userId", + sourceField: "userId", + isArray: false, + description: "", + }, + after: { + targetType: "User", + targetField: "userId", + sourceField: "userId", + isArray: true, + description: "", + }, + }, + ], + ]); + + applyPreMigrationRelationshipAdjustments( + relationships as Parameters[0], + typeChanges, + ); + + expect(relationships).toEqual({}); + }); +}); diff --git a/packages/sdk/src/cli/commands/tailordb/migrate/pre-migration-schema.ts b/packages/sdk/src/cli/commands/tailordb/migrate/pre-migration-schema.ts index dde430bdc..6468c74a8 100644 --- a/packages/sdk/src/cli/commands/tailordb/migrate/pre-migration-schema.ts +++ b/packages/sdk/src/cli/commands/tailordb/migrate/pre-migration-schema.ts @@ -10,6 +10,10 @@ * - `field_added` with `required: true`: relax to `required: false`. * - `field_modified` optional→required, unique constraint added, enum * value removed: keep the looser side until Post-phase. + * - `relationship_removed`: re-insert the removed relationship so migrate.ts + * can still resolve `innerJoin` through the relationship being dropped in + * the same migration. The physical drop happens in Post-phase together + * with the underlying FK field. * * Type-level deletions (`type_removed`) are handled by the deploy flow, * which retains the type until Post-phase rather than via this module. @@ -18,13 +22,16 @@ * to fix up data. */ -import { convertFieldConfigToProto } from "./snapshot-manifest"; +import { convertFieldConfigToProto, convertRelationshipToProto } from "./snapshot-manifest"; import type { DiffChange } from "./diff-calculator"; -import type { SnapshotFieldConfig } from "./snapshot"; +import type { SnapshotFieldConfig, SnapshotRelationship } from "./snapshot"; import type { PendingMigration } from "./types"; import type { EnumValue } from "@/types/field-types"; import type { MessageInitShape } from "@bufbuild/protobuf"; -import type { TailorDBType_FieldConfigSchema } from "@tailor-proto/tailor/v1/tailordb_resource_pb"; +import type { + TailorDBType_FieldConfigSchema, + TailorDBType_RelationshipConfigSchema, +} from "@tailor-proto/tailor/v1/tailordb_resource_pb"; /** * Diff change kinds that require pre-migration schema adjustments. @@ -35,6 +42,24 @@ const PRE_MIGRATION_FIELD_KINDS = new Set([ "field_removed", ]); +/** + * Get the inner map for `key`, inserting an empty one if absent. + * @param outer - Outer map keyed by typeName + * @param key - Outer key (typeName) + * @returns The inner map (existing or newly created) + */ +function getOrCreateInnerMap( + outer: Map>, + key: string, +): Map { + let inner = outer.get(key); + if (!inner) { + inner = new Map(); + outer.set(key, inner); + } + return inner; +} + /** * Map of pre-migration field changes: typeName -> fieldName -> change. * @@ -58,9 +83,7 @@ export function buildPreMigrationChangesMap( for (const change of migration.diff.changes) { if (!PRE_MIGRATION_FIELD_KINDS.has(change.kind)) continue; if (!change.fieldName) continue; - const perType = map.get(change.typeName) ?? new Map(); - perType.set(change.fieldName, change); - map.set(change.typeName, perType); + getOrCreateInnerMap(map, change.typeName).set(change.fieldName, change); } } return map; @@ -145,3 +168,53 @@ export function applyPreMigrationFieldAdjustments( } } } + +/** + * Map of pre-migration relationship changes: typeName -> relationshipName -> change. + * + * Only `relationship_removed` is tracked — the Pre-phase reinstates the + * removed relationship so that `migrate.ts` can resolve joins via it before + * the Post-phase performs the physical drop alongside the underlying FK field. + */ +export type PreMigrationRelationshipChangesMap = Map>; + +/** + * Build a map of relationship changes that require pre-migration adjustment. + * @param {PendingMigration[]} pendingMigrations - Pending migrations to scan + * @returns {PreMigrationRelationshipChangesMap} Map keyed by typeName/relationshipName + */ +export function buildPreMigrationRelationshipChangesMap( + pendingMigrations: PendingMigration[], +): PreMigrationRelationshipChangesMap { + const map: PreMigrationRelationshipChangesMap = new Map(); + for (const migration of pendingMigrations) { + for (const change of migration.diff.changes) { + if (change.kind !== "relationship_removed") continue; + if (!change.relationshipName) continue; + getOrCreateInnerMap(map, change.typeName).set(change.relationshipName, change); + } + } + return map; +} + +/** + * Restore relationships that were removed in this migration so the Pre-phase + * schema still exposes them to `migrate.ts`. Mutates the supplied map in place. + * @param {Record>} relationships - Relationship map to adjust (mutated in place) + * @param {Map} typeChanges - Relationship changes for this type + */ +export function applyPreMigrationRelationshipAdjustments( + relationships: Record>, + typeChanges: Map, +): void { + for (const [relationshipName, change] of typeChanges) { + if (change.kind !== "relationship_removed") continue; + const before = change.before as SnapshotRelationship | undefined; + if (!before) continue; + + // Mirror the steady-state forward/backward field mapping so Pre-phase and + // steady-state messages agree. + const direction = change.relationshipType ?? "forward"; + relationships[relationshipName] = convertRelationshipToProto(before, direction); + } +} diff --git a/packages/sdk/src/cli/commands/tailordb/migrate/script.test.ts b/packages/sdk/src/cli/commands/tailordb/migrate/script.test.ts new file mode 100644 index 000000000..f74b080eb --- /dev/null +++ b/packages/sdk/src/cli/commands/tailordb/migrate/script.test.ts @@ -0,0 +1,40 @@ +import { describe, expect, it } from "vitest"; +import { parseMigrationNumber } from "./script"; + +describe("parseMigrationNumber", () => { + describe("accepts canonical and integer forms", () => { + it("parses the canonical 4-digit form", () => { + expect(parseMigrationNumber("0001")).toBe(1); + expect(parseMigrationNumber("0042")).toBe(42); + expect(parseMigrationNumber("9999")).toBe(9999); + }); + + it("parses bare integer form", () => { + expect(parseMigrationNumber("1")).toBe(1); + expect(parseMigrationNumber("42")).toBe(42); + expect(parseMigrationNumber("9999")).toBe(9999); + }); + }); + + describe("rejects invalid input", () => { + it("rejects integer forms with leading zeros that are not the canonical 4 digits", () => { + expect(() => parseMigrationNumber("00001")).toThrow(/Invalid migration number format/); + expect(() => parseMigrationNumber("00")).toThrow(/Invalid migration number format/); + }); + + it("rejects non-digit input", () => { + expect(() => parseMigrationNumber("abc")).toThrow(/Invalid migration number format/); + expect(() => parseMigrationNumber("1a")).toThrow(/Invalid migration number format/); + expect(() => parseMigrationNumber("")).toThrow(/Invalid migration number format/); + }); + + it("rejects integers above 9999", () => { + expect(() => parseMigrationNumber("10000")).toThrow(/out of range/); + expect(() => parseMigrationNumber("100000")).toThrow(/out of range/); + }); + + it("rejects the initial schema number (0)", () => { + expect(() => parseMigrationNumber("0000")).toThrow(/initial schema snapshot/); + }); + }); +}); diff --git a/packages/sdk/src/cli/commands/tailordb/migrate/script.ts b/packages/sdk/src/cli/commands/tailordb/migrate/script.ts index 9cfae3bf8..34f667e62 100644 --- a/packages/sdk/src/cli/commands/tailordb/migrate/script.ts +++ b/packages/sdk/src/cli/commands/tailordb/migrate/script.ts @@ -36,36 +36,46 @@ export interface ScriptOptions { } /** - * Add a migrate.ts template to an existing migration directory. - * @param {ScriptOptions} options - Command options + * Parse a user-supplied migration number into the canonical integer form. + * Accepts either the 4-digit form ("0001") or a bare integer ("1"–"9999"). + * Rejects leading-zero non-canonical forms ("00001"), non-digit input, and + * the initial schema number (0000) which cannot have a migration script. + * @param input - User-supplied migration number string + * @returns Parsed migration number in 1–9999 */ -async function script(options: ScriptOptions): Promise { - logBetaWarning("tailordb migration"); - - // Accept either the canonical 4-digit form ("0001") or a bare integer - // ("1"–"9999"). Reject inputs containing non-digit characters, integer - // forms with leading zeros ("00001"), and anything outside the - // 0000-9999 directory range that the migrations system supports. +export function parseMigrationNumber(input: string): number { let migrationNumber: number; - if (isValidMigrationNumber(options.number)) { - migrationNumber = parseInt(options.number, 10); - } else if (/^[1-9]\d*$/.test(options.number)) { - migrationNumber = parseInt(options.number, 10); + if (isValidMigrationNumber(input)) { + migrationNumber = parseInt(input, 10); + } else if (/^[1-9]\d*$/.test(input)) { + migrationNumber = parseInt(input, 10); if (migrationNumber > 9999) { - throw new Error(`Migration number ${options.number} is out of range. Expected 1-9999.`); + throw new Error(`Migration number ${input} is out of range. Expected 1-9999.`); } } else { throw new Error( - `Invalid migration number format: ${options.number}. Expected 4-digit format (e.g., 0001) or integer 1-9999 (e.g., 1).`, + `Invalid migration number format: ${input}. Expected 4-digit format (e.g., 0001) or integer 1-9999 (e.g., 1).`, ); } if (migrationNumber === INITIAL_SCHEMA_NUMBER) { throw new Error( - `Migration ${options.number} is the initial schema snapshot and cannot have a migration script.`, + `Migration ${input} is the initial schema snapshot and cannot have a migration script.`, ); } + return migrationNumber; +} + +/** + * Add a migrate.ts template to an existing migration directory. + * @param {ScriptOptions} options - Command options + */ +async function script(options: ScriptOptions): Promise { + logBetaWarning("tailordb migration"); + + const migrationNumber = parseMigrationNumber(options.number); + const { config } = await loadConfig(options.configPath); const configDir = path.dirname(config.path); @@ -91,6 +101,11 @@ async function script(options: ScriptOptions): Promise { throw new Error(`Migration script already exists at ${migratePath}.`); } + const dbTypesPath = getMigrationFilePath(migrationsDir, migrationNumber, "db"); + if (fs.existsSync(dbTypesPath)) { + throw new Error(`Migration db types file already exists at ${dbTypesPath}.`); + } + const diff = loadDiff(diffPath); // Reconstruct the schema state immediately before this migration so that diff --git a/packages/sdk/src/cli/commands/tailordb/migrate/snapshot-manifest.test.ts b/packages/sdk/src/cli/commands/tailordb/migrate/snapshot-manifest.test.ts index a551c0ede..84c1af134 100644 --- a/packages/sdk/src/cli/commands/tailordb/migrate/snapshot-manifest.test.ts +++ b/packages/sdk/src/cli/commands/tailordb/migrate/snapshot-manifest.test.ts @@ -293,6 +293,23 @@ describe("snapshot-manifest", () => { expect(manifest.schema?.settings?.disableGqlOperations?.delete).toBe(true); }); + it("never emits typeHook even when snapshot still carries a type-level hooks slot", () => { + // Old snapshots predating Case Y may have `TailorDBSnapshotType.hooks` populated. + // The wire format must drop them in favor of per-field hooks; this test + // pins that behavior so we cannot accidentally reintroduce typeHook. + const snapshotType = createTestSnapshotType("Stale", { + hooks: { + create: { expr: "({data}) => ({ ...data })" }, + }, + }); + + const manifest = generateTailorDBTypeManifestFromSnapshot(snapshotType); + + expect( + (manifest.schema as unknown as { typeHook?: unknown } | undefined)?.typeHook, + ).toBeUndefined(); + }); + it("handles hooks configuration", () => { const snapshotType = createTestSnapshotType("User", { fields: { diff --git a/packages/sdk/src/cli/commands/tailordb/migrate/snapshot-manifest.ts b/packages/sdk/src/cli/commands/tailordb/migrate/snapshot-manifest.ts index 0820d0963..5e73a5087 100644 --- a/packages/sdk/src/cli/commands/tailordb/migrate/snapshot-manifest.ts +++ b/packages/sdk/src/cli/commands/tailordb/migrate/snapshot-manifest.ts @@ -20,10 +20,11 @@ import { type TailorDBType_Permission_PolicySchema, type TailorDBType_PermissionSchema, type TailorDBType_RelationshipConfigSchema, + type TailorDBType_TypeValidateSchema, type TailorDBTypeSchema, } from "@tailor-proto/tailor/v1/tailordb_resource_pb"; import * as inflection from "inflection"; -import { isSnapshotFieldRefOperand } from "./snapshot"; +import { buildCombinedTypeValidateExpr, isSnapshotFieldRefOperand } from "./snapshot"; import type { SchemaSnapshot, SnapshotEnumValue, @@ -154,6 +155,8 @@ export function generateTailorDBTypeManifestFromSnapshot( ? convertRecordPermissionToProto(snapshotType.permissions.record) : defaultPermission; + const typeValidate = toProtoSnapshotTypeValidate(snapshotType); + return { name: snapshotType.name, schema: { @@ -166,10 +169,30 @@ export function generateTailorDBTypeManifestFromSnapshot( indexes, files, permission, + ...(typeValidate && { typeValidate }), }, }; } +/** + * Convert a snapshot type's record-level validators into a proto + * `type_validate` message. Returns undefined when there are no validators so + * the caller can omit the field entirely. + * @param snapshotType - Snapshot type containing optional `validate` + * @returns Proto type_validate message or undefined + */ +export function toProtoSnapshotTypeValidate( + snapshotType: TailorDBSnapshotType, +): MessageInitShape | undefined { + const combined = buildCombinedTypeValidateExpr(snapshotType.validate); + if (combined === null) return undefined; + const script = { expr: combined }; + return { + create: script, + update: script, + }; +} + /** * Convert a snapshot field config to proto format * @param {SnapshotFieldConfig} config - Snapshot field config @@ -254,7 +277,13 @@ function toProtoSnapshotFieldHooks( } /** - * Process nested fields from snapshot format to proto format + * Process nested fields from snapshot format to proto format. + * + * Nested fields share the same `FieldConfig` shape as top-level fields but + * the platform ignores index/unique/foreignKey/vector for sub-fields, so we + * force them to `false` after delegating to the shared converter. The + * `foreignKeyType`/`foreignKeyField` strings are likewise scrubbed since + * they only make sense at the top level. * @param {Record} fields - Nested fields * @returns {Record>} Proto nested fields */ @@ -264,65 +293,31 @@ function processNestedFieldsFromSnapshot( const nestedFields: Record> = {}; for (const [fieldName, fieldConfig] of Object.entries(fields)) { - if (fieldConfig.type === "nested" && fieldConfig.fields) { - const deepNestedFields = processNestedFieldsFromSnapshot(fieldConfig.fields); - nestedFields[fieldName] = { - type: "nested", - allowedValues: fieldConfig.allowedValues?.map((v: SnapshotEnumValue) => ({ ...v })) ?? [], - description: fieldConfig.description || "", - validate: toProtoSnapshotFieldValidate(fieldConfig), - required: fieldConfig.required ?? true, - array: fieldConfig.array ?? false, - index: false, - unique: false, - foreignKey: false, - vector: false, - ...toProtoSnapshotFieldHooks(fieldConfig), - fields: deepNestedFields, - ...(fieldConfig.scale !== undefined && { scale: fieldConfig.scale }), - }; - } else { - nestedFields[fieldName] = { - type: fieldConfig.type, - allowedValues: - fieldConfig.type === "enum" - ? (fieldConfig.allowedValues?.map((v: SnapshotEnumValue) => ({ ...v })) ?? []) - : [], - description: fieldConfig.description || "", - validate: toProtoSnapshotFieldValidate(fieldConfig), - required: fieldConfig.required ?? true, - array: fieldConfig.array ?? false, - index: false, - unique: false, - foreignKey: false, - vector: false, - ...toProtoSnapshotFieldHooks(fieldConfig), - ...(fieldConfig.serial && { - serial: { - start: BigInt(fieldConfig.serial.start), - ...(fieldConfig.serial.maxValue !== undefined && { - maxValue: BigInt(fieldConfig.serial.maxValue), - }), - ...(fieldConfig.serial.format && { - format: fieldConfig.serial.format, - }), - }, - }), - ...(fieldConfig.scale !== undefined && { scale: fieldConfig.scale }), - }; - } + const entry = convertFieldConfigToProto(fieldConfig); + entry.index = false; + entry.unique = false; + entry.foreignKey = false; + entry.vector = false; + entry.foreignKeyType = undefined; + entry.foreignKeyField = undefined; + nestedFields[fieldName] = entry; } return nestedFields; } /** - * Convert a snapshot relationship to proto format + * Convert a snapshot relationship to proto format. + * + * Forward and backward relationships swap the `refField` / `srcField` roles — + * forward stores the source FK in `refField`, backward in `srcField`. Both + * the steady-state manifest and the Pre-phase relationship restoration share + * this mapping, so any change must update both call sites together. * @param {SnapshotRelationship} rel - Snapshot relationship * @param {"forward" | "backward"} direction - Relationship direction * @returns {MessageInitShape} Proto relationship config */ -function convertRelationshipToProto( +export function convertRelationshipToProto( rel: SnapshotRelationship, direction: "forward" | "backward", ): MessageInitShape { diff --git a/packages/sdk/src/cli/commands/tailordb/migrate/snapshot.test.ts b/packages/sdk/src/cli/commands/tailordb/migrate/snapshot.test.ts index 00e5cfc5d..68e7c0813 100644 --- a/packages/sdk/src/cli/commands/tailordb/migrate/snapshot.test.ts +++ b/packages/sdk/src/cli/commands/tailordb/migrate/snapshot.test.ts @@ -2,6 +2,7 @@ import * as fs from "node:fs"; import * as path from "pathe"; import { describe, expect, it, beforeEach, afterAll } from "vitest"; import { + applyDiffToSnapshot, createSnapshotFromLocalTypes, loadSnapshot, loadDiff, @@ -758,6 +759,128 @@ describe("snapshot", () => { expect(forwardChange?.relationshipType).toBe("forward"); expect(backwardChange?.relationshipType).toBe("backward"); }); + + it("ignores stale type-level hooks (record-level hooks materialize as field-level diffs)", () => { + // Pre-Case-Y snapshots could carry a type-level `hooks` slot. The wire + // format never reads it now, and field-level hook changes are caught by + // `field_modified` instead, so the comparator must not emit a spurious + // `type_modified` change just because the stale slot disappeared. + const previous: SchemaSnapshot = { + ...createEmptySnapshot(), + types: { + Order: { + name: "Order", + pluralForm: "Orders", + fields: { id: { type: "uuid", required: true } }, + }, + }, + }; + const current: SchemaSnapshot = { + ...createEmptySnapshot(), + types: { + Order: { + name: "Order", + pluralForm: "Orders", + fields: { id: { type: "uuid", required: true } }, + hooks: { + create: { expr: "({data}) => ({ ...data, createdAt: new Date() })" }, + }, + }, + }, + }; + + const diff = compareSnapshots(previous, current); + + expect(diff.changes).toHaveLength(0); + }); + + it("detects record-level validator change", () => { + const previous: SchemaSnapshot = { + ...createEmptySnapshot(), + types: { + Order: { + name: "Order", + pluralForm: "Orders", + fields: { id: { type: "uuid", required: true } }, + validate: [{ script: { expr: "data.quantity > 0" }, errorMessage: "quantity > 0" }], + }, + }, + }; + const current: SchemaSnapshot = { + ...createEmptySnapshot(), + types: { + Order: { + name: "Order", + pluralForm: "Orders", + fields: { id: { type: "uuid", required: true } }, + validate: [ + { script: { expr: "data.quantity > 0" }, errorMessage: "quantity > 0" }, + { script: { expr: "data.name.length > 0" }, errorMessage: "name required" }, + ], + }, + }, + }; + + const diff = compareSnapshots(previous, current); + + expect(diff.changes).toHaveLength(1); + expect(diff.changes[0].kind).toBe("type_modified"); + expect(diff.changes[0].reason).toContain("record-level validators changed"); + }); + + it("persists record-level validator removal across a JSON-stringified diff", () => { + // Regression: when validators are fully removed, currType.validate is + // undefined. If the diff encodes the removal as `validate: undefined`, + // JSON.stringify drops the property and applyDiffToSnapshot's + // `"validate" in after` check falls through, leaving stale validators + // in the reconstructed snapshot. + const previous: SchemaSnapshot = { + ...createEmptySnapshot(), + types: { + Order: { + name: "Order", + pluralForm: "Orders", + fields: { id: { type: "uuid", required: true } }, + validate: [{ script: { expr: "data.quantity > 0" }, errorMessage: "quantity > 0" }], + }, + }, + }; + const current: SchemaSnapshot = { + ...createEmptySnapshot(), + types: { + Order: { + name: "Order", + pluralForm: "Orders", + fields: { id: { type: "uuid", required: true } }, + }, + }, + }; + + const diff = compareSnapshots(previous, current); + const persisted: MigrationDiff = JSON.parse(JSON.stringify(diff)); + const applied = applyDiffToSnapshot(previous, persisted); + + expect(applied.types.Order.validate).toBeUndefined(); + }); + + it("does not detect change when record-level hooks/validate are identical", () => { + const snapshot: SchemaSnapshot = { + ...createEmptySnapshot(), + types: { + Order: { + name: "Order", + pluralForm: "Orders", + fields: { id: { type: "uuid", required: true } }, + hooks: { create: { expr: "createExpr" }, update: { expr: "updateExpr" } }, + validate: [{ script: { expr: "validExpr" }, errorMessage: "msg" }], + }, + }, + }; + + const diff = compareSnapshots(snapshot, snapshot); + + expect(diff.changes).toHaveLength(0); + }); }); // ========================================================================== @@ -2031,6 +2154,112 @@ describe("snapshot", () => { expect(drifts.length).toBe(1); expect(drifts[0].kind).toBe("type_missing_local"); }); + + describe("type_validate drift", () => { + const snapshotWithValidator: SchemaSnapshot = { + version: SCHEMA_SNAPSHOT_VERSION, + namespace, + createdAt: new Date().toISOString(), + types: { + User: { + name: "User", + pluralForm: "Users", + fields: { id: { type: "uuid", required: true } }, + validate: [ + { script: { expr: "(data.age >= 0)" }, errorMessage: "age must be non-negative" }, + ], + }, + }, + }; + const expectedCombined = `Object.assign({}, (data.age >= 0))`; + + function withTypeValidate( + baseType: ProtoTailorDBType, + create: string | null, + update: string | null, + ): ProtoTailorDBType { + return { + ...baseType, + schema: { + ...(baseType.schema ?? { fields: {} }), + typeValidate: { + create: create === null ? undefined : { expr: create }, + update: update === null ? undefined : { expr: update }, + }, + }, + } as unknown as ProtoTailorDBType; + } + + it("returns no drift when remote type_validate matches snapshot", () => { + const remoteTypes = [ + withTypeValidate( + createMockRemoteType("User", { id: { type: "uuid", required: true } }), + expectedCombined, + expectedCombined, + ), + ]; + const drifts = compareRemoteWithSnapshot(remoteTypes, snapshotWithValidator); + expect(drifts).toEqual([]); + }); + + it("detects drift when remote type_validate is missing", () => { + const remoteTypes = [ + createMockRemoteType("User", { id: { type: "uuid", required: true } }), + ]; + const drifts = compareRemoteWithSnapshot(remoteTypes, snapshotWithValidator); + expect(drifts.length).toBe(1); + expect(drifts[0].kind).toBe("type_validate_mismatch"); + expect(drifts[0].typeName).toBe("User"); + }); + + it("detects drift when remote type_validate expr differs", () => { + const remoteTypes = [ + withTypeValidate( + createMockRemoteType("User", { id: { type: "uuid", required: true } }), + "Object.assign({}, (data.age >= 18))", + "Object.assign({}, (data.age >= 18))", + ), + ]; + const drifts = compareRemoteWithSnapshot(remoteTypes, snapshotWithValidator); + expect(drifts.length).toBe(1); + expect(drifts[0].kind).toBe("type_validate_mismatch"); + }); + + it("detects drift when remote create and update exprs disagree with snapshot", () => { + const remoteTypes = [ + withTypeValidate( + createMockRemoteType("User", { id: { type: "uuid", required: true } }), + expectedCombined, + null, + ), + ]; + const drifts = compareRemoteWithSnapshot(remoteTypes, snapshotWithValidator); + expect(drifts.length).toBe(1); + expect(drifts[0].kind).toBe("type_validate_mismatch"); + }); + + it("detects drift when remote has validators but snapshot does not", () => { + const snapshotNoValidator: SchemaSnapshot = { + ...snapshotWithValidator, + types: { + User: { + ...snapshotWithValidator.types.User, + validate: undefined, + }, + }, + }; + const remoteTypes = [ + withTypeValidate( + createMockRemoteType("User", { id: { type: "uuid", required: true } }), + expectedCombined, + expectedCombined, + ), + ]; + const drifts = compareRemoteWithSnapshot(remoteTypes, snapshotNoValidator); + expect(drifts.length).toBe(1); + expect(drifts[0].kind).toBe("type_validate_mismatch"); + }); + }); }); // ========================================================================== diff --git a/packages/sdk/src/cli/commands/tailordb/migrate/snapshot.ts b/packages/sdk/src/cli/commands/tailordb/migrate/snapshot.ts index ad06ced4b..6e86a1823 100644 --- a/packages/sdk/src/cli/commands/tailordb/migrate/snapshot.ts +++ b/packages/sdk/src/cli/commands/tailordb/migrate/snapshot.ts @@ -304,6 +304,11 @@ export interface TailorDBSnapshotType { record?: SnapshotRecordPermission; gql?: SnapshotGqlPermission; }; + hooks?: { + create?: SnapshotHook; + update?: SnapshotHook; + }; + validate?: SnapshotValidation[]; } /** @@ -673,6 +678,13 @@ export function createSnapshotType(type: TailorDBType): TailorDBSnapshotType { } } + if (type.validate && type.validate.length > 0) { + snapshotType.validate = type.validate.map((v) => ({ + script: { expr: v.script.expr }, + errorMessage: v.errorMessage, + })); + } + return snapshotType; } @@ -824,7 +836,7 @@ export function getNextMigrationNumber(migrationsDir: string): number { * @param {MigrationDiff} diff - Diff to apply * @returns {SchemaSnapshot} Resulting snapshot after applying diff */ -function applyDiffToSnapshot(snapshot: SchemaSnapshot, diff: MigrationDiff): SchemaSnapshot { +export function applyDiffToSnapshot(snapshot: SchemaSnapshot, diff: MigrationDiff): SchemaSnapshot { const types = { ...snapshot.types }; for (const change of diff.changes) { @@ -840,12 +852,27 @@ function applyDiffToSnapshot(snapshot: SchemaSnapshot, diff: MigrationDiff): Sch const after = change.after as { indexes?: Record; files?: Record; + hooks?: TailorDBSnapshotType["hooks"]; + validate?: TailorDBSnapshotType["validate"]; }; - types[change.typeName] = { - ...types[change.typeName], - ...(after.indexes !== undefined && { indexes: after.indexes }), - ...(after.files !== undefined && { files: after.files }), - }; + const next = { ...types[change.typeName] }; + if (after.indexes !== undefined) next.indexes = after.indexes; + if (after.files !== undefined) next.files = after.files; + if ("hooks" in after) { + if (after.hooks === undefined) delete next.hooks; + else next.hooks = after.hooks; + } + if ("validate" in after) { + // Empty array is the JSON-persistable sentinel for "validators + // removed" — JSON.stringify would drop a literal undefined here, + // so the diff writer encodes removal as `[]`. + if (after.validate === undefined || after.validate.length === 0) { + delete next.validate; + } else { + next.validate = after.validate; + } + } + types[change.typeName] = next; } break; case "field_added": @@ -1329,6 +1356,75 @@ function compareTypeFields( } } +/** + * Detect changes in record-level validators and emit a type_modified change + * carrying the new values for re-apply. + * + * Note: type-level hooks are no longer emitted by the parser. Record-level + * hooks materialize as per-field FieldHooks and surface via field_modified. + * Stale hooks from old snapshots are therefore intentionally ignored here; + * the wire format would discard them anyway. + * @param ctx - Diff accumulation context + * @param typeName - Type being compared + * @param prevType - Previous-snapshot type + * @param currType - Current-snapshot type + */ +function compareTypeHooksValidate( + ctx: DiffContext, + typeName: string, + prevType: TailorDBSnapshotType, + currType: TailorDBSnapshotType, +): void { + const validateChanged = !areValidationsEqual(prevType.validate, currType.validate); + if (!validateChanged) return; + + ctx.changes.push({ + kind: "type_modified", + typeName, + reason: "record-level validators changed", + before: { + ...(prevType.validate !== undefined && { validate: prevType.validate }), + }, + after: { + // Encode "validators removed" as `[]` so the deletion survives a + // JSON.stringify round-trip when this diff is persisted to disk. + validate: currType.validate ?? [], + }, + }); +} + +/** + * Build the combined `type_validate` script expression for a snapshot type. + * Used by `toProtoSnapshotTypeValidate` (manifest emission) and the remote + * drift comparison so both agree on the canonical local expression. + * @param validators - The snapshot type's `validate` array (or undefined) + * @returns The combined expr string, or null if no validators + */ +export function buildCombinedTypeValidateExpr( + validators: TailorDBSnapshotType["validate"], +): string | null { + if (!validators || validators.length === 0) return null; + // Each parsed validator script evaluates to a map (`{}` on success, + // `{ _record_: msg }` on failure); merge them so all per-predicate + // messages reach the platform. + const exprs = validators.map((v) => v.script.expr || "({})"); + return `Object.assign({}, ${exprs.join(", ")})`; +} + +function areValidationsEqual( + a: TailorDBSnapshotType["validate"], + b: TailorDBSnapshotType["validate"], +): boolean { + const left = a ?? []; + const right = b ?? []; + if (left.length !== right.length) return false; + for (let i = 0; i < left.length; i++) { + if (left[i].errorMessage !== right[i].errorMessage) return false; + if (left[i].script.expr !== right[i].script.expr) return false; + } + return true; +} + /** * Compare type-level indexes * @param {DiffContext} ctx - Diff context @@ -1656,6 +1752,9 @@ export function compareSnapshots(previous: SchemaSnapshot, current: SchemaSnapsh prevType.permissions?.gql, currType.permissions?.gql, ); + + // Compare record-level hooks / validate + compareTypeHooksValidate(ctx, typeName, prevType, currType); } return { @@ -2152,11 +2251,60 @@ export function compareRemoteWithSnapshot( drifts.push(drift); } } + + const validateDrift = compareTypeValidators(typeName, remoteType, snapshotType); + if (validateDrift) drifts.push(validateDrift); } return drifts; } +/** + * Compare type-level (record-level) validators between remote and snapshot. + * Local snapshots store validators as `{script, errorMessage}[]` but emit a + * single combined expression via `buildCombinedTypeValidateExpr`. Detect drift + * when the remote's `type_validate.create`/`.update` script no longer matches + * the expression the snapshot would produce (e.g. validators changed in remote + * out-of-band while the migration label still matches). + * @param typeName - Name of the type + * @param remoteType - Remote type from the platform + * @param snapshotType - Local snapshot type + * @returns Drift info or null if validators match + */ +function compareTypeValidators( + typeName: string, + remoteType: ProtoTailorDBType, + snapshotType: TailorDBSnapshotType, +): SchemaDrift | null { + const localExpr = buildCombinedTypeValidateExpr(snapshotType.validate); + const remoteCreate = remoteType.schema?.typeValidate?.create?.expr ?? null; + const remoteUpdate = remoteType.schema?.typeValidate?.update?.expr ?? null; + + if (localExpr === null && remoteCreate === null && remoteUpdate === null) return null; + + if (localExpr === null) { + return { + typeName, + kind: "type_validate_mismatch", + details: `Snapshot has no record-level validators but remote has type_validate (create=${formatExpr(remoteCreate)}, update=${formatExpr(remoteUpdate)})`, + }; + } + + if (remoteCreate !== localExpr || remoteUpdate !== localExpr) { + return { + typeName, + kind: "type_validate_mismatch", + details: `Remote type_validate does not match snapshot. local=${formatExpr(localExpr)} remote.create=${formatExpr(remoteCreate)} remote.update=${formatExpr(remoteUpdate)}`, + }; + } + + return null; +} + +function formatExpr(expr: string | null): string { + return expr === null ? "(none)" : JSON.stringify(expr); +} + /** * Format schema drifts for display * @param {SchemaDrift[]} drifts - List of drifts to format diff --git a/packages/sdk/src/cli/commands/tailordb/migrate/types.ts b/packages/sdk/src/cli/commands/tailordb/migrate/types.ts index e3e551b7e..24fe97394 100644 --- a/packages/sdk/src/cli/commands/tailordb/migrate/types.ts +++ b/packages/sdk/src/cli/commands/tailordb/migrate/types.ts @@ -118,7 +118,8 @@ export type SchemaDriftKind = | "type_missing_local" | "field_missing_remote" | "field_missing_local" - | "field_mismatch"; + | "field_mismatch" + | "type_validate_mismatch"; /** * Single schema drift item diff --git a/packages/sdk/src/cli/services/tailordb/hooks-validate-bundler.ts b/packages/sdk/src/cli/services/tailordb/hooks-validate-bundler.ts index 2d2dc348b..1c333abcb 100644 --- a/packages/sdk/src/cli/services/tailordb/hooks-validate-bundler.ts +++ b/packages/sdk/src/cli/services/tailordb/hooks-validate-bundler.ts @@ -3,7 +3,7 @@ import { parseSync } from "oxc-parser"; import { join, resolve } from "pathe"; import * as rolldown from "rolldown"; import { getDistDir } from "@/cli/shared/dist-dir"; -import { stringifyFunction, tailorUserMap } from "@/parser/service/tailordb/field"; +import { SCRIPT_ARG_MAPS, stringifyFunction } from "@/parser/service/tailordb/field"; import { setPrecompiledScriptExpr } from "@/parser/service/tailordb/hooks-validate-precompiled-expr"; import { ES_BUILTINS } from "./es-builtins"; import type { TailorDBTypeRaw as TailorDBTypeSchemaOutput } from "@/types/tailordb.generated"; @@ -19,11 +19,35 @@ import type { type ScriptFunction = (...args: unknown[]) => unknown; +/** + * `record-hooks` runs at the field-level binding context: each override key + * gets its own `FieldHook` whose script invokes the record-level function and + * indexes out the key. The platform binds the record map to `_data` at that + * level, so the script must reference `_data`, not the type-level `_input`. + * + * `record-validate` is still emitted as `type_validate` at the type level, + * where the platform binds the record map to `_input`. + * + * Field-level scripts (`hooks`, `validate`) additionally receive `_value`. + */ +type ScriptKind = "hooks" | "validate" | "record-hooks" | "record-validate"; + type ScriptTarget = { fn: ScriptFunction; - kind: "hooks" | "validate"; + kind: ScriptKind; }; +function scriptInvocationArgs(kind: ScriptKind): string { + switch (kind) { + case "record-hooks": + return SCRIPT_ARG_MAPS.recordHook; + case "record-validate": + return SCRIPT_ARG_MAPS.recordValidate; + default: + return SCRIPT_ARG_MAPS.field; + } +} + /** Binding found in the source file: either an import or a top-level declaration */ export type SourceBinding = { name: string; @@ -86,9 +110,37 @@ function toScriptFunction(value: unknown): ScriptFunction | undefined { return value as unknown as ScriptFunction; } +// eslint-disable-next-line @typescript-eslint/no-unsafe-function-type +type ValidateInputEntry = Function | [Function, string]; + +function pushValidateTargets( + validators: readonly ValidateInputEntry[] | undefined, + kind: ScriptKind, + targets: ScriptTarget[], +): void { + if (!validators) return; + for (const validateInput of validators) { + const candidate = typeof validateInput === "function" ? validateInput : validateInput[0]; + const fn = toScriptFunction(candidate); + if (fn) targets.push({ fn, kind }); + } +} + function collectScriptTargets(type: TailorDBTypeSchemaOutput): ScriptTarget[] { const targets: ScriptTarget[] = []; + // Collect record-level hooks + const recordCreateHook = toScriptFunction(type.metadata.hooks?.create); + if (recordCreateHook) { + targets.push({ fn: recordCreateHook, kind: "record-hooks" }); + } + const recordUpdateHook = toScriptFunction(type.metadata.hooks?.update); + if (recordUpdateHook) { + targets.push({ fn: recordUpdateHook, kind: "record-hooks" }); + } + + pushValidateTargets(type.metadata.validate, "record-validate", targets); + const collectFieldTargets = (field: TailorDBTypeSchemaOutput["fields"][string]) => { const metadata = field.metadata; @@ -101,15 +153,7 @@ function collectScriptTargets(type: TailorDBTypeSchemaOutput): ScriptTarget[] { targets.push({ fn: updateHook, kind: "hooks" }); } - for (const validateInput of metadata.validate ?? []) { - if (typeof validateInput === "function") { - const validateFn = toScriptFunction(validateInput); - if (validateFn) targets.push({ fn: validateFn, kind: "validate" }); - } else { - const validateFn = toScriptFunction(validateInput[0]); - if (validateFn) targets.push({ fn: validateFn, kind: "validate" }); - } - } + pushValidateTargets(metadata.validate, "validate", targets); if (field.type === "nested" && field.fields) { for (const nestedField of Object.values(field.fields as TailorDBTypeSchemaOutput["fields"])) { @@ -385,13 +429,13 @@ export function resolveNeededBindings( }; } -function buildPrecompiledExpr(bundleCode: string): string { +function buildPrecompiledExpr(bundleCode: string, kind: ScriptKind): string { return ( "(() => {\n" + " const module = { exports: {} };\n" + " const exports = module.exports;\n" + `${bundleCode}\n` + - ` return module.exports.main({ value: _value, data: _data, user: ${tailorUserMap} });\n` + + ` return module.exports.main(${scriptInvocationArgs(kind)});\n` + "})()" ); } @@ -430,7 +474,7 @@ export function buildMinimalEntryFromResolved( async function bundleScriptTarget(args: { fn: ScriptFunction; - kind: "hooks" | "validate"; + kind: ScriptKind; sourceFilePath: string; sourceBindings: Map; tempDir: string; @@ -439,7 +483,7 @@ async function bundleScriptTarget(args: { }): Promise { const { fn, kind, sourceFilePath, sourceBindings, tempDir, targetIndex, tsconfig } = args; const fnSource = stringifyFunction(fn); - const inlineExpr = `(${fnSource})({ value: _value, data: _data, user: ${tailorUserMap} })`; + const inlineExpr = `(${fnSource})(${scriptInvocationArgs(kind)})`; // Check if the function has free variables that need bundling const freeVars = findUndefinedReferences(`const __fn = ${fnSource};`); @@ -486,7 +530,7 @@ async function bundleScriptTarget(args: { } as rolldown.BuildOptions); const bundledCode = buildResult.output[0].code; - return buildPrecompiledExpr(bundledCode); + return buildPrecompiledExpr(bundledCode, kind); } /** diff --git a/packages/sdk/src/cli/shared/skills-installer.ts b/packages/sdk/src/cli/shared/skills-installer.ts index aacd57d79..54e023ad3 100644 --- a/packages/sdk/src/cli/shared/skills-installer.ts +++ b/packages/sdk/src/cli/shared/skills-installer.ts @@ -33,9 +33,9 @@ function resolveSkillsSource(source: string): string { * Build CLI arguments for `skills add` with the fixed tailor-sdk skill target. * `--copy` is included so the installed skill survives `pnpm install` wiping `node_modules`. * @param options - Options controlling the generated `skills add` arguments - * @param options.source - * @param options.agent - * @param options.yes + * @param options.source - npm spec or path of the skill bundle to install + * @param options.agent - Optional agent label forwarded as `--agent` + * @param options.yes - When true, pass `--yes` to skip interactive prompts * @returns CLI arguments for `npx skills add` */ export function buildSkillsAddArgs(options: { diff --git a/packages/sdk/src/configure/services/index.ts b/packages/sdk/src/configure/services/index.ts index 037468709..c9b6a974d 100644 --- a/packages/sdk/src/configure/services/index.ts +++ b/packages/sdk/src/configure/services/index.ts @@ -1,6 +1,8 @@ export * from "./auth"; export { db, + createTable, + timestampFields, type TailorDBType, type TailorAnyDBType, type TailorDBField, diff --git a/packages/sdk/src/configure/services/resolver/descriptor.ts b/packages/sdk/src/configure/services/resolver/descriptor.ts new file mode 100644 index 000000000..ddc2d450e --- /dev/null +++ b/packages/sdk/src/configure/services/resolver/descriptor.ts @@ -0,0 +1,242 @@ +import { type AllowedValues, type AllowedValuesOutput } from "@/configure/types/field"; +import { type TailorAnyField, type TailorField, createTailorField } from "@/configure/types/type"; +import type { FieldOptions, TailorFieldType, TailorToTs } from "@/types/field-types"; +import type { InferFieldsOutput } from "@/types/helpers"; +import type { FieldValidateInput, ValidateConfig } from "@/types/validation"; + +type CommonFieldOptions = { + optional?: boolean; + array?: boolean; + description?: string; +}; + +const kindToFieldType = { + string: "string", + int: "integer", + float: "float", + bool: "boolean", + uuid: "uuid", + decimal: "decimal", + date: "date", + datetime: "datetime", + time: "time", + enum: "enum", + object: "nested", +} as const satisfies Record; + +export type KindToFieldType = typeof kindToFieldType; + +type KindToTsType = { + [K in keyof KindToFieldType as K extends "enum" | "object" + ? never + : K]: TailorToTs[KindToFieldType[K]]; +}; + +type ValidatableOptions = { + validate?: FieldValidateInput | FieldValidateInput[]; +}; + +type SimpleDescriptor = CommonFieldOptions & + ValidatableOptions & { + kind: K; + }; + +type EnumDescriptor = CommonFieldOptions & + ValidatableOptions> & { + kind: "enum"; + values: V; + typeName?: string; + }; + +type ObjectDescriptor = CommonFieldOptions & { + kind: "object"; + fields: Record; + typeName?: string; +}; + +export type ResolverFieldDescriptor = + | SimpleDescriptor<"string"> + | SimpleDescriptor<"int"> + | SimpleDescriptor<"float"> + | SimpleDescriptor<"bool"> + | SimpleDescriptor<"uuid"> + | SimpleDescriptor<"decimal"> + | SimpleDescriptor<"date"> + | SimpleDescriptor<"datetime"> + | SimpleDescriptor<"time"> + | EnumDescriptor + | ObjectDescriptor; + +export type ResolverFieldEntry = ResolverFieldDescriptor | TailorAnyField; + +// --- Type-level output inference --- + +type DescriptorBaseOutput = D extends { + kind: "enum"; + values: infer V; +} + ? V extends AllowedValues + ? AllowedValuesOutput + : string + : D extends { kind: "object"; fields: infer F } + ? F extends Record + ? InferFieldsOutput> + : Record + : D["kind"] extends keyof KindToTsType + ? KindToTsType[D["kind"]] + : unknown; + +type ApplyArrayAndOptional = D extends { array: true } + ? D extends { optional: true } + ? T[] | null + : T[] + : D extends { optional: true } + ? T | null + : T; + +export type ResolverDescriptorOutput = ApplyArrayAndOptional< + DescriptorBaseOutput, + D +>; + +type DescriptorDefined = { + type: D["kind"] extends keyof KindToFieldType ? KindToFieldType[D["kind"]] : TailorFieldType; + array: D extends { array: true } ? true : false; +}; + +export type ResolvedResolverField = E extends ResolverFieldDescriptor + ? TailorField, ResolverDescriptorOutput> + : E; + +export type ResolvedResolverFieldMap> = { + [K in keyof M]: ResolvedResolverField; +}; + +// --- Runtime conversion --- + +function isPassthroughField(entry: ResolverFieldEntry): entry is TailorAnyField { + return !("kind" in entry); +} + +export function isResolverFieldDescriptor(entry: unknown): entry is ResolverFieldDescriptor { + if (entry === null || typeof entry !== "object" || !("kind" in entry)) return false; + const kind = (entry as { kind: unknown }).kind; + return typeof kind === "string" && kind in kindToFieldType; +} + +function isValidateConfig(v: unknown): v is ValidateConfig { + return Array.isArray(v) && v.length === 2 && typeof v[1] === "string"; +} + +const COMMON_RESOLVER_KEYS = ["kind", "optional", "array", "description"] as const; +const SIMPLE_RESOLVER_KEYS = [...COMMON_RESOLVER_KEYS, "validate"] as const; + +// Allowed keys per resolver descriptor kind. Used to reject unsupported options +// (typos, legacy options) that TS structural typing would otherwise silently accept. +const KIND_ALLOWED_KEYS: Record = { + string: SIMPLE_RESOLVER_KEYS, + int: SIMPLE_RESOLVER_KEYS, + float: SIMPLE_RESOLVER_KEYS, + bool: SIMPLE_RESOLVER_KEYS, + uuid: SIMPLE_RESOLVER_KEYS, + decimal: SIMPLE_RESOLVER_KEYS, + date: SIMPLE_RESOLVER_KEYS, + datetime: SIMPLE_RESOLVER_KEYS, + time: SIMPLE_RESOLVER_KEYS, + enum: [...SIMPLE_RESOLVER_KEYS, "values", "typeName"], + object: [...COMMON_RESOLVER_KEYS, "fields", "typeName"], +}; + +function assertResolverDescriptorKeys(descriptor: ResolverFieldDescriptor, path: string): void { + const allowed = KIND_ALLOWED_KEYS[descriptor.kind]; + const unknown = Object.keys(descriptor).filter((k) => !allowed.includes(k)); + if (unknown.length === 0) return; + throw new Error( + `Resolver field "${path}" (kind "${descriptor.kind}"): unknown option(s) ${unknown + .map((k) => `"${k}"`) + .join(", ")}. Allowed: ${allowed.join(", ")}`, + ); +} + +export function resolveResolverField(entry: ResolverFieldEntry, path: string): TailorAnyField { + if (isPassthroughField(entry)) { + const cast = entry as { type?: unknown; metadata?: unknown }; + if (typeof cast.type !== "string" || typeof cast.metadata !== "object" || !cast.metadata) { + throw new Error( + "Expected a field descriptor (with `kind`) or a t.*() field instance (with `type`)", + ); + } + return entry; + } + return buildResolverField(entry, path); +} + +export function resolveResolverFieldMap( + entries: Record, + pathPrefix = "", +): Record { + let hasDescriptor = false; + const resolved: Record = {}; + for (const [key, entry] of Object.entries(entries)) { + // `"kind" in entry` cheaply distinguishes descriptors from passthrough fields + // without re-running the full kind-validity check inside `resolveResolverField`. + if ("kind" in entry) hasDescriptor = true; + resolved[key] = resolveResolverField(entry, pathPrefix ? `${pathPrefix}.${key}` : key); + } + return hasDescriptor ? resolved : (entries as Record); +} + +function buildResolverField(descriptor: ResolverFieldDescriptor, path: string): TailorAnyField { + if (!(descriptor.kind in kindToFieldType)) { + throw new Error( + `Unknown resolver field descriptor kind: "${String((descriptor as { kind: unknown }).kind)}"`, + ); + } + assertResolverDescriptorKeys(descriptor, path); + const fieldType = kindToFieldType[descriptor.kind]; + const options: FieldOptions = { + ...(descriptor.optional === true && { optional: true as const }), + ...(descriptor.array === true && { array: true as const }), + }; + + let values: AllowedValues | undefined; + if (descriptor.kind === "enum") { + if (!Array.isArray(descriptor.values) || descriptor.values.length === 0) { + throw new Error('Enum field descriptor requires a non-empty "values" array'); + } + values = descriptor.values; + } + + const nestedFields = + descriptor.kind === "object" ? resolveResolverFieldMap(descriptor.fields, path) : undefined; + + let field: TailorAnyField = createTailorField(fieldType, options, nestedFields, values); + + if (descriptor.description !== undefined) { + field = field.description(descriptor.description); + } + + if ( + (descriptor.kind === "enum" || descriptor.kind === "object") && + descriptor.typeName !== undefined + ) { + // oxlint-disable-next-line no-explicit-any -- typeName() is only present on enum/nested field interfaces + field = (field as any).typeName(descriptor.typeName); + } + + if (descriptor.kind === "object") { + return field; + } + + if (descriptor.validate !== undefined) { + if (Array.isArray(descriptor.validate) && !isValidateConfig(descriptor.validate)) { + // oxlint-disable-next-line no-explicit-any -- union of typed FieldValidateInput variants; widen to any + field = field.validate(...(descriptor.validate as any)); + } else { + // oxlint-disable-next-line no-explicit-any -- union of typed FieldValidateInput variants; widen to any + field = field.validate(descriptor.validate as any); + } + } + + return field; +} diff --git a/packages/sdk/src/configure/services/resolver/resolver.test.ts b/packages/sdk/src/configure/services/resolver/resolver.test.ts index 5688b8930..43dba4bb7 100644 --- a/packages/sdk/src/configure/services/resolver/resolver.test.ts +++ b/packages/sdk/src/configure/services/resolver/resolver.test.ts @@ -734,4 +734,563 @@ describe("createResolver", () => { expect(resolver.description).toBeUndefined(); }); }); + + describe("descriptor-based fields", () => { + test("descriptor input fields infer correct types", () => { + const resolver = createResolver({ + name: "descriptorInput", + operation: "query", + input: { + name: { kind: "string" }, + age: { kind: "int", optional: true }, + }, + output: t.bool(), + body: () => true, + }); + expect(resolver.input!.name.type).toBe("string"); + expect(resolver.input!.name.metadata.required).toBe(true); + expect(resolver.input!.age.type).toBe("integer"); + expect(resolver.input!.age.metadata.required).toBe(false); + }); + + test("descriptor output field infers correct return type", () => { + createResolver({ + name: "descriptorOutput", + operation: "query", + input: { + a: { kind: "int" }, + b: { kind: "int" }, + }, + output: { kind: "int", description: "Sum" }, + body: ({ input }) => { + expectTypeOf(input.a).toEqualTypeOf(); + return input.a + input.b; + }, + }); + }); + + test("descriptor output Record infers correct return type", () => { + createResolver({ + name: "descriptorRecordOutput", + operation: "mutation", + input: { + id: { kind: "uuid" }, + }, + output: { + success: { kind: "bool" }, + message: { kind: "string" }, + }, + body: ({ input }) => { + expectTypeOf(input.id).toEqualTypeOf(); + return { success: true, message: "done" }; + }, + }); + }); + + test("mixed fluent and descriptor fields work together", () => { + createResolver({ + name: "mixed", + operation: "query", + input: { + a: { kind: "int" }, + b: t.int(), + }, + output: t.int(), + body: ({ input }) => { + expectTypeOf(input.a).toEqualTypeOf(); + expectTypeOf(input.b).toEqualTypeOf(); + return input.a + input.b; + }, + }); + }); + + test("enum descriptor infers literal union type", () => { + const resolver = createResolver({ + name: "enumDesc", + operation: "query", + input: { + role: { kind: "enum", values: ["ADMIN", "USER"] }, + }, + output: { kind: "string" }, + body: ({ input }) => input.role, + }); + expect(resolver.input!.role.type).toBe("enum"); + expect(resolver.input!.role.metadata.allowedValues).toEqual([ + { value: "ADMIN", description: "" }, + { value: "USER", description: "" }, + ]); + }); + + test("object descriptor infers nested type", () => { + const resolver = createResolver({ + name: "objectDesc", + operation: "query", + input: { + user: { + kind: "object", + fields: { + name: { kind: "string" }, + age: { kind: "int", optional: true }, + }, + }, + }, + output: { kind: "string" }, + body: ({ input }) => input.user.name, + }); + expect(resolver.input!.user.type).toBe("nested"); + const nestedFields = resolver.input!.user.fields; + expect(nestedFields.name.type).toBe("string"); + expect(nestedFields.age.type).toBe("integer"); + expect(nestedFields.age.metadata.required).toBe(false); + }); + + test("array descriptor infers array type", () => { + createResolver({ + name: "arrayDesc", + operation: "query", + input: { + tags: { kind: "string", array: true }, + }, + output: { kind: "int" }, + body: ({ input }) => { + expectTypeOf(input.tags).toEqualTypeOf(); + return input.tags.length; + }, + }); + }); + + test("descriptor input resolves to TailorField at runtime", () => { + const resolver = createResolver({ + name: "runtimeCheck", + operation: "query", + input: { + name: { kind: "string", description: "User name" }, + count: { kind: "int" }, + }, + output: { kind: "bool" }, + body: () => true, + }); + expect(resolver.input).toBeDefined(); + expect(resolver.input!.name.type).toBe("string"); + expect(resolver.input!.name.metadata.description).toBe("User name"); + expect(resolver.input!.count.type).toBe("integer"); + expect(resolver.output.type).toBe("boolean"); + }); + + test("descriptor with validate sets metadata correctly", () => { + const validate: [({ value }: { value: number }) => boolean, string] = [ + ({ value }) => value >= 0, + "Must be non-negative", + ]; + const resolver = createResolver({ + name: "validateCheck", + operation: "query", + input: { + age: { + kind: "int", + validate, + }, + }, + output: { kind: "bool" }, + body: () => true, + }); + expect(resolver.input!.age.metadata.validate).toBeDefined(); + expect(resolver.input!.age.metadata.validate!.length).toBe(1); + }); + + test("descriptor validate accepts an array of bare predicates", () => { + const resolver = createResolver({ + name: "validateBareArray", + operation: "query", + input: { + age: { + kind: "int", + validate: [ + ({ value }: { value: number }) => value >= 0, + ({ value }: { value: number }) => value <= 150, + ], + }, + }, + output: { kind: "bool" }, + body: () => true, + }); + expect(resolver.input!.age.metadata.validate).toHaveLength(2); + }); + + test("descriptor validate accepts an array of [fn, message] tuples", () => { + const resolver = createResolver({ + name: "validateTupleArray", + operation: "query", + input: { + age: { + kind: "int", + validate: [ + [({ value }: { value: number }) => value >= 0, "Must be non-negative"], + [({ value }: { value: number }) => value <= 150, "Must be at most 150"], + ], + }, + }, + output: { kind: "bool" }, + body: () => true, + }); + expect(resolver.input!.age.metadata.validate).toHaveLength(2); + }); + + test("decimal descriptor outputs string type", () => { + createResolver({ + name: "decimalDesc", + operation: "query", + input: { + amount: { kind: "decimal" }, + }, + output: { kind: "decimal" }, + body: ({ input }) => { + expectTypeOf(input.amount).toEqualTypeOf(); + return input.amount; + }, + }); + }); + + test("all-descriptor resolver is compatible with ResolverInput", () => { + const resolver = createResolver({ + name: "allDescriptor", + operation: "query", + input: { + id: { kind: "uuid" }, + name: { kind: "string" }, + }, + output: { + found: { kind: "bool" }, + }, + body: () => ({ found: true }), + }); + expectTypeOf(resolver).toExtend(); + }); + + test("unknown kind in input throws an error", () => { + expect(() => + createResolver({ + name: "unknownKind", + operation: "query", + input: { + // @ts-expect-error testing runtime behavior with unknown kind + name: { kind: "strng" }, + }, + output: { kind: "bool" }, + body: () => true, + }), + ).toThrow('Unknown resolver field descriptor kind: "strng"'); + }); + + test("enum descriptor without values throws an error", () => { + expect(() => + createResolver({ + name: "enumNoValues", + operation: "query", + input: { + // @ts-expect-error testing runtime behavior with missing values + status: { kind: "enum" }, + }, + output: { kind: "bool" }, + body: () => true, + }), + ).toThrow('Enum field descriptor requires a non-empty "values" array'); + }); + + test("plain object without kind or type throws in input", () => { + expect(() => + createResolver({ + name: "malformed", + operation: "query", + input: { + // @ts-expect-error testing runtime behavior with malformed entry + name: { optional: true }, + }, + output: { kind: "bool" }, + body: () => true, + }), + ).toThrow("Expected a field descriptor"); + }); + + // TS structural typing silently accepts extra keys on inferred descriptors, + // so unknown keys (typos, legacy options) must be rejected at runtime. + test("rejects unknown descriptor option keys", () => { + expect(() => + createResolver({ + name: "unknownOption", + operation: "query", + input: { + name: { + kind: "string", + optinal: true, + }, + }, + output: { kind: "bool" }, + body: () => true, + }), + ).toThrow(/Resolver field "input\.name" \(kind "string"\): unknown option\(s\) "optinal"/); + }); + + test("rejects unknown keys inside nested object descriptor", () => { + expect(() => + createResolver({ + name: "unknownNested", + operation: "query", + output: { + kind: "object", + fields: { + inner: { + kind: "string", + hooks: { create: () => "x" }, + }, + }, + }, + body: () => ({ inner: "x" }), + }), + ).toThrow(/Resolver field "output\.inner" \(kind "string"\): unknown option\(s\) "hooks"/); + }); + + test("record output with a field named 'kind' is not confused with a descriptor", () => { + const resolver = createResolver({ + name: "withKindField", + operation: "query", + output: { + kind: t.string(), + name: t.string(), + }, + body: () => ({ kind: "category", name: "test" }), + }); + + expect(resolver.output.type).toBe("nested"); + }); + + describe("validate works across all validatable scalar kinds", () => { + test("string descriptor accepts a [fn, message] validate", () => { + const resolver = createResolver({ + name: "vStr", + operation: "query", + input: { + name: { + kind: "string", + validate: [({ value }) => value.length > 0, "Name required"] as [ + ({ value }: { value: string }) => boolean, + string, + ], + }, + }, + output: { kind: "bool" }, + body: () => true, + }); + const validate = resolver.input!.name.metadata.validate; + expect(validate).toBeDefined(); + expect(validate!.length).toBe(1); + }); + + test("decimal descriptor accepts a validate function", () => { + const validate: ({ value }: { value: string }) => boolean = ({ value }) => + Number(value) >= 0; + const resolver = createResolver({ + name: "vDec", + operation: "query", + input: { + amount: { + kind: "decimal", + validate, + }, + }, + output: { kind: "bool" }, + body: () => true, + }); + expect(resolver.input!.amount.metadata.validate).toBeDefined(); + }); + + test("date descriptor accepts a validate function", () => { + const validate: ({ value }: { value: string }) => boolean = ({ value }) => + typeof value === "string"; + const resolver = createResolver({ + name: "vDate", + operation: "query", + input: { + day: { + kind: "date", + validate, + }, + }, + output: { kind: "bool" }, + body: () => true, + }); + expect(resolver.input!.day.metadata.validate).toBeDefined(); + }); + + test("datetime descriptor accepts a validate function", () => { + const validate: ({ value }: { value: string | Date }) => boolean = ({ value }) => + typeof value === "string" || value instanceof Date; + const resolver = createResolver({ + name: "vDt", + operation: "query", + input: { + at: { + kind: "datetime", + validate, + }, + }, + output: { kind: "bool" }, + body: () => true, + }); + expect(resolver.input!.at.metadata.validate).toBeDefined(); + }); + + test("enum descriptor accepts a [fn, message] validate", () => { + const validate: [({ value }: { value: string }) => boolean, string] = [ + ({ value }) => value === "ADMIN" || value === "USER", + "Invalid role", + ]; + const resolver = createResolver({ + name: "vEnum", + operation: "query", + input: { + role: { + kind: "enum", + values: ["ADMIN", "USER"], + validate, + }, + }, + output: { kind: "bool" }, + body: () => true, + }); + expect(resolver.input!.role.metadata.validate).toBeDefined(); + }); + + test("scalar descriptor accepts an array of validators", () => { + const resolver = createResolver({ + name: "vMulti", + operation: "query", + input: { + age: { + kind: "int", + validate: [ + [({ value }) => value >= 0, "Must be non-negative"], + [({ value }) => value < 200, "Too large"], + ] as [({ value }: { value: number }) => boolean, string][], + }, + }, + output: { kind: "bool" }, + body: () => true, + }); + const validate = resolver.input!.age.metadata.validate; + expect(validate).toBeDefined(); + expect(validate!.length).toBe(2); + }); + }); + + describe("ObjectDescriptor combinations", () => { + test("optional object descriptor resolves with required=false", () => { + const resolver = createResolver({ + name: "objOptional", + operation: "query", + input: { + profile: { + kind: "object", + optional: true, + fields: { + name: { kind: "string" }, + }, + }, + }, + output: { kind: "bool" }, + body: () => true, + }); + expect(resolver.input!.profile.type).toBe("nested"); + expect(resolver.input!.profile.metadata.required).toBe(false); + }); + + test("array object descriptor resolves with array=true", () => { + const resolver = createResolver({ + name: "objArray", + operation: "query", + input: { + people: { + kind: "object", + array: true, + fields: { + name: { kind: "string" }, + }, + }, + }, + output: { kind: "int" }, + body: ({ input }) => input.people.length, + }); + expect(resolver.input!.people.type).toBe("nested"); + expect(resolver.input!.people.metadata.array).toBe(true); + }); + + test("object descriptor with description sets metadata.description", () => { + const resolver = createResolver({ + name: "objDesc", + operation: "query", + input: { + profile: { + kind: "object", + description: "User profile payload", + fields: { + name: { kind: "string" }, + }, + }, + }, + output: { kind: "bool" }, + body: () => true, + }); + expect(resolver.input!.profile.metadata.description).toBe("User profile payload"); + }); + + test("object descriptor with typeName sets metadata.typeName", () => { + const resolver = createResolver({ + name: "objTypeName", + operation: "query", + input: { + payload: { + kind: "object", + typeName: "ProfilePayload", + fields: { + name: { kind: "string" }, + }, + }, + }, + output: { kind: "bool" }, + body: () => true, + }); + expect(resolver.input!.payload.metadata.typeName).toBe("ProfilePayload"); + }); + + test("optional + array + description + typeName combine correctly", () => { + const resolver = createResolver({ + name: "objAll", + operation: "query", + input: { + items: { + kind: "object", + optional: true, + array: true, + description: "List of items", + typeName: "Item", + fields: { + id: { kind: "uuid" }, + name: { kind: "string" }, + }, + }, + }, + output: { kind: "bool" }, + body: () => true, + }); + const items = resolver.input!.items; + expect(items.type).toBe("nested"); + expect(items.metadata.array).toBe(true); + expect(items.metadata.required).toBe(false); + expect(items.metadata.description).toBe("List of items"); + expect(items.metadata.typeName).toBe("Item"); + expect(items.fields.id.type).toBe("uuid"); + expect(items.fields.name.type).toBe("string"); + }); + }); + }); }); diff --git a/packages/sdk/src/configure/services/resolver/resolver.ts b/packages/sdk/src/configure/services/resolver/resolver.ts index f32e681be..fd49d202a 100644 --- a/packages/sdk/src/configure/services/resolver/resolver.ts +++ b/packages/sdk/src/configure/services/resolver/resolver.ts @@ -1,15 +1,31 @@ import { t } from "@/configure/types/type"; import { brandValue } from "@/utils/brand"; +import { + type ResolverFieldEntry, + type ResolverFieldDescriptor, + type ResolvedResolverFieldMap, + type ResolverDescriptorOutput, + type KindToFieldType, + isResolverFieldDescriptor, + resolveResolverFieldMap, + resolveResolverField, +} from "./descriptor"; import type { AuthInvoker } from "@/configure/services/auth"; import type { MachineUserName } from "@/configure/types/machine-user"; import type { TailorAnyField, TailorField } from "@/configure/types/type"; import type { TailorEnv } from "@/types/env"; +import type { TailorFieldType } from "@/types/field-types"; import type { InferFieldsOutput, output } from "@/types/helpers"; import type { ResolverInput } from "@/types/resolver.generated"; import type { TailorInvoker, TailorUser } from "@/types/user"; -type Context | undefined> = { - input: Input extends Record ? InferFieldsOutput : never; +type ResolvedInput = + Input extends Record ? ResolvedResolverFieldMap : undefined; + +type Context = { + input: Input extends Record + ? InferFieldsOutput> + : never; user: TailorUser; invoker?: TailorInvoker; env: TailorEnv; @@ -17,29 +33,43 @@ type Context | undefined> = { type OutputType = O extends TailorAnyField ? output - : O extends Record - ? InferFieldsOutput - : never; + : O extends ResolverFieldDescriptor + ? ResolverDescriptorOutput + : O extends Record + ? InferFieldsOutput> + : never; /** * Normalized output type that preserves generic type information. * - If Output is already a TailorField, use it as-is + * - If Output is a descriptor, resolve it to a TailorField * - If Output is a Record of fields, wrap it as a nested TailorField */ -type NormalizedOutput> = - Output extends TailorAnyField - ? Output +type NormalizedOutput = Output extends TailorAnyField + ? Output + : Output extends ResolverFieldDescriptor + ? TailorField< + { + type: Output["kind"] extends keyof KindToFieldType + ? KindToFieldType[Output["kind"]] + : TailorFieldType; + array: Output extends { array: true } ? true : false; + }, + ResolverDescriptorOutput + > : TailorField< { type: "nested"; array: false }, - InferFieldsOutput>> + InferFieldsOutput< + ResolvedResolverFieldMap>> + > >; -type ResolverReturn< - Input extends Record | undefined, - Output extends TailorAnyField | Record, -> = Omit & +type ResolverReturn = Omit< + ResolverInput, + "input" | "output" | "body" | "authInvoker" +> & Readonly<{ - input?: Input; + input?: ResolvedInput; output: NormalizedOutput; body: (context: Context) => OutputType | Promise>; authInvoker?: AuthInvoker | MachineUserName; @@ -52,8 +82,11 @@ type ResolverReturn< * `user`, `invoker` (reflects `authInvoker` delegation), and `env`. * The return value of `body` must match the `output` type. * - * `output` accepts either a single TailorField (e.g. `t.string()`) or a - * Record of fields (e.g. `{ name: t.string(), age: t.int() }`). + * `input` and `output` fields accept either fluent API fields (e.g. `t.string()`) + * or object-literal descriptors (e.g. `{ kind: "string" }`). Both styles can be mixed. + * + * `output` accepts either a single field (fluent or descriptor), or a + * Record of fields (e.g. `{ name: t.string(), age: { kind: "int" } }`). * * `publishEvents` enables publishing execution events for this resolver. * If not specified, this is automatically set to true when an executor uses this resolver @@ -66,27 +99,35 @@ type ResolverReturn< * @example * import { createResolver, t } from "@tailor-platform/sdk"; * + * // Fluent API style * export default createResolver({ * name: "getUser", * operation: "query", * input: { * id: t.string(), * }, - * body: async ({ input, user }) => { - * const db = getDB("tailordb"); - * const result = await db.selectFrom("User").selectAll().where("id", "=", input.id).executeTakeFirst(); - * return { name: result?.name ?? "", email: result?.email ?? "" }; + * body: async ({ input }) => ({ name: "Alice" }), + * output: t.object({ name: t.string() }), + * }); + * + * // Object-literal descriptor style + * export default createResolver({ + * name: "add", + * operation: "query", + * input: { + * a: { kind: "int", description: "First number" }, + * b: { kind: "int", description: "Second number" }, * }, - * output: t.object({ - * name: t.string(), - * email: t.string(), - * }), + * body: ({ input }) => input.a + input.b, + * output: { kind: "int", description: "Sum" }, * }); */ /* @__NO_SIDE_EFFECTS__ */ export function createResolver< - Input extends Record | undefined = undefined, - Output extends TailorAnyField | Record = TailorAnyField, + Input extends Record | undefined = undefined, + Output extends TailorAnyField | ResolverFieldDescriptor | Record = + | TailorAnyField + | ResolverFieldDescriptor, >( config: Omit & Readonly<{ @@ -96,26 +137,48 @@ export function createResolver< authInvoker?: AuthInvoker | MachineUserName; }>, ): ResolverReturn { - // Check if output is already a TailorField using duck typing. - // TailorField has `type: string` (e.g., "uuid", "string"), while - // Record either lacks `type` or has TailorField as value. - const isTailorField = (obj: unknown): obj is TailorAnyField => - typeof obj === "object" && - obj !== null && - "type" in obj && - typeof (obj as { type: unknown }).type === "string"; - - const normalizedOutput = isTailorField(config.output) ? config.output : t.object(config.output); + const resolvedInput = config.input + ? resolveResolverFieldMap(config.input as Record, "input") + : undefined; + const normalizedOutput = resolveOutput(config.output); return brandValue( { ...config, + input: resolvedInput, output: normalizedOutput, } as ResolverReturn, "resolver", ); } +function isTailorField(obj: unknown): obj is TailorAnyField { + return ( + typeof obj === "object" && + obj !== null && + "type" in obj && + typeof (obj as { type: unknown }).type === "string" + ); +} + +function resolveOutput( + output: TailorAnyField | ResolverFieldDescriptor | Record, +): TailorAnyField { + if (isResolverFieldDescriptor(output)) { + return resolveResolverField(output, "output"); + } + + if (isTailorField(output)) { + return output; + } + + const resolvedFields = resolveResolverFieldMap( + output as Record, + "output", + ); + return t.object(resolvedFields); +} + // A loose config alias for userland use-cases // oxlint-disable-next-line no-explicit-any export type ResolverConfig = ReturnType>; diff --git a/packages/sdk/src/configure/services/tailordb/createTable.test.ts b/packages/sdk/src/configure/services/tailordb/createTable.test.ts new file mode 100644 index 000000000..8395ba057 --- /dev/null +++ b/packages/sdk/src/configure/services/tailordb/createTable.test.ts @@ -0,0 +1,981 @@ +import { describe, it, expectTypeOf, expect } from "vitest"; +import { parseTypes } from "@/parser/service/tailordb"; +import { toSchemaOutputs } from "@/utils/test/internal"; +import { createTable, timestampFields } from "./createTable"; +import { unsafeAllowAllGqlPermission } from "./permission"; +import { db } from "./schema"; +import type { output } from "@/types/helpers"; + +describe("createTable basic field type tests", () => { + it("string field outputs string type correctly", () => { + const result = createTable("Test", { + name: { kind: "string" }, + }); + expectTypeOf>().toEqualTypeOf<{ + id: string; + name: string; + }>(); + }); + + it("int field outputs number type correctly", () => { + const result = createTable("Test", { + age: { kind: "int" }, + }); + expectTypeOf>().toEqualTypeOf<{ + id: string; + age: number; + }>(); + }); + + it("bool field outputs boolean type correctly", () => { + const result = createTable("Test", { + active: { kind: "bool" }, + }); + expectTypeOf>().toEqualTypeOf<{ + id: string; + active: boolean; + }>(); + }); + + it("float field outputs number type correctly", () => { + const result = createTable("Test", { + price: { kind: "float" }, + }); + expectTypeOf>().toEqualTypeOf<{ + id: string; + price: number; + }>(); + }); + + it("uuid field outputs string type correctly", () => { + const result = createTable("Test", { + ref: { kind: "uuid" }, + }); + expectTypeOf>().toEqualTypeOf<{ + id: string; + ref: string; + }>(); + }); + + it("date field outputs string type correctly", () => { + const result = createTable("Test", { + birthDate: { kind: "date" }, + }); + expectTypeOf>().toEqualTypeOf<{ + id: string; + birthDate: string; + }>(); + }); + + it("datetime field outputs string | Date type correctly", () => { + const result = createTable("Test", { + timestamp: { kind: "datetime" }, + }); + expectTypeOf>().toMatchObjectType<{ + timestamp: string | Date; + }>(); + }); + + it("time field outputs string type correctly", () => { + const result = createTable("Test", { + openingTime: { kind: "time" }, + }); + expectTypeOf>().toEqualTypeOf<{ + id: string; + openingTime: string; + }>(); + }); + + it("decimal field outputs string type correctly", () => { + const result = createTable("Test", { + amount: { kind: "decimal" }, + }); + expectTypeOf>().toEqualTypeOf<{ + id: string; + amount: string; + }>(); + }); +}); + +describe("createTable optional and array tests", () => { + it("optional generates nullable type", () => { + const result = createTable("Test", { + description: { kind: "string", optional: true }, + }); + expectTypeOf>().toEqualTypeOf<{ + id: string; + description?: string | null; + }>(); + }); + + it("array generates array type", () => { + const result = createTable("Test", { + tags: { kind: "string", array: true }, + }); + expectTypeOf>().toEqualTypeOf<{ + id: string; + tags: string[]; + }>(); + }); + + it("optional array works correctly", () => { + const result = createTable("Test", { + items: { kind: "string", optional: true, array: true }, + }); + expectTypeOf>().toEqualTypeOf<{ + id: string; + items?: string[] | null; + }>(); + }); +}); + +describe("createTable enum tests", () => { + it("enum literal types are inferred", () => { + const result = createTable("Test", { + role: { kind: "enum", values: ["MANAGER", "STAFF"] }, + }); + expectTypeOf>().toEqualTypeOf<{ + id: string; + role: "MANAGER" | "STAFF"; + }>(); + }); + + it("optional enum works correctly", () => { + const result = createTable("Test", { + priority: { kind: "enum", values: ["high", "medium", "low"], optional: true }, + }); + expectTypeOf>().toEqualTypeOf<{ + id: string; + priority?: "high" | "medium" | "low" | null; + }>(); + }); + + it("enum metadata has correct allowedValues", () => { + const result = createTable("Test", { + status: { kind: "enum", values: ["active", "inactive"] }, + }); + expect(result.fields.status.metadata.allowedValues).toEqual([ + { value: "active", description: "" }, + { value: "inactive", description: "" }, + ]); + }); + + it("enum values support {value, description} entries", () => { + const result = createTable("Test", { + status: { + kind: "enum", + values: [ + { value: "active", description: "Active record" }, + { value: "inactive", description: "Archived record" }, + ], + }, + }); + expect(result.fields.status.metadata.allowedValues).toEqual([ + { value: "active", description: "Active record" }, + { value: "inactive", description: "Archived record" }, + ]); + }); +}); + +describe("createTable runtime metadata tests", () => { + it("unique sets metadata correctly", () => { + const result = createTable("Test", { + email: { kind: "string", unique: true }, + }); + expect(result.fields.email.metadata.unique).toBe(true); + expect(result.fields.email.metadata.index).toBe(true); + }); + + it("index sets metadata correctly", () => { + const result = createTable("Test", { + name: { kind: "string", index: true }, + }); + expect(result.fields.name.metadata.index).toBe(true); + expect(result.fields.name.metadata.unique).toBeUndefined(); + }); + + it("vector sets metadata correctly", () => { + const result = createTable("Test", { + embedding: { kind: "string", vector: true }, + }); + expect(result.fields.embedding.metadata.vector).toBe(true); + }); + + it("serial sets metadata correctly", () => { + const result = createTable("Test", { + code: { kind: "string", serial: { start: 1, format: "INV-%05d" } }, + }); + expect(result.fields.code.metadata.serial).toEqual({ + start: 1, + format: "INV-%05d", + }); + }); + + it("description sets metadata correctly", () => { + const result = createTable("Test", { + name: { kind: "string", description: "The user's name" }, + }); + expect(result.fields.name.metadata.description).toBe("The user's name"); + }); + + it("decimal scale sets metadata correctly", () => { + const result = createTable("Test", { + amount: { kind: "decimal", scale: 4 }, + }); + expect(result.fields.amount.metadata.scale).toBe(4); + }); + + it("decimal scale rejects out-of-range values", () => { + expect(() => createTable("Test", { amount: { kind: "decimal", scale: -1 } })).toThrow( + "scale must be an integer between 0 and 12", + ); + expect(() => createTable("Test", { amount: { kind: "decimal", scale: 13 } })).toThrow( + "scale must be an integer between 0 and 12", + ); + expect(() => createTable("Test", { amount: { kind: "decimal", scale: 1.5 } })).toThrow( + "scale must be an integer between 0 and 12", + ); + }); + + it("decimal scale accepts boundary values 0 and 12", () => { + const low = createTable("Test", { amount: { kind: "decimal", scale: 0 } }); + expect(low.fields.amount.metadata.scale).toBe(0); + + const high = createTable("Test", { amount: { kind: "decimal", scale: 12 } }); + expect(high.fields.amount.metadata.scale).toBe(12); + }); +}); + +describe("createTable relation tests", () => { + const User = db.type("User", { + name: db.string(), + }); + + it("n-1 relation sets rawRelation and index", () => { + const result = createTable("Test", { + userId: { + kind: "uuid", + relation: { + type: "n-1", + toward: { type: User }, + }, + }, + }); + expect(result.fields.userId.rawRelation).toBeDefined(); + expect(result.fields.userId.rawRelation!.type).toBe("n-1"); + expect(result.fields.userId.metadata.index).toBe(true); + expect(result.fields.userId.metadata.unique).toBeUndefined(); + }); + + it("oneToOne relation sets rawRelation, index, and unique", () => { + const result = createTable("Test", { + userId: { + kind: "uuid", + relation: { + type: "oneToOne", + toward: { type: User }, + }, + }, + }); + expect(result.fields.userId.rawRelation).toBeDefined(); + expect(result.fields.userId.rawRelation!.type).toBe("oneToOne"); + expect(result.fields.userId.metadata.index).toBe(true); + expect(result.fields.userId.metadata.unique).toBe(true); + }); + + it("self-referencing relation works", () => { + const result = createTable("Test", { + name: { kind: "string" }, + parentId: { + kind: "uuid", + optional: true, + relation: { + type: "n-1", + toward: { type: "self" as const }, + }, + }, + }); + expect(result.fields.parentId.rawRelation).toBeDefined(); + }); + + it("relation.toward.as is preserved on the rawRelation", () => { + const User = createTable("User", { name: { kind: "string" } }); + const result = createTable("Test", { + userId: { + kind: "uuid", + relation: { + type: "n-1", + toward: { type: User, as: "owner" }, + }, + }, + }); + expect(result.fields.userId.rawRelation?.toward.as).toBe("owner"); + }); + + it("relation.backward is preserved on the rawRelation", () => { + const User = createTable("User", { name: { kind: "string" } }); + const result = createTable("Test", { + userId: { + kind: "uuid", + relation: { + type: "n-1", + toward: { type: User }, + backward: "tests", + }, + }, + }); + expect(result.fields.userId.rawRelation?.backward).toBe("tests"); + }); +}); + +describe("createTable keyOnly relation", () => { + it("keyOnly relation sets rawRelation and index", () => { + const Target = createTable("Target", { name: { kind: "string" } }); + const result = createTable("Test", { + targetId: { + kind: "uuid", + relation: { + type: "keyOnly", + toward: { type: Target }, + }, + }, + }); + expect(result.fields.targetId.rawRelation).toBeDefined(); + expect(result.fields.targetId.rawRelation!.type).toBe("keyOnly"); + expect(result.fields.targetId.metadata.index).toBe(true); + expect(result.fields.targetId.metadata.unique).toBeUndefined(); + }); +}); + +describe("createTable type-safe options", () => { + it("permission accepts record operands typed to the type's fields", () => { + const result = createTable( + "Employee", + { + name: { kind: "string" }, + ownerId: { kind: "uuid" }, + }, + { + permission: { + create: [{ conditions: [[{ user: "_loggedIn" }, "=", true]], permit: true }], + read: [{ conditions: [[{ record: "name" }, "=", "admin"]], permit: true }], + update: [{ conditions: [[{ newRecord: "ownerId" }, "=", { user: "id" }]], permit: true }], + delete: [{ conditions: [[{ record: "ownerId" }, "=", { user: "id" }]], permit: true }], + }, + }, + ); + expect(result.metadata.permissions).toBeDefined(); + }); + + it("indexes validates field names against the type's fields", () => { + const result = createTable( + "Employee", + { + name: { kind: "string" }, + department: { kind: "string" }, + }, + { + indexes: [{ fields: ["name", "department"], unique: true }], + }, + ); + expect(result.metadata.indexes).toBeDefined(); + }); + + it("files accepts keys that do not collide with field names", () => { + const result = createTable( + "Employee", + { name: { kind: "string" } }, + { files: { avatar: "image/png" } }, + ); + expect(result.metadata.files).toBeDefined(); + }); +}); + +describe("createTable array field guards", () => { + it("array fields do not get index or unique metadata", () => { + // Runtime guard: buildField skips index/unique for array fields + const result = createTable("Test", { + tags: { kind: "string", array: true }, + }); + expect(result.fields.tags.metadata.index).toBeUndefined(); + expect(result.fields.tags.metadata.unique).toBeUndefined(); + }); +}); + +describe("createTable nested object guards", () => { + it("nested object descriptor inside object descriptor causes type error", () => { + createTable("Test", { + address: { + kind: "object", + fields: { + street: { kind: "string" }, + // @ts-expect-error Nested object inside object is not allowed + location: { + kind: "object", + fields: { lat: { kind: "float" }, lng: { kind: "float" } }, + }, + }, + }, + }); + }); + + it("nested db.object() inside object descriptor causes type error", () => { + createTable("Test", { + address: { + kind: "object", + fields: { + street: { kind: "string" }, + // @ts-expect-error Nested db.object() inside object descriptor is not allowed + location: db.object({ lat: db.float(), lng: db.float() }), + }, + }, + }); + }); + + it("flat object descriptor is allowed", () => { + const result = createTable("Test", { + address: { + kind: "object", + fields: { + street: { kind: "string" }, + city: { kind: "string" }, + }, + }, + }); + expect(result.fields.address.type).toBe("nested"); + }); +}); + +describe("createTable plugins option", () => { + it("plugins are set on the type via options", () => { + const result = createTable( + "Test", + { name: { kind: "string" } }, + { + plugins: [{ pluginId: "test-plugin", config: { enabled: true } }], + }, + ); + expect(result.plugins).toEqual([{ pluginId: "test-plugin", config: { enabled: true } }]); + }); + + it("multiple plugins are set in order", () => { + const result = createTable( + "Test", + { name: { kind: "string" } }, + { + plugins: [ + { pluginId: "plugin-a", config: { a: 1 } }, + { pluginId: "plugin-b", config: { b: 2 } }, + ], + }, + ); + expect(result.plugins).toEqual([ + { pluginId: "plugin-a", config: { a: 1 } }, + { pluginId: "plugin-b", config: { b: 2 } }, + ]); + }); +}); + +describe("createTable relation key validation", () => { + it("invalid relation key against target type causes type error", () => { + const Target = createTable("Target", { name: { kind: "string" } }); + createTable("Test", { + // @ts-expect-error 'nonExistent' does not exist on Target fields + targetId: { + kind: "uuid", + relation: { + type: "n-1", + toward: { type: Target, key: "nonExistent" }, + }, + }, + }); + }); + + it("valid relation key matching target field name is accepted", () => { + const Target = createTable("Target", { name: { kind: "string" } }); + const result = createTable("Test", { + targetId: { + kind: "uuid", + relation: { + type: "n-1", + toward: { type: Target, key: "name" }, + }, + }, + }); + expect(result.fields.targetId.rawRelation).toBeDefined(); + }); + + it("explicit 'id' relation key is always accepted for target types", () => { + const Target = createTable("Target", { name: { kind: "string" } }); + const result = createTable("Test", { + targetId: { + kind: "uuid", + relation: { + type: "n-1", + toward: { type: Target, key: "id" }, + }, + }, + }); + expect(result.fields.targetId.rawRelation!.toward.key).toBe("id"); + }); + + it("explicit 'id' relation key is always accepted for self-references", () => { + const result = createTable("Test", { + parentId: { + kind: "uuid", + optional: true, + relation: { + type: "n-1", + toward: { type: "self" as const, key: "id" }, + }, + }, + }); + expect(result.fields.parentId.rawRelation!.toward.key).toBe("id"); + }); + + it("invalid self-referencing relation key causes type error", () => { + createTable("Test", { + // @ts-expect-error 'nonExistent' does not exist on own fields + parentId: { + kind: "uuid", + optional: true, + relation: { + type: "n-1", + toward: { type: "self" as const, key: "nonExistent" }, + }, + }, + }); + }); + + it("valid self-referencing relation key is accepted", () => { + const result = createTable("Test", { + name: { kind: "string" }, + parentId: { + kind: "uuid", + optional: true, + relation: { + type: "n-1", + toward: { type: "self" as const, key: "name" }, + }, + }, + }); + expect(result.fields.parentId.rawRelation).toBeDefined(); + }); + + it("relation without key is accepted", () => { + const Target = createTable("Target", { name: { kind: "string" } }); + const result = createTable("Test", { + targetId: { + kind: "uuid", + relation: { + type: "n-1", + toward: { type: Target }, + }, + }, + }); + expect(result.fields.targetId.rawRelation).toBeDefined(); + }); +}); + +describe("createTable array+vector/serial guards", () => { + it("array + vector causes type error", () => { + createTable("Test", { + // @ts-expect-error array and vector are incompatible + tags: { kind: "string", array: true, vector: true }, + }); + }); + + it("array + serial causes type error", () => { + createTable("Test", { + // @ts-expect-error array and serial are incompatible + codes: { kind: "string", array: true, serial: { start: 1 } }, + }); + }); + + it("non-array vector is accepted", () => { + const result = createTable("Test", { + embedding: { kind: "string", vector: true }, + }); + expect(result.fields.embedding.metadata.vector).toBe(true); + }); + + it("non-array serial is accepted", () => { + const result = createTable("Test", { + code: { kind: "string", serial: { start: 1 } }, + }); + expect(result.fields.code.metadata.serial).toEqual({ start: 1 }); + }); +}); + +describe("createTable unique on many-to-one relation guard", () => { + it("unique: true on n-1 relation causes type error", () => { + const Target = createTable("Target", { name: { kind: "string" } }); + createTable("Test", { + // @ts-expect-error unique is not allowed on n-1 relations + targetId: { + kind: "uuid", + unique: true, + relation: { + type: "n-1", + toward: { type: Target }, + }, + }, + }); + }); + + it("unique: true on manyToOne relation causes type error", () => { + const Target = createTable("Target", { name: { kind: "string" } }); + createTable("Test", { + // @ts-expect-error unique is not allowed on manyToOne relations + targetId: { + kind: "uuid", + unique: true, + relation: { + type: "manyToOne", + toward: { type: Target }, + }, + }, + }); + }); + + it("unique: true on oneToOne relation is accepted", () => { + const Target = createTable("Target", { name: { kind: "string" } }); + const result = createTable("Test", { + targetId: { + kind: "uuid", + unique: true, + relation: { + type: "oneToOne", + toward: { type: Target }, + }, + }, + }); + expect(result.fields.targetId.metadata.unique).toBe(true); + expect(result.fields.targetId.metadata.index).toBe(true); + }); + + it("n-1 relation without unique sets index only", () => { + const Target = createTable("Target", { name: { kind: "string" } }); + const result = createTable("Test", { + targetId: { + kind: "uuid", + relation: { + type: "n-1", + toward: { type: Target }, + }, + }, + }); + expect(result.fields.targetId.metadata.index).toBe(true); + expect(result.fields.targetId.metadata.unique).toBeUndefined(); + }); +}); + +describe("createTable array relation index guard", () => { + it("array relation does not set index or unique metadata", () => { + const Target = createTable("Target", { name: { kind: "string" } }); + const result = createTable("Test", { + targetIds: { + kind: "uuid", + array: true, + relation: { + type: "n-1", + toward: { type: Target }, + }, + }, + }); + expect(result.fields.targetIds.rawRelation).toBeDefined(); + expect(result.fields.targetIds.metadata.index).toBeUndefined(); + expect(result.fields.targetIds.metadata.unique).toBeUndefined(); + }); + + it("array oneToOne relation does not set index or unique metadata", () => { + const Target = createTable("Target", { name: { kind: "string" } }); + const result = createTable("Test", { + targetIds: { + kind: "uuid", + array: true, + relation: { + type: "oneToOne", + toward: { type: Target }, + }, + }, + }); + expect(result.fields.targetIds.rawRelation).toBeDefined(); + expect(result.fields.targetIds.metadata.index).toBeUndefined(); + expect(result.fields.targetIds.metadata.unique).toBeUndefined(); + }); +}); + +describe("createTable id field guard", () => { + it("defining id field causes type error", () => { + createTable("Test", { + // @ts-expect-error id is a system field and cannot be redefined + id: { kind: "uuid" }, + name: { kind: "string" }, + }); + }); +}); + +describe("createTable unknown descriptor kind", () => { + it("throws on unknown kind value", () => { + expect(() => + createTable("Test", { + // @ts-expect-error testing runtime behavior with unknown kind + name: { kind: "strng" }, + }), + ).toThrow('Unknown field descriptor kind: "strng"'); + }); + + it("throws on enum descriptor without values", () => { + expect(() => + createTable("Test", { + // @ts-expect-error testing runtime behavior with missing values + status: { kind: "enum" }, + }), + ).toThrow('Enum field descriptor requires a non-empty "values" array'); + }); + + it("throws on plain object without kind or type", () => { + expect(() => + createTable("Test", { + // @ts-expect-error testing runtime behavior with malformed entry + name: { optional: true }, + }), + ).toThrow("Expected a field descriptor (with `kind`) or a db.*() field instance (with `type`)"); + }); +}); + +describe("createTable unknown descriptor options", () => { + // TS structural typing silently accepts extra keys on inferred descriptors + // (no excess-property check fires once D is inferred from the literal), + // so these unknown keys must be rejected at runtime to prevent silently + // ignored hooks/validate/typos. + it("rejects field-level validate", () => { + expect(() => + createTable("Test", { + name: { + kind: "string", + validate: () => true, + }, + }), + ).toThrow(/Field "name" \(kind "string"\): unknown option\(s\) "validate"/); + }); + + it("rejects field-level hooks", () => { + expect(() => + createTable("Test", { + name: { + kind: "string", + hooks: { create: () => "x" }, + }, + }), + ).toThrow(/Field "name" \(kind "string"\): unknown option\(s\) "hooks"/); + }); + + it("rejects typos in option keys", () => { + expect(() => + createTable("Test", { + name: { + kind: "string", + uniqe: true, + }, + }), + ).toThrow(/Field "name" \(kind "string"\): unknown option\(s\) "uniqe"/); + }); + + it("rejects unique/index on object descriptors", () => { + expect(() => + createTable("Test", { + meta: { + kind: "object", + fields: { foo: { kind: "string" } }, + unique: true, + }, + }), + ).toThrow(/Field "meta" \(kind "object"\): unknown option\(s\) "unique"/); + }); + + it("reports the nested path when the offending key is inside an object descriptor", () => { + expect(() => + createTable("Test", { + meta: { + kind: "object", + fields: { + inner: { + kind: "string", + validate: () => true, + }, + }, + }, + }), + ).toThrow(/Field "meta\.inner" \(kind "string"\): unknown option\(s\) "validate"/); + }); +}); + +describe("createTable mixed fluent and descriptor fields", () => { + it("accepts both db.field() and descriptor in the same type", () => { + const result = createTable("Test", { + name: db.string(), + email: { kind: "string", unique: true }, + }); + expectTypeOf>().toEqualTypeOf<{ + id: string; + name: string; + email: string; + }>(); + expect(result.fields.email.metadata.unique).toBe(true); + }); +}); + +describe("timestampFields", () => { + it("returns createdAt and updatedAt descriptors", () => { + const result = createTable("Test", { + name: { kind: "string" }, + ...timestampFields(), + }); + expect(result.fields.createdAt).toBeDefined(); + expect(result.fields.updatedAt).toBeDefined(); + expect(result.fields.createdAt.metadata.required).toBe(true); + expect(result.fields.updatedAt.metadata.required).toBe(false); + }); +}); + +describe("createTable type-level options", () => { + it("pluralForm via options sets settings.pluralForm", () => { + const result = createTable("Person", { name: { kind: "string" } }, { pluralForm: "People" }); + expect(result.metadata.settings).toEqual({ pluralForm: "People" }); + }); + + it("pluralForm via tuple overload sets settings.pluralForm", () => { + const result = createTable(["Person", "People"], { name: { kind: "string" } }); + expect(result.metadata.settings).toEqual({ pluralForm: "People" }); + }); + + it("rejects pluralForm specified in both the name tuple and options", () => { + expect(() => + createTable(["Person", "People"], { name: { kind: "string" } }, { pluralForm: "Folks" }), + ).toThrow(/pluralForm is specified twice/); + }); + + it("type-level description sets metadata.description", () => { + const result = createTable( + "Employee", + { name: { kind: "string" } }, + { description: "Company employee" }, + ); + expect(result.metadata.description).toBe("Company employee"); + }); + + it("features sets metadata.settings", () => { + const result = createTable( + "Order", + { total: { kind: "int" } }, + { features: { aggregation: true } }, + ); + expect(result.metadata.settings).toEqual({ aggregation: true }); + }); + + it("gqlPermission sets metadata.permissions.gql", () => { + const result = createTable( + "Secret", + { value: { kind: "string" } }, + { gqlPermission: unsafeAllowAllGqlPermission }, + ); + expect(result.metadata.permissions.gql).toBeDefined(); + }); +}); + +describe("createTable record-level hooks/validate options", () => { + it("options.hooks accepts record-level create/update with full data typing", () => { + const result = createTable( + "Test", + { + name: { kind: "string" }, + score: { kind: "int" }, + }, + { + hooks: { + create: ({ data }) => { + expectTypeOf(data).toEqualTypeOf< + Readonly<{ id: string; name: string; score: number }> + >(); + return { score: data.score + 1 }; + }, + update: ({ data }) => ({ score: data.score + 1 }), + }, + }, + ); + expect(result.metadata.hooks).toBeDefined(); + expect(result.metadata.hooks?.create).toBeDefined(); + expect(result.metadata.hooks?.update).toBeDefined(); + }); + + it("options.validate accepts single function", () => { + const result = createTable( + "Test", + { name: { kind: "string" } }, + { + validate: ({ data }) => data.name.length > 0, + }, + ); + expect(result.metadata.validate).toHaveLength(1); + }); + + it("options.validate accepts single [fn, message] tuple", () => { + const result = createTable( + "Test", + { name: { kind: "string" } }, + { + validate: [({ data }) => data.name.length > 0, "Name must not be empty"], + }, + ); + expect(result.metadata.validate).toHaveLength(1); + }); + + it("options.validate accepts mixed array of fns and tuples", () => { + const result = createTable( + "Test", + { + name: { kind: "string" }, + age: { kind: "int" }, + }, + { + validate: [ + ({ data }) => data.name.length > 0, + [({ data }) => data.age >= 0, "Age must be non-negative"], + ], + }, + ); + expect(result.metadata.validate).toHaveLength(2); + }); + + it("record-level hooks expand into per-field FieldHook entries after parseTypes", () => { + const type = createTable( + "Order", + { + name: { kind: "string" }, + score: { kind: "int" }, + ...timestampFields(), + }, + { + hooks: { + create: () => ({ score: 0, createdAt: new Date() }), + update: ({ data }) => ({ score: data.score + 1, updatedAt: new Date() }), + }, + }, + ); + + const types = parseTypes(toSchemaOutputs({ Order: type }), "test", {}); + const parsed = types.Order; + + expect(parsed.fields.score.config.hooks?.create?.expr).toContain("score"); + expect(parsed.fields.score.config.hooks?.update?.expr).toContain("score"); + expect(parsed.fields.createdAt.config.hooks?.create?.expr).toContain("createdAt"); + expect(parsed.fields.createdAt.config.hooks?.update).toBeUndefined(); + expect(parsed.fields.updatedAt.config.hooks?.update?.expr).toContain("updatedAt"); + expect(parsed.fields.updatedAt.config.hooks?.create).toBeUndefined(); + // Fields not present in any hook return literal must stay free of hooks. + expect(parsed.fields.name.config.hooks?.create).toBeUndefined(); + expect(parsed.fields.name.config.hooks?.update).toBeUndefined(); + }); +}); diff --git a/packages/sdk/src/configure/services/tailordb/createTable.ts b/packages/sdk/src/configure/services/tailordb/createTable.ts new file mode 100644 index 000000000..11ca8da58 --- /dev/null +++ b/packages/sdk/src/configure/services/tailordb/createTable.ts @@ -0,0 +1,542 @@ +import { type AllowedValues, type AllowedValuesOutput } from "@/configure/types/field"; +import { + type TailorAnyDBField, + type TailorAnyDBType, + type TailorDBField, + type TailorDBType, + assertValidDecimalScale, + createTailorDBField, + createTailorDBType, +} from "./schema"; +import type { TailorTypeGqlPermission, TailorTypePermission } from "./permission"; +import type { RecordHook, TypeFeatures } from "./types"; +import type { TailorFieldType, TailorToTs } from "@/types/field-types"; +import type { InferFieldsOutput, output } from "@/types/helpers"; +import type { PluginAttachment } from "@/types/plugin"; +import type { IndexDef, RelationType, SerialConfig } from "@/types/tailordb"; +import type { InferredAttributeMap } from "@/types/user"; +import type { RecordValidators } from "@/types/validation"; + +type CommonFieldOptions = { + optional?: boolean; + description?: string; + generated?: boolean; +}; + +const kindToFieldType = { + string: "string", + int: "integer", + float: "float", + bool: "boolean", + uuid: "uuid", + decimal: "decimal", + date: "date", + datetime: "datetime", + time: "time", + enum: "enum", + object: "nested", +} as const satisfies Record; + +type KindToFieldType = typeof kindToFieldType; + +type KindToTsType = { + [K in keyof KindToFieldType as K extends "enum" | "object" + ? never + : K]: TailorToTs[KindToFieldType[K]]; +}; + +// Field-level options. +// NOTE: field-level `hooks` and `validate` have been removed. Configure them at +// record level via the third `options` argument of `createTable` instead. +type FieldOptions = { + unique?: boolean; + index?: boolean; +}; + +type StringDescriptor = CommonFieldOptions & + FieldOptions & { + kind: "string"; + array?: boolean; + vector?: boolean; + serial?: SerialConfig<"string">; + }; + +type IntDescriptor = CommonFieldOptions & + FieldOptions & { + kind: "int"; + array?: boolean; + serial?: SerialConfig<"integer">; + }; + +type SimpleDescriptor = CommonFieldOptions & + FieldOptions & { + kind: K; + array?: boolean; + }; + +type FloatDescriptor = SimpleDescriptor<"float">; +type BoolDescriptor = SimpleDescriptor<"bool">; +type DateDescriptor = SimpleDescriptor<"date">; +type DatetimeDescriptor = SimpleDescriptor<"datetime">; +type TimeDescriptor = SimpleDescriptor<"time">; +type DecimalDescriptor = CommonFieldOptions & + FieldOptions & { + kind: "decimal"; + array?: boolean; + scale?: number; + }; + +type UuidDescriptor = CommonFieldOptions & + FieldOptions & { + kind: "uuid"; + array?: boolean; + relation?: { + type: RelationType; + toward: { + type: TailorAnyDBType | "self"; + as?: string; + // Typed as plain `string` here (not `keyof T["fields"]`); validated + // at the createTable call site via `ValidateRelationKeys`. + key?: string; + }; + backward?: string; + }; + }; + +type EnumDescriptor = CommonFieldOptions & + FieldOptions & { + kind: "enum"; + array?: boolean; + values: V; + typeName?: string; + }; + +// Nested object sub-fields bypass top-level constraint types (RejectArrayCombinations, etc.) +// because recursive mapped-type constraints would add significant complexity. This is a shared gap +// with the fluent API (db.object() sub-fields are also unconstrained). Invalid nested combinations +// are caught at deployment time by the platform. +type ObjectDescriptor = CommonFieldOptions & { + kind: "object"; + array?: boolean; + fields: Record; + typeName?: string; +}; + +type FieldDescriptor = + | StringDescriptor + | IntDescriptor + | FloatDescriptor + | BoolDescriptor + | DateDescriptor + | DatetimeDescriptor + | TimeDescriptor + | DecimalDescriptor + | UuidDescriptor + | EnumDescriptor + | ObjectDescriptor; + +type FieldEntry = FieldDescriptor | TailorAnyDBField; + +type DescriptorBaseOutput = D extends { kind: "enum"; values: infer V } + ? V extends AllowedValues + ? AllowedValuesOutput + : string + : D extends { kind: "object"; fields: infer F } + ? F extends Record + ? InferFieldsOutput> + : Record + : D["kind"] extends keyof KindToTsType + ? KindToTsType[D["kind"]] + : unknown; + +type ApplyArrayAndOptional = D extends { array: true } + ? D extends { optional: true } + ? T[] | null + : T[] + : D extends { optional: true } + ? T | null + : T; + +type DescriptorOutput = ApplyArrayAndOptional< + DescriptorBaseOutput, + D +>; + +type DescriptorDefined = { + type: D["kind"] extends keyof KindToFieldType ? KindToFieldType[D["kind"]] : TailorFieldType; + array: D extends { array: true } ? true : false; +} & (D extends { unique: true } + ? { unique: true; index: true } + : D extends { index: true } + ? { index: true } + : unknown) & + (D extends { serial: object } + ? { serial: true; hooks: { create: false; update: false } } + : unknown) & + (D extends { vector: true } ? { vector: true } : unknown) & + (D extends { kind: "uuid"; relation: object } + ? D extends { array: true } + ? { relation: true } + : D extends { relation: { type: "oneToOne" | "1-1" } } + ? { relation: true; unique: true; index: true } + : { relation: true; index: true } + : unknown); + +type ResolvedField = E extends FieldDescriptor + ? TailorDBField, DescriptorOutput> + : E; + +// oxlint-disable-next-line no-explicit-any +type ResolvedFieldMap> = { + [K in keyof M]: ResolvedField; +}; + +// Rejects nested objects inside object descriptors (matching ExcludeNestedDBFields in fluent API). +type RejectNestedSubFields> = { + [K in keyof F]: F[K] extends + | { kind: "object" } + // oxlint-disable-next-line no-explicit-any -- loose match for nested TailorDBField + | TailorDBField<{ type: "nested"; array: boolean }, any> + ? never + : F[K]; +}; + +// All descriptor-level validations in a single mapped type to minimize type +// evaluation passes (avoids combinatorial explosion with union descriptors). +type ValidatedDescriptors> = D & { + [K in keyof D]: D[K] extends // 1. RejectArrayCombinations: array + index/unique/vector/serial + | { array: true; unique: true } + | { array: true; index: true } + | { array: true; vector: true } + | { array: true; serial: object } + ? never + : // 2. RejectUniqueOnManyRelation: unique only allowed on oneToOne uuid relations + D[K] extends { kind: "uuid"; unique: true; relation: { type: infer T } } + ? T extends "oneToOne" | "1-1" + ? D[K] + : never + : // 3. RejectNestedInObject: no nested objects inside object fields + D[K] extends { kind: "object"; fields: infer F } + ? F extends Record + ? D[K] & { fields: RejectNestedSubFields } + : D[K] + : // 4. ValidateRelationKeys: relation key must exist in target type + D[K] extends { kind: "uuid"; relation: { toward: { type: infer T; key: infer Key } } } + ? Key extends string + ? T extends TailorAnyDBType + ? Key extends (keyof T["fields"] & string) | "id" + ? D[K] + : never + : T extends "self" + ? Key extends (keyof D & string) | "id" + ? D[K] + : never + : D[K] + : D[K] + : D[K]; +}; + +type CreateTableOptions< + FieldNames extends string = string, + // oxlint-disable-next-line no-explicit-any + Fields extends Record = any, +> = { + description?: string; + pluralForm?: string; + features?: Omit; + indexes?: IndexDef<{ fields: Record }>[]; + files?: Record & Partial>; + permission?: TailorTypePermission>>; + gqlPermission?: TailorTypeGqlPermission; + plugins?: PluginAttachment[]; + /** + * Record-level create/update hooks. Each callback receives `{ data, user }` + * (the entire record snapshot) and returns an object with only the fields + * to override; omitted fields keep their incoming values. + */ + hooks?: RecordHook>; + /** + * Record-level validators. Each callback receives `{ data, user }` and must + * return `true` for a valid record. Use the tuple form `[fn, message]` for + * diagnosable error messages. + */ + validate?: RecordValidators>; +}; + +function isPassthroughField(entry: FieldEntry): entry is TailorAnyDBField { + // All FieldDescriptor variants have `kind`; TailorAnyDBField does not. + return !("kind" in entry); +} + +const COMMON_DESCRIPTOR_KEYS = ["kind", "optional", "array", "description", "generated"] as const; +const INDEXABLE_DESCRIPTOR_KEYS = [...COMMON_DESCRIPTOR_KEYS, "unique", "index"] as const; + +// Allowed keys per descriptor kind. Used to reject unsupported options (e.g. legacy +// field-level `hooks`/`validate`, typos like `uniqe`) that TS structural typing would +// otherwise accept silently — buildField doesn't read them, so they would have no effect. +const KIND_ALLOWED_KEYS: Record = { + string: [...INDEXABLE_DESCRIPTOR_KEYS, "vector", "serial"], + int: [...INDEXABLE_DESCRIPTOR_KEYS, "serial"], + float: INDEXABLE_DESCRIPTOR_KEYS, + bool: INDEXABLE_DESCRIPTOR_KEYS, + uuid: [...INDEXABLE_DESCRIPTOR_KEYS, "relation"], + decimal: [...INDEXABLE_DESCRIPTOR_KEYS, "scale"], + date: INDEXABLE_DESCRIPTOR_KEYS, + datetime: INDEXABLE_DESCRIPTOR_KEYS, + time: INDEXABLE_DESCRIPTOR_KEYS, + enum: [...INDEXABLE_DESCRIPTOR_KEYS, "values", "typeName"], + object: [...COMMON_DESCRIPTOR_KEYS, "fields", "typeName"], +}; + +function assertDescriptorKeys(descriptor: FieldDescriptor, path: string): void { + const allowed = KIND_ALLOWED_KEYS[descriptor.kind]; + const unknown = Object.keys(descriptor).filter((k) => !allowed.includes(k)); + if (unknown.length === 0) return; + throw new Error( + `Field "${path}" (kind "${descriptor.kind}"): unknown option(s) ${unknown + .map((k) => `"${k}"`) + .join(", ")}. Allowed: ${allowed.join(", ")}`, + ); +} + +function resolveField(entry: FieldEntry, path: string): TailorAnyDBField { + if (isPassthroughField(entry)) { + const cast = entry as { type?: unknown; metadata?: unknown }; + if (typeof cast.type !== "string" || typeof cast.metadata !== "object" || !cast.metadata) { + throw new Error( + "Expected a field descriptor (with `kind`) or a db.*() field instance (with `type`)", + ); + } + return entry; + } + return buildField(entry, path); +} + +function resolveFieldMap( + entries: Record, + pathPrefix: string, +): Record { + return Object.fromEntries( + Object.entries(entries).map(([key, entry]) => [ + key, + resolveField(entry, pathPrefix ? `${pathPrefix}.${key}` : key), + ]), + ); +} + +function buildField(descriptor: FieldDescriptor, path: string): TailorAnyDBField { + if (!(descriptor.kind in kindToFieldType)) { + throw new Error(`Unknown field descriptor kind: "${String(descriptor.kind)}"`); + } + assertDescriptorKeys(descriptor, path); + const fieldType = kindToFieldType[descriptor.kind]; + const options = { + ...(descriptor.optional === true && { optional: true as const }), + ...(descriptor.array === true && { array: true as const }), + }; + + let values: AllowedValues | undefined; + if (descriptor.kind === "enum") { + if (!Array.isArray(descriptor.values) || descriptor.values.length === 0) { + throw new Error('Enum field descriptor requires a non-empty "values" array'); + } + values = descriptor.values; + } + + const nestedFields = + descriptor.kind === "object" ? resolveFieldMap(descriptor.fields, path) : undefined; + + let field: TailorAnyDBField = createTailorDBField(fieldType, options, nestedFields, values); + + if (descriptor.generated === true) { + field._metadata.generated = true; + } + + if (descriptor.description !== undefined) { + field = field.description(descriptor.description); + } + + if ( + (descriptor.kind === "enum" || descriptor.kind === "object") && + descriptor.typeName !== undefined + ) { + // oxlint-disable-next-line no-explicit-any -- typeName() is only present on enum/nested field interfaces + field = (field as any).typeName(descriptor.typeName); + } + + // Object descriptors only support description and typeName; skip indexable options. + if (descriptor.kind === "object") { + return field; + } + + const isArray = descriptor.array === true; + const relation = descriptor.kind === "uuid" ? descriptor.relation : undefined; + + // When a relation is present, the relation handler dictates index/unique flags. + if (!isArray && !relation) { + if (descriptor.unique === true) { + field = field.unique(); + } else if (descriptor.index === true) { + field = field.index(); + } + } + + if (!isArray && descriptor.kind === "string" && descriptor.vector === true) { + field = field.vector(); + } + + if (descriptor.kind === "decimal" && descriptor.scale !== undefined) { + assertValidDecimalScale(descriptor.scale); + // oxlint-disable-next-line no-explicit-any -- decimal scale is set via internal metadata + (field as any)._metadata.scale = descriptor.scale; + } + + if ( + !isArray && + (descriptor.kind === "string" || descriptor.kind === "int") && + descriptor.serial !== undefined + ) { + field = field.serial(descriptor.serial); + } + + if (relation) { + // oxlint-disable-next-line no-explicit-any -- relation() is only present on uuid field interface + field = (field as any).relation(relation); + if (!isArray) { + const relType = relation.type; + if (relType === "oneToOne" || relType === "1-1") { + field = field.unique(); + } else { + field = field.index(); + } + } + } + + return field; +} + +const idField = createTailorDBField("uuid"); +type IdField = typeof idField; + +type AllFields> = { id: IdField } & ResolvedFieldMap; + +/** + * Create a TailorDB type using an object-literal API. + * @param name - The name of the type, or a tuple of [name, pluralForm] + * @param descriptors - Field descriptors as an object literal + * @param options - Optional type-level options (permission, gqlPermission, features, etc.) + * @returns A new TailorDBType instance + * @example + * export const user = createTable("User", { + * name: { kind: "string" }, + * email: { kind: "string", unique: true }, + * status: { kind: "string", optional: true }, + * role: { kind: "enum", values: ["MANAGER", "STAFF"] }, + * ...timestampFields(), + * }); + * export type user = typeof user; + */ +// Overload 1: FieldDescriptor-only. Narrows the entry constraint so TS infers +// descriptor literals against `FieldDescriptor` rather than the wider +// `FieldEntry` union, which is needed for `options.permission`/`options.hooks` +// callbacks to receive precisely-typed `data` for descriptor-only types. +export function createTable>( + name: string | [string, string], + descriptors: [D] extends [ValidatedDescriptors] ? D : ValidatedDescriptors, + options?: CreateTableOptions & string, AllFields>, +): TailorDBType>; +// Overload 2: mixed FieldDescriptor + TailorAnyDBField (fallback) +export function createTable>( + name: string | [string, string], + descriptors: [D] extends [ValidatedDescriptors] ? D : ValidatedDescriptors, + options?: CreateTableOptions & string, AllFields>, +): TailorDBType>; +export function createTable>( + name: string | [string, string], + descriptors: [D] extends [ValidatedDescriptors] ? D : ValidatedDescriptors, + options?: CreateTableOptions & string, AllFields>, +): TailorDBType> { + if (Array.isArray(name) && options?.pluralForm !== undefined) { + throw new Error( + `createTable("${name[0]}"): pluralForm is specified twice (once via the name tuple "${name[1]}" and once via options.pluralForm "${options.pluralForm}"). Pick one.`, + ); + } + const [typeName, pluralForm] = Array.isArray(name) ? name : [name, options?.pluralForm]; + const fields = { + id: idField.clone(), + ...resolveFieldMap(descriptors, ""), + } as AllFields; + + const dbType = createTailorDBType(typeName, fields, { + pluralForm, + description: options?.description, + }); + + if (options?.features) { + dbType.features(options.features); + } + if (options?.indexes) { + // oxlint-disable-next-line no-explicit-any -- IndexDef generic param differs structurally from TailorDBType + dbType.indexes(...(options.indexes as any)); + } + if (options?.files) { + // oxlint-disable-next-line no-explicit-any -- files() infers literal key type; pre-validated by CreateTableOptions constraint + dbType.files(options.files as any); + } + if (options?.permission) { + dbType.permission(options.permission); + } + if (options?.gqlPermission) { + dbType.gqlPermission(options.gqlPermission); + } + if (options?.plugins) { + for (const { pluginId, config } of options.plugins) { + // oxlint-disable-next-line no-explicit-any -- PluginAttachment.config is unknown; bypass PluginConfigs generic constraint + dbType.plugin({ [pluginId]: config } as any); + } + } + if (options?.hooks) { + dbType.hooks(options.hooks); + } + if (options?.validate) { + dbType.validate(options.validate); + } + + return dbType; +} + +/** + * Returns standard timestamp field descriptors (createdAt, updatedAt). + * Hooks for auto-populating these timestamps must be configured at the record + * level via `options.hooks` (see `createTable`). + * @returns An object with createdAt and updatedAt field descriptors + * @example + * const model = createTable( + * "Model", + * { + * name: { kind: "string" }, + * ...timestampFields(), + * }, + * { + * hooks: { + * create: () => ({ createdAt: new Date() }), + * update: () => ({ updatedAt: new Date() }), + * }, + * }, + * ); + */ +export function timestampFields() { + return { + createdAt: { + kind: "datetime", + description: "Record creation timestamp", + generated: true, + }, + updatedAt: { + kind: "datetime", + optional: true, + description: "Record last update timestamp", + generated: true, + }, + } as const satisfies Record; +} diff --git a/packages/sdk/src/configure/services/tailordb/index.ts b/packages/sdk/src/configure/services/tailordb/index.ts index 6e13dfe43..b74cb73f5 100644 --- a/packages/sdk/src/configure/services/tailordb/index.ts +++ b/packages/sdk/src/configure/services/tailordb/index.ts @@ -6,6 +6,7 @@ export { type TailorDBType, } from "./schema"; export type { TailorDBInstance } from "./schema"; +export { createTable, timestampFields } from "./createTable"; export { unsafeAllowAllTypePermission, unsafeAllowAllGqlPermission, @@ -13,7 +14,7 @@ export { type TailorTypeGqlPermission, type PermissionCondition, } from "./permission"; -export type { Hook, TypeFeatures } from "./types"; +export type { RecordHook, TypeFeatures } from "./types"; export type { TailorDBServiceConfig } from "@/types/tailordb.generated"; export type { DBFieldMetadata, diff --git a/packages/sdk/src/configure/services/tailordb/schema.test.ts b/packages/sdk/src/configure/services/tailordb/schema.test.ts index a5cdacbbf..e8710a722 100644 --- a/packages/sdk/src/configure/services/tailordb/schema.test.ts +++ b/packages/sdk/src/configure/services/tailordb/schema.test.ts @@ -1,10 +1,10 @@ import { describe, it, expectTypeOf, expect } from "vitest"; import { t } from "@/configure/types"; import { db } from "./schema"; -import type { Hook } from "./types"; +import type { RecordHook } from "./types"; import type { output } from "@/types/helpers"; import type { TailorUser } from "@/types/user"; -import type { FieldValidateInput, ValidateConfig } from "@/types/validation"; +import type { RecordValidators } from "@/types/validation"; describe("TailorDBField basic field type tests", () => { it("string field outputs string type correctly", () => { @@ -414,102 +414,26 @@ describe("TailorDBField relation modifier tests", () => { }); }); -describe("TailorDBField hooks modifier tests", () => { - it("hooks modifier does not affect output type", () => { - const _hookType = db.type("Test", { - name: db.string().hooks({ - create: () => "created", - update: () => "updated", - }), - }); - expectTypeOf>().toEqualTypeOf<{ - id: string; - name: string; - }>(); - }); - - it("setting hooks on nested field causes type error", () => { - // @ts-expect-error hooks() cannot be called on nested fields - db.object({ - first: db.string(), - last: db.string(), - }).hooks({ create: () => ({ first: "A", last: "B" }) }); - }); - - it("hooks modifier on string field receives string", () => { - const _hooks = db.string().hooks; - expectTypeOf[0]>().toEqualTypeOf>(); - }); - - it("hooks modifier on optional field receives null", () => { - const _hooks = db.string({ optional: true }).hooks; - expectTypeOf[0]>().toEqualTypeOf>(); - }); -}); - -describe("TailorDBField validate modifier tests", () => { - it("validate modifier does not affect type", () => { - const _validateType = db.type("Test", { - email: db.string().validate(() => true), - }); - expectTypeOf>().toEqualTypeOf<{ - id: string; - email: string; - }>(); - }); - - it("validate modifier can receive object with message", () => { - const _validateType = db.type("Test", { - email: db.string().validate([({ value }) => value.includes("@"), "Email must contain @"]), - }); - expectTypeOf>().toEqualTypeOf<{ - id: string; - email: string; - }>(); - - // Validate that the validation is stored correctly in metadata - const fieldMetadata = _validateType.fields.email.metadata; - expect(fieldMetadata.validate).toBeDefined(); - expect(fieldMetadata.validate).toHaveLength(1); - // Error message is part of the tuple [fn, message] - expect(fieldMetadata.validate?.[0]).toEqual([expect.any(Function), "Email must contain @"]); - }); - - it("validate modifier can receive multiple validators", () => { - const _validateType = db.type("Test", { - password: db - .string() - .validate( - ({ value }) => value.length >= 8, - [({ value }) => /[A-Z]/.test(value), "Password must contain uppercase letter"], - ), - }); - - const fieldMetadata = _validateType.fields.password.metadata; - expect(fieldMetadata.validate).toHaveLength(2); - // Second validator is a tuple [fn, errorMessage] - expect((fieldMetadata.validate?.[1] as [unknown, string])[1]).toBe( - "Password must contain uppercase letter", - ); - }); - - it("calling validate modifier more than once causes type error", () => { - // @ts-expect-error validate() cannot be called after validate() has already been called - db.string() - .validate(() => true) - .validate(() => true); - }); - - it("validate modifier on string field receives string", () => { - const _validate = db.string().validate; - expectTypeOf[1]>().toEqualTypeOf>(); - }); - - it("validate modifier on optional field receives null", () => { - const _validate = db.string({ optional: true }).validate; - expectTypeOf[1]>().toEqualTypeOf< - FieldValidateInput - >(); +describe("TailorDBField field-level hooks/validate removal", () => { + it("TailorDBField does not expose a field-level hooks method", () => { + // Type-level assertion only (do not invoke at runtime) + const field = db.string(); + // @ts-expect-error `hooks` has been removed from the field-level API + type _Hooks = typeof field.hooks; + }); + + it("TailorDBField validate is typed as `this: never` to block field-level calls", () => { + // The `validate` method is declared as + // validate(this: never, ...args: never[]): never; + // so calling it on a concrete field instance is a type error. Pattern- + // match on the function signature to assert both the `this` type and the + // return type are `never`. + type FieldValidate = ReturnType["validate"]; + type _AssertShape = FieldValidate extends (this: never, ...args: never[]) => never + ? true + : false; + const _check: _AssertShape = true; + expect(_check).toBe(true); }); }); @@ -837,18 +761,17 @@ describe("TailorDBType plural form tests", () => { name: db.string(), email: db.string(), }) - .validate({ - name: [({ value }) => value.length > 0], - email: [({ value }) => value.includes("@"), "Invalid email format"], - }); + .validate([ + ({ data }) => data.name.length > 0, + [({ data }) => data.email.includes("@"), "Invalid email format"], + ]); expect(_userType.name).toBe("User"); expect(_userType.metadata.settings?.pluralForm).toBe("Users"); - // Validate that the validation function is stored correctly in metadata - const emailMetadata = _userType.fields.email.metadata; - expect(emailMetadata.validate).toBeDefined(); - expect(emailMetadata.validate).toHaveLength(1); + // Record-level validators are stored on the type metadata + expect(_userType.metadata.validate).toBeDefined(); + expect(_userType.metadata.validate).toHaveLength(2); }); it("plural form works correctly for types with relations", () => { @@ -877,17 +800,15 @@ describe("TailorDBType plural form tests", () => { }); }); -describe("TailorDBType hooks modifier tests", () => { +describe("TailorDBType record-level hooks modifier tests", () => { it("hooks modifier does not affect output type", () => { const _hookType = db .type("Test", { name: db.string(), }) .hooks({ - name: { - create: () => "created", - update: () => "updated", - }, + create: () => ({ name: "created" }), + update: () => ({ name: "updated" }), }); expectTypeOf>().toEqualTypeOf<{ id: string; @@ -895,154 +816,118 @@ describe("TailorDBType hooks modifier tests", () => { }>(); }); - it("setting hooks on id causes type error", () => { + it("hooks create/update receive the full record as readonly data", () => { db.type("Test", { name: db.string(), + score: db.int(), }).hooks({ - // @ts-expect-error hooks() cannot be called on the "id" field - id: { - create: () => "created", + create: ({ data }) => { + expectTypeOf(data).toEqualTypeOf>(); + return { score: data.score + 1 }; }, + update: ({ data }) => ({ score: data.score + 1 }), }); }); - it("setting hooks on nested field causes type error", () => { + it("hooks may return only the fields to override", () => { db.type("Test", { - name: db.object({ - first: db.string(), - last: db.string(), - }), - // @ts-expect-error hooks() cannot be called on nested fields + name: db.string(), + score: db.int(), }).hooks({ - name: { - create: () => "created", - }, + create: () => ({ name: "created" }), + update: ({ data }) => ({ score: data.score + 1 }), }); }); - it("hooks modifier on string field receives string", () => { + it("hooks modifier accepts RecordHook parameter", () => { const testType = db.type("Test", { name: db.string() }); - const _hooks = testType.hooks; - type ExpectedHooksParam = Parameters[0]; - type ActualNameType = Exclude; - - expectTypeOf().toEqualTypeOf< - Hook< - { - id: string; - readonly name: string; - }, - string - > - >(); + type HooksParam = Parameters[0]; + expectTypeOf().toEqualTypeOf>(); }); - it("hooks modifier on optional field receives null", () => { - const testType = db.type("Test", { - name: db.string({ optional: true }), + it("hooks modifier stores hooks on type metadata", () => { + const createHook = (_args: { data: Readonly<{ id: string; name: string }> }) => ({ + name: "c", + }); + const updateHook = (_args: { data: Readonly<{ id: string; name: string }> }) => ({ + name: "u", }); - const _hooks = testType.hooks; - type ExpectedHooksParam = Parameters[0]; - type ActualNameType = Exclude; + const hookType = db + .type("Test", { + name: db.string(), + }) + .hooks({ create: createHook, update: updateHook }); - expectTypeOf().toEqualTypeOf< - Hook< - { - id: string; - name?: string | null; - }, - string | null - > - >(); + expect(hookType.metadata.hooks).toBeDefined(); + expect(hookType.metadata.hooks?.create).toBe(createHook); + expect(hookType.metadata.hooks?.update).toBe(updateHook); }); }); -describe("TailorDBType validate modifier tests", () => { - it("validate modifier can receive function", () => { +describe("TailorDBType record-level validate modifier tests", () => { + it("validate modifier can receive a single function", () => { const _validateType = db .type("Test", { email: db.string(), }) - .validate({ - email: () => true, - }); + .validate(({ data }) => data.email.includes("@")); expectTypeOf>().toEqualTypeOf<{ id: string; email: string; }>(); - const fieldMetadata = _validateType.fields.email.metadata; - expect(fieldMetadata.validate).toHaveLength(1); + expect(_validateType.metadata.validate).toHaveLength(1); }); - it("validate modifier can receive object with message", () => { + it("validate modifier can receive a single [fn, message] tuple", () => { const _validateType = db .type("Test", { email: db.string(), }) - .validate({ - email: [({ value }) => value.includes("@"), "Email must contain @"], - }); + .validate([({ data }) => data.email.includes("@"), "Email must contain @"]); - const fieldMetadata = _validateType.fields.email.metadata; - expect(fieldMetadata.validate).toHaveLength(1); - // Validator is a tuple [fn, errorMessage] - expect((fieldMetadata.validate?.[0] as [unknown, string])[1]).toBe("Email must contain @"); + expect(_validateType.metadata.validate).toHaveLength(1); + expect((_validateType.metadata.validate?.[0] as [unknown, string])[1]).toBe( + "Email must contain @", + ); }); - it("validate modifier can receive multiple validators", () => { + it("validate modifier can receive an array of validators", () => { const _validateType = db .type("Test", { password: db.string(), }) - .validate({ - password: [ - ({ value }) => value.length >= 8, - [({ value }) => /[A-Z]/.test(value), "Password must contain uppercase letter"], - ], - }); + .validate([ + ({ data }) => data.password.length >= 8, + [({ data }) => /[A-Z]/.test(data.password), "Password must contain uppercase letter"], + ]); - const fieldMetadata = _validateType.fields.password.metadata; - expect(fieldMetadata.validate).toHaveLength(2); + expect(_validateType.metadata.validate).toHaveLength(2); // Second validator is a tuple [fn, errorMessage] - expect((fieldMetadata.validate?.[1] as [unknown, string])[1]).toBe( + expect((_validateType.metadata.validate?.[1] as [unknown, string])[1]).toBe( "Password must contain uppercase letter", ); }); - it("type error occurs when validate is already set on TailorDBField", () => { - db.type("Test", { - name: db.string().validate(() => true), - // @ts-expect-error validate() cannot be called after validate() has already been called - }).validate({ - name: () => true, - }); + it("validate modifier accepts RecordValidators parameter", () => { + const testType = db.type("Test", { name: db.string() }); + type ValidatorsParam = Parameters[0]; + expectTypeOf().toEqualTypeOf>(); }); - it("setting validate on id causes type error", () => { + it("validate fn receives the full record as data", () => { db.type("Test", { name: db.string(), - }).validate({ - // @ts-expect-error validate() cannot be called on the "id" field - id: () => true, + age: db.int({ optional: true }), + }).validate(({ data }) => { + expectTypeOf(data).toEqualTypeOf<{ + id: string; + name: string; + age?: number | null; + }>(); + return data.name.length > 0; }); }); - - it("validate modifier on string field receives string", () => { - const _validate = db.type("Test", { name: db.string() }).validate; - expectTypeOf>().toExtend< - Parameters[0]["name"] - >(); - }); - - it("validate modifier on optional field receives null", () => { - const _validate = db.type("Test", { - name: db.string({ optional: true }), - }).validate; - expectTypeOf>().toExtend< - Parameters[0]["name"] - >(); - }); }); describe("db.object tests", () => { @@ -1282,11 +1167,7 @@ describe("TailorDBField fluent API type preservation", () => { }); it("multiple method chain preserves type", () => { - const _field = db - .string() - .description("Email address") - .index() - .validate(({ value }) => value.includes("@")); + const _field = db.string().description("Email address").index().unique(); expectTypeOf>().toEqualTypeOf(); }); @@ -1604,27 +1485,6 @@ describe("TailorDBType gqlOperations alias tests", () => { }); describe("TailorDBField immutability", () => { - it("field.hooks() returns a new field without mutating the original", () => { - const original = db.string(); - const withHooks = original.hooks({ create: () => "created" }); - - // hooks() should return a NEW field - expect(withHooks).not.toBe(original); - // Original should NOT have hooks - expect(original.metadata.hooks).toBeUndefined(); - // New field should have hooks - expect(withHooks.metadata.hooks?.create).toBeDefined(); - }); - - it("field.validate() returns a new field without mutating the original", () => { - const original = db.string(); - const withValidate = original.validate(({ value }) => value.length > 0); - - expect(withValidate).not.toBe(original); - expect(original.metadata.validate).toBeUndefined(); - expect(withValidate.metadata.validate).toHaveLength(1); - }); - it("field.description() returns a new field without mutating the original", () => { const original = db.string(); const withDesc = original.description("desc"); @@ -1681,62 +1541,43 @@ describe("TailorDBField immutability", () => { }); it("chained fluent calls produce correct result", () => { - const field = db - .string() - .description("name") - .index() - .hooks({ create: () => "x" }); + const field = db.string().description("name").index().unique(); expect(field.metadata.description).toBe("name"); expect(field.metadata.index).toBe(true); - expect(field.metadata.hooks?.create).toBeDefined(); + expect(field.metadata.unique).toBe(true); }); }); -describe("TailorDBType does not mutate shared fields", () => { - it("type.hooks() does not mutate the shared field", () => { +describe("TailorDBType record-level hooks/validate storage", () => { + it("type.hooks() stores hooks on the owning type only", () => { const sharedField = db.string(); - const typeA = db.type("TypeA", { name: sharedField }).hooks({ name: { create: () => "A" } }); + const typeA = db.type("TypeA", { name: sharedField }).hooks({ + create: () => ({ name: "A" }), + }); const typeB = db.type("TypeB", { name: sharedField }); - expect(typeA.fields.name.metadata.hooks).toBeDefined(); - expect(typeB.fields.name.metadata.hooks).toBeUndefined(); + expect(typeA.metadata.hooks).toBeDefined(); + expect(typeB.metadata.hooks).toBeUndefined(); + // Shared field metadata is untouched expect(sharedField.metadata.hooks).toBeUndefined(); }); - it("type.validate() does not mutate the shared field", () => { + it("type.validate() stores validators on the owning type only", () => { const sharedField = db.string(); const typeA = db .type("TypeA", { email: sharedField }) - .validate({ email: ({ value }) => value.includes("@") }); + .validate(({ data }) => data.email.includes("@")); const typeB = db.type("TypeB", { email: sharedField }); - expect(typeA.fields.email.metadata.validate).toBeDefined(); - expect(typeB.fields.email.metadata.validate).toBeUndefined(); + expect(typeA.metadata.validate).toBeDefined(); + expect(typeA.metadata.validate).toHaveLength(1); + expect(typeB.metadata.validate).toBeUndefined(); + // Shared field metadata is untouched expect(sharedField.metadata.validate).toBeUndefined(); }); - - it("hooks() does not replace entries in the original fields record", () => { - const nameField = db.string(); - const fields = { name: nameField }; - - db.type("TypeA", fields).hooks({ name: { create: () => "hooked" } }); - - // The fields record should still reference the original field instance - expect(fields.name).toBe(nameField); - }); - - it("validate() does not replace entries in the original fields record", () => { - const emailField = db.string(); - const fields = { email: emailField }; - - db.type("TypeA", fields).validate({ email: ({ value }) => value.includes("@") }); - - // The fields record should still reference the original field instance - expect(fields.email).toBe(emailField); - }); }); describe("TailorDBField clone tests", () => { @@ -1814,44 +1655,6 @@ describe("TailorDBField clone tests", () => { expect(cloned.rawRelation?.toward).not.toBe(original.rawRelation?.toward); }); - it("clones hooks correctly", () => { - const createHook = () => "created"; - const original = db.string().hooks({ create: createHook }); - const cloned = original.clone(); - - expect(cloned.metadata.hooks).toBeDefined(); - expect(cloned.metadata.hooks?.create).toBe(createHook); - - // Verify deep copy (different reference) - expect(cloned.metadata.hooks).not.toBe(original.metadata.hooks); - }); - - it("clones validate correctly", () => { - const validator = ({ value }: { value: string }) => value.length > 0; - const original = db.string().validate(validator); - const cloned = original.clone(); - - expect(cloned.metadata.validate).toBeDefined(); - expect(cloned.metadata.validate).toHaveLength(1); - - // Verify deep copy (different reference) - expect(cloned.metadata.validate).not.toBe(original.metadata.validate); - }); - - it("clones validate with tuple format correctly", () => { - const validator = ({ value }: { value: string }) => value.length > 0; - const original = db.string().validate([validator, "Value must not be empty"]); - const cloned = original.clone(); - - expect(cloned.metadata.validate).toBeDefined(); - expect(cloned.metadata.validate).toHaveLength(1); - expect(cloned.metadata.validate?.[0]).toEqual([validator, "Value must not be empty"]); - - // Verify deep copy (different reference for array and tuple) - expect(cloned.metadata.validate).not.toBe(original.metadata.validate); - expect(cloned.metadata.validate?.[0]).not.toBe(original.metadata.validate?.[0]); - }); - it("clones serial config correctly", () => { const original = db.int().serial({ start: 100 }); const cloned = original.clone(); diff --git a/packages/sdk/src/configure/services/tailordb/schema.ts b/packages/sdk/src/configure/services/tailordb/schema.ts index 6624b0666..d0d783bc0 100644 --- a/packages/sdk/src/configure/services/tailordb/schema.ts +++ b/packages/sdk/src/configure/services/tailordb/schema.ts @@ -6,7 +6,7 @@ import { } from "@/configure/types/field"; import { brandValue } from "@/utils/brand"; import type { TailorTypeGqlPermission, TailorTypePermission } from "./permission"; -import type { Hook, Hooks, ExcludeNestedDBFields, TypeFeatures } from "./types"; +import type { ExcludeNestedDBFields, RecordHook, TypeFeatures } from "./types"; import type { FieldOptions, FieldOutput, TailorFieldType, TailorToTs } from "@/types/field-types"; import type { output, InferFieldsOutput, Prettify } from "@/types/helpers"; import type { PluginAttachment, PluginConfigs } from "@/types/plugin"; @@ -26,9 +26,32 @@ import type { } from "@/types/tailordb"; import type { RawPermissions } from "@/types/tailordb.generated"; import type { InferredAttributeMap, TailorUser } from "@/types/user"; -import type { FieldValidateInput, ValidateConfig, Validators } from "@/types/validation"; +import type { RecordValidateInput, RecordValidators } from "@/types/validation"; import type { StandardSchemaV1 } from "@standard-schema/spec"; +/** + * Distinguishes a single `[fn, message]` tuple from an array of record validators. + * A config tuple has exactly 2 elements where the second is a string. + * @param value - Potential validators array or tuple + * @returns True if the value is a single `[fn, message]` tuple + */ +function isRecordValidateConfig(value: readonly unknown[]): boolean { + return value.length === 2 && typeof value[1] === "string" && typeof value[0] === "function"; +} + +/** + * Assert that a decimal `scale` is an integer in the supported range (0-12). + * Used by both the fluent `db.decimal()` factory and the `createTable` descriptor + * pipeline so both APIs reject malformed scales with the same error. + * @param scale - Candidate scale value + * @throws Error if the scale is not an integer between 0 and 12 + */ +export function assertValidDecimalScale(scale: number): void { + if (!Number.isInteger(scale) || scale < 0 || scale > 12) { + throw new Error("scale must be an integer between 0 and 12"); + } +} + // Helper alias: DB fields can be arbitrarily nested, so we intentionally keep this loose. // oxlint-disable-next-line no-explicit-any export type TailorAnyDBField = TailorDBField; @@ -40,6 +63,10 @@ export type TailorAnyDBType = TailorDBType; /** * Full TailorDBField interface with builder methods. * Extends the minimal structural interface from types/ with fluent API methods. + * + * NOTE: Field-level `hooks` and `validate` have been removed from the public API. + * Configure them at the record level via `db.type(...).hooks(...) / .validate(...)` + * or via the third `options` argument of `createTable`. */ export interface TailorDBField< Defined extends DefinedDBFieldMetadata = DefinedDBFieldMetadata, @@ -60,6 +87,13 @@ export interface TailorDBField< */ _parseInternal(args: FieldParseInternalArgs): StandardSchemaV1.Result; + /** + * Field-level `validate` has been removed from the public TailorDB API. + * Configure validation at the record level via + * `db.type(...).validate(...)` or the third `options` argument of `createTable`. + */ + validate(this: never, ...args: never[]): never; + /** * typeName is not available on TailorDB fields. * Use typeName on pipeline fields (t.enum / t.object) instead. @@ -139,39 +173,6 @@ export interface TailorDBField< : never, ): TailorDBField, Output>; - /** - * Add hooks for create/update operations on this field. - */ - hooks>( - this: CurrentDefined extends { hooks: unknown } - ? never - : CurrentDefined extends { type: "nested" } - ? never - : TailorDBField, - hooks: H, - ): TailorDBField< - Prettify< - CurrentDefined & { - hooks?: { - create: H extends { create: unknown } ? true : false; - update: H extends { update: unknown } ? true : false; - }; - serial: false; - } - >, - Output - >; - - /** - * Add validation functions to the field. - */ - validate( - this: CurrentDefined extends { validate: unknown } - ? never - : TailorDBField, - ...validate: FieldValidateInput[] - ): TailorDBField, Output>; - /** * Configure serial/auto-increment behavior */ @@ -220,8 +221,27 @@ export interface TailorDBType< > extends TailorDBTypeBase { _description?: string; - hooks(hooks: Hooks): TailorDBType; - validate(validators: Validators): TailorDBType; + /** + * Add record-level create/update hooks. Each callback receives `{ data, user }` + * and returns an object containing only the fields to override on the record. + * Unchanged fields can be omitted; their incoming values are preserved. + * + * Note: until the platform supports true type-level hooks, each returned key + * is materialized as an independent field-level hook on the wire. The + * function body is therefore re-evaluated once per returned key, so the + * values must be pure with respect to the inputs — `crypto.randomUUID()`, + * `new Date()`, and other side-effecting calls produce a different result + * per field rather than a single shared value. + */ + hooks(hooks: RecordHook>): TailorDBType; + + /** + * Add record-level validators. Each callback receives `{ data, user }` and must + * return `true` for a valid record. Use the tuple form `[fn, message]` for + * diagnosable error messages. + */ + validate(validators: RecordValidators>): TailorDBType; + features(features: Omit): TailorDBType; indexes(...indexes: IndexDef>[]): TailorDBType; files( @@ -258,6 +278,10 @@ export interface TailorDBType< : never; }; omitFields(keys: K[]): Omit; + + /** Plugin attachments for this type */ + readonly plugins: PluginAttachment[]; + plugin

>(config: { [K in P]: PluginConfigs[K]; }): TailorDBType; @@ -335,7 +359,7 @@ type FieldParseInternalArgs = { * @param values - Allowed values for enum-like fields * @returns A new TailorDBField */ -function createTailorDBField< +export function createTailorDBField< const T extends TailorFieldType, const TOptions extends FieldOptions, const OutputBase = TailorToTs[T], @@ -498,24 +522,6 @@ function createTailorDBField< break; } - // Custom validation functions - const validateFns = field._metadata.validate; - if (validateFns && validateFns.length > 0) { - for (const validateInput of validateFns) { - const { fn, message } = - typeof validateInput === "function" - ? { fn: validateInput, message: "Validation failed" } - : { fn: validateInput[0], message: validateInput[1] }; - - if (!fn({ value, data, user })) { - issues.push({ - message, - path: pathArray.length > 0 ? pathArray : undefined, - }); - } - } - } - return issues; } @@ -621,10 +627,15 @@ function createTailorDBField< // oxlint-disable-next-line no-explicit-any typeName: ((typeName: string) => cloneWith({ typeName })) as any, - validate(...validateInputs: FieldValidateInput>[]) { + // Field-level `validate` has been removed. The stub throws to surface the mistake + // at runtime even though the `this: never` signature prevents type-level calls. + // oxlint-disable-next-line no-explicit-any + validate: (() => { + throw new Error( + "Field-level `.validate()` has been removed. Use `db.type(...).validate(...)` or the third `options` argument of `createTable` instead.", + ); // oxlint-disable-next-line no-explicit-any - return cloneWith({ validate: validateInputs }) as any; - }, + }) as any, parse(args: FieldParseArgs): StandardSchemaV1.Result> { return parseInternal({ @@ -670,11 +681,6 @@ function createTailorDBField< return cloneWith({ vector: true }) as any; }, - hooks(hooks: Hook>) { - // oxlint-disable-next-line no-explicit-any - return cloneWith({ hooks }) as any; - }, - serial(config: SerialConfig) { // oxlint-disable-next-line no-explicit-any return cloneWith({ serial: config }) as any; @@ -800,9 +806,7 @@ interface DecimalFieldOptions extends FieldOptions { */ function decimal(options?: Opt) { if (options?.scale !== undefined) { - if (!Number.isInteger(options.scale) || options.scale < 0 || options.scale > 12) { - throw new Error("scale must be an integer between 0 and 12"); - } + assertValidDecimalScale(options.scale); } const field = createField("decimal", options); if (options?.scale !== undefined) { @@ -889,7 +893,7 @@ function object< * @param options.description - Optional description * @returns A new TailorDBType */ -function createTailorDBType< +export function createTailorDBType< // oxlint-disable-next-line no-explicit-any const Fields extends Record = any, User extends object = InferredAttributeMap, @@ -904,6 +908,8 @@ function createTailorDBType< const _permissions: RawPermissions = {}; let _files: Record = {}; const _plugins: PluginAttachment[] = []; + let _recordHooks: RecordHook> | undefined; + let _recordValidators: RecordValidateInput>[] | undefined; if (options.pluralForm) { if (name === options.pluralForm) { @@ -939,43 +945,21 @@ function createTailorDBType< permissions: _permissions, files: _files, ...(Object.keys(indexes).length > 0 && { indexes }), + ...(_recordHooks && { hooks: _recordHooks }), + ...(_recordValidators && { validate: _recordValidators }), }; }, - hooks(hooks: Hooks) { - // `Hooks` is strongly typed, but `Object.entries()` loses that information. - // oxlint-disable-next-line no-explicit-any - Object.entries(hooks).forEach(([fieldName, fieldHooks]: [string, any]) => { - (this.fields as Record)[fieldName] = - this.fields[fieldName].hooks(fieldHooks); - }); + hooks(hooks: RecordHook>) { + _recordHooks = hooks; return this; }, - validate(validators: Validators) { - Object.entries(validators).forEach(([fieldName, fieldValidators]) => { - const field = this.fields[fieldName] as TailorAnyDBField; - - const validators = fieldValidators as - | FieldValidateInput - | FieldValidateInput[]; - - const isValidateConfig = (v: unknown): v is ValidateConfig => { - return Array.isArray(v) && v.length === 2 && typeof v[1] === "string"; - }; - - let updatedField: TailorAnyDBField; - if (Array.isArray(validators)) { - if (isValidateConfig(validators)) { - updatedField = field.validate(validators); - } else { - updatedField = field.validate(...validators); - } - } else { - updatedField = field.validate(validators); - } - (this.fields as Record)[fieldName] = updatedField; - }); + validate(validators: RecordValidators>) { + _recordValidators = + Array.isArray(validators) && !isRecordValidateConfig(validators) + ? (validators as RecordValidateInput>[]) + : [validators as RecordValidateInput>]; return this; }, @@ -1149,22 +1133,25 @@ export const db = { object, fields: { /** - * Creates standard timestamp fields (createdAt, updatedAt) with auto-hooks. - * createdAt is set on create, updatedAt is set on update. + * Creates standard timestamp fields (createdAt, updatedAt). + * Users must populate these via record-level hooks on `db.type(...).hooks(...)` + * or via the third `options` argument of `createTable`. * @returns An object with createdAt and updatedAt fields * @example * const model = db.type("Model", { * name: db.string(), * ...db.fields.timestamps(), + * }).hooks({ + * create: () => ({ createdAt: new Date() }), + * update: () => ({ updatedAt: new Date() }), * }); */ - timestamps: () => ({ - createdAt: datetime() - .hooks({ create: () => new Date() }) - .description("Record creation timestamp"), - updatedAt: datetime({ optional: true }) - .hooks({ update: () => new Date() }) - .description("Record last update timestamp"), - }), + timestamps: () => { + const createdAt = datetime().description("Record creation timestamp"); + createdAt._metadata.generated = true; + const updatedAt = datetime({ optional: true }).description("Record last update timestamp"); + updatedAt._metadata.generated = true; + return { createdAt, updatedAt }; + }, }, }; diff --git a/packages/sdk/src/configure/services/tailordb/types.ts b/packages/sdk/src/configure/services/tailordb/types.ts index fa3c29b59..547b3c987 100644 --- a/packages/sdk/src/configure/services/tailordb/types.ts +++ b/packages/sdk/src/configure/services/tailordb/types.ts @@ -1,36 +1,37 @@ -import type { output } from "@/types/helpers"; import type { TailorAnyDBField, TailorDBField } from "@/types/tailor-db-field"; import type { GqlOperationsConfig } from "@/types/tailordb"; import type { TailorUser } from "@/types/user"; -import type { NonEmptyObject } from "type-fest"; // --- Hook types (UX-focused, for configure layer) --- -type HookFn = (args: { - value: TValue; - data: TData extends Record - ? { readonly [K in keyof TData]?: TData[K] | null | undefined } - : unknown; - user: TailorUser; -}) => TReturn; - -export type Hook = { - create?: HookFn; - update?: HookFn; +/** + * Record-level hook function arguments. + * `data` is the full record snapshot at hook time. + */ +type RecordHookFnArgs = { + readonly data: Readonly; + readonly user: TailorUser; }; -export type Hooks< - F extends Record, - TData = { [K in keyof F]: output }, -> = NonEmptyObject<{ - [K in Exclude as F[K]["_defined"] extends { - hooks: unknown; - } - ? never - : F[K]["_defined"] extends { type: "nested" } - ? never - : K]?: Hook>; -}>; +/** + * Record-level hook function. + * Receives the entire record `data` and must return an object containing + * only the fields to override on the record. Unchanged fields can be omitted. + * `id` cannot be overridden — TailorDB owns the synthetic UUID and the deploy + * manifest strips it from the field set, so any local override would silently + * desync from production. + */ +type RecordHookFn = (args: RecordHookFnArgs) => Partial>; + +/** + * Record-level hooks for create/update operations. + * Each callback receives `{ data, user }` and returns an object with only the + * fields to override; omitted fields keep their incoming values. + */ +export type RecordHook = { + create?: RecordHookFn; + update?: RecordHookFn; +}; // --- Field helper types --- diff --git a/packages/sdk/src/configure/types/type.ts b/packages/sdk/src/configure/types/type.ts index 136a1572f..a209239b7 100644 --- a/packages/sdk/src/configure/types/type.ts +++ b/packages/sdk/src/configure/types/type.ts @@ -120,13 +120,14 @@ type FieldParseInternalArgs = { /** * Creates a new TailorField instance. + * @internal * @param type - Field type * @param options - Field options * @param fields - Nested fields for object-like types * @param values - Allowed values for enum-like fields * @returns A new TailorField */ -function createTailorField< +export function createTailorField< const T extends TailorFieldType, const TOptions extends FieldOptions, const OutputBase = TailorToTs[T], diff --git a/packages/sdk/src/parser/service/resolver/schema.test.ts b/packages/sdk/src/parser/service/resolver/schema.test.ts new file mode 100644 index 000000000..c04f61de0 --- /dev/null +++ b/packages/sdk/src/parser/service/resolver/schema.test.ts @@ -0,0 +1,127 @@ +import { describe, expect, it } from "vitest"; +import { createResolver } from "@/configure/services/resolver/resolver"; +import { t } from "@/configure/types/type"; +import { ResolverSchema } from "./schema"; + +describe("ResolverSchema accepts descriptor-built resolvers", () => { + it("parses a resolver built entirely from object-literal descriptors", () => { + const resolver = createResolver({ + name: "add", + operation: "query", + input: { + a: { kind: "int", description: "First number" }, + b: { kind: "int" }, + }, + output: { kind: "int", description: "Sum" }, + body: ({ input }) => input.a + input.b, + }); + + const parsed = ResolverSchema.safeParse(resolver); + expect(parsed.success).toBe(true); + if (!parsed.success) return; + + expect(parsed.data.name).toBe("add"); + expect(parsed.data.operation).toBe("query"); + expect(parsed.data.input?.a.type).toBe("integer"); + expect(parsed.data.input?.a.metadata.description).toBe("First number"); + expect(parsed.data.input?.b.type).toBe("integer"); + expect(parsed.data.output.type).toBe("integer"); + expect(parsed.data.output.metadata.description).toBe("Sum"); + }); + + it("parses a resolver with mixed fluent and descriptor input fields", () => { + const resolver = createResolver({ + name: "mixedFields", + operation: "query", + input: { + descriptorField: { kind: "string" }, + fluentField: t.int(), + }, + output: { kind: "bool" }, + body: () => true, + }); + + const parsed = ResolverSchema.safeParse(resolver); + expect(parsed.success).toBe(true); + if (!parsed.success) return; + expect(parsed.data.input?.descriptorField.type).toBe("string"); + expect(parsed.data.input?.fluentField.type).toBe("integer"); + }); + + it("parses a resolver whose output is a Record of descriptors (wrapped as nested)", () => { + const resolver = createResolver({ + name: "recordOutput", + operation: "mutation", + input: { id: { kind: "uuid" } }, + output: { + success: { kind: "bool" }, + message: { kind: "string", optional: true }, + }, + body: () => ({ success: true, message: null }), + }); + + const parsed = ResolverSchema.safeParse(resolver); + expect(parsed.success).toBe(true); + if (!parsed.success) return; + expect(parsed.data.output.type).toBe("nested"); + expect(parsed.data.output.fields.success.type).toBe("boolean"); + expect(parsed.data.output.fields.message.type).toBe("string"); + expect(parsed.data.output.fields.message.metadata.required).toBe(false); + }); + + it("parses a resolver with enum and object descriptors carrying typeName", () => { + const resolver = createResolver({ + name: "richDescriptors", + operation: "query", + input: { + role: { + kind: "enum", + values: ["ADMIN", "USER"], + typeName: "RoleEnum", + }, + profile: { + kind: "object", + typeName: "ProfilePayload", + fields: { + displayName: { kind: "string" }, + age: { kind: "int", optional: true }, + }, + }, + }, + output: { kind: "bool" }, + body: () => true, + }); + + const parsed = ResolverSchema.safeParse(resolver); + expect(parsed.success).toBe(true); + if (!parsed.success) return; + + expect(parsed.data.input?.role.type).toBe("enum"); + expect(parsed.data.input?.role.metadata.typeName).toBe("RoleEnum"); + expect(parsed.data.input?.role.metadata.allowedValues).toEqual([ + { value: "ADMIN", description: "" }, + { value: "USER", description: "" }, + ]); + expect(parsed.data.input?.profile.type).toBe("nested"); + expect(parsed.data.input?.profile.metadata.typeName).toBe("ProfilePayload"); + expect(parsed.data.input?.profile.fields.displayName.type).toBe("string"); + }); + + it("parses an array descriptor and preserves the array flag", () => { + const resolver = createResolver({ + name: "arrayInput", + operation: "query", + input: { + tags: { kind: "string", array: true }, + }, + output: { kind: "int" }, + body: ({ input }) => input.tags.length, + }); + + const parsed = ResolverSchema.safeParse(resolver); + expect(parsed.success).toBe(true); + if (!parsed.success) return; + expect(parsed.data.input?.tags.type).toBe("string"); + expect(parsed.data.input?.tags.metadata.array).toBe(true); + }); +}); diff --git a/packages/sdk/src/parser/service/tailordb/field.precompiled.test.ts b/packages/sdk/src/parser/service/tailordb/field.precompiled.test.ts deleted file mode 100644 index 95471707e..000000000 --- a/packages/sdk/src/parser/service/tailordb/field.precompiled.test.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { describe, expect, it } from "vitest"; -import { db } from "@/configure/services/tailordb/schema"; -import { toSchemaOutputs } from "@/utils/test/internal"; -import { parseFieldConfig } from "./field"; -import { setPrecompiledScriptExpr } from "./hooks-validate-precompiled-expr"; - -describe("parseFieldConfig precompiled expressions", () => { - it("uses precompiled hook expression when attached", () => { - const createHook = ({ value }: { value: string | null }) => value ?? "fallback"; - setPrecompiledScriptExpr(createHook, "PRECOMPILED_HOOK_EXPR"); - - const type = db.type("User", { - email: db.string().hooks({ create: createHook }), - }); - - const schema = toSchemaOutputs({ User: type }); - const field = parseFieldConfig(schema.User.fields.email); - - expect(field.hooks?.create?.expr).toBe("PRECOMPILED_HOOK_EXPR"); - }); - - it("uses precompiled validate expression when attached", () => { - const validator = ({ value }: { value: string }) => value.length > 0; - setPrecompiledScriptExpr(validator, "PRECOMPILED_VALIDATE_EXPR"); - - const type = db.type("User", { - email: db.string().validate(validator), - }); - - const schema = toSchemaOutputs({ User: type }); - const field = parseFieldConfig(schema.User.fields.email); - - expect(field.validate?.[0]?.script.expr).toBe("PRECOMPILED_VALIDATE_EXPR"); - }); -}); diff --git a/packages/sdk/src/parser/service/tailordb/field.test.ts b/packages/sdk/src/parser/service/tailordb/field.test.ts new file mode 100644 index 000000000..53576e601 --- /dev/null +++ b/packages/sdk/src/parser/service/tailordb/field.test.ts @@ -0,0 +1,41 @@ +import { describe, expect, it } from "vitest"; +import { db } from "@/configure/services/tailordb/schema"; +import { parseFieldConfig } from "./field"; + +describe("parseFieldConfig", () => { + describe("generated datetime hooks", () => { + it("generates create hook for required generated datetime (createdAt)", () => { + const { createdAt } = db.fields.timestamps(); + const config = parseFieldConfig(createdAt); + + expect(config.hooks).toBeDefined(); + expect(config.hooks?.create).toEqual({ expr: "new Date()" }); + expect(config.hooks?.update).toBeUndefined(); + }); + + it("generates update hook for optional generated datetime (updatedAt)", () => { + const { updatedAt } = db.fields.timestamps(); + const config = parseFieldConfig(updatedAt); + + expect(config.hooks).toBeDefined(); + expect(config.hooks?.create).toBeUndefined(); + expect(config.hooks?.update).toEqual({ expr: "new Date()" }); + }); + + it("does not generate hooks for non-generated datetime", () => { + const field = db.datetime(); + const config = parseFieldConfig(field); + + expect(config.hooks).toBeUndefined(); + }); + + it("does not generate hooks for generated non-datetime field", () => { + const field = db.string(); + // Manually set generated to simulate a non-datetime generated field + (field as unknown as { _metadata: { generated: boolean } })._metadata.generated = true; + const config = parseFieldConfig(field); + + expect(config.hooks).toBeUndefined(); + }); + }); +}); diff --git a/packages/sdk/src/parser/service/tailordb/field.ts b/packages/sdk/src/parser/service/tailordb/field.ts index 139e0f858..c930a18f6 100644 --- a/packages/sdk/src/parser/service/tailordb/field.ts +++ b/packages/sdk/src/parser/service/tailordb/field.ts @@ -35,20 +35,67 @@ export const stringifyFunction = (fn: Function): string => { }; /** - * Convert a hook function to a script expression. - * @param fn - Hook function - * @returns JavaScript expression calling the hook + * Argument-map literals passed to script invocations. The key (e.g. `_data`, + * `_value`, `_input`) is the runtime binding the platform exposes at the + * relevant scope; the property name (e.g. `data`, `value`) is the SDK-side + * parameter name that callbacks destructure. */ -// eslint-disable-next-line @typescript-eslint/no-unsafe-function-type -const convertHookToExpr = (fn: Function): string => { - const precompiledExpr = getPrecompiledScriptExpr(fn as (...args: never[]) => unknown); +export const SCRIPT_ARG_MAPS = { + /** Field-level scope: `_value`, `_data`, and `user` are bound. */ + field: `{ value: _value, data: _data, user: ${tailorUserMap} }`, + /** Record-level hook scope: each generated FieldHook binds the record to `_data`. */ + recordHook: `{ data: _data, user: ${tailorUserMap} }`, + /** Record-level validate scope: type_validate binds the record to `_input`. */ + recordValidate: `{ data: _input, user: ${tailorUserMap} }`, +} as const; + +export type ScriptArgMap = keyof typeof SCRIPT_ARG_MAPS; + +/** + * Compile a user-supplied callback into a JavaScript expression that invokes + * it inside the platform script sandbox. Uses the bundled/precompiled body + * when available (so `import`s in the user file resolve) and otherwise falls + * back to stringifying the function — `stringifyFunction` rewrites method + * shorthand into a function expression so the result is always callable. + * @param fn - Callback to compile + * @param argMap - Argument-map kind appropriate for the binding context + * @returns JavaScript expression evaluating the callback at runtime + */ +export const compileScriptExpr = ( + fn: (...args: never[]) => unknown, + argMap: ScriptArgMap = "field", +): string => { + const precompiledExpr = getPrecompiledScriptExpr(fn); if (precompiledExpr) { return precompiledExpr; } - const normalized = stringifyFunction(fn); - return `(${normalized})({ value: _value, data: _data, user: ${tailorUserMap} })`; + // eslint-disable-next-line @typescript-eslint/no-unsafe-function-type + const normalized = stringifyFunction(fn as unknown as Function); + return `(${normalized})(${SCRIPT_ARG_MAPS[argMap]})`; }; +/** + * Normalize a validator entry into a `{ fn, message }` pair. Accepts either a + * bare predicate function or a `[fn, message]` tuple. When only a function is + * supplied, synthesizes a default message from the function source so the + * surfaced error still references the offending predicate. + * @param v - Validator entry (function or `[function, message]` tuple) + * @returns Predicate function and the resolved error message + */ +// eslint-disable-next-line @typescript-eslint/no-unsafe-function-type +export function normalizeValidatorEntry(v: Function | [Function, string]): { + fn: (...args: never[]) => unknown; + message: string; +} { + if (typeof v === "function") { + return { + fn: v as (...args: never[]) => unknown, + message: `failed by \`${v.toString().trim()}\``, + }; + } + return { fn: v[0] as (...args: never[]) => unknown, message: v[1] }; +} + /** * Parse TailorDBField into OperatorFieldConfig. * This transforms user-defined functions into script expressions. @@ -80,16 +127,10 @@ export function parseFieldConfig( } : {}), validate: metadata.validate?.map((v) => { - const { fn, message } = - typeof v === "function" - ? { fn: v, message: `failed by \`${v.toString().trim()}\`` } - : { fn: v[0], message: v[1] }; - + const { fn, message } = normalizeValidatorEntry(v); return { script: { - expr: - getPrecompiledScriptExpr(fn) ?? - `(${fn.toString().trim()})({ value: _value, data: _data, user: ${tailorUserMap} })`, + expr: compileScriptExpr(fn), }, errorMessage: message, }; @@ -98,16 +139,25 @@ export function parseFieldConfig( ? { create: metadata.hooks.create ? { - expr: convertHookToExpr(metadata.hooks.create), + expr: compileScriptExpr(metadata.hooks.create as (...args: never[]) => unknown), } : undefined, update: metadata.hooks.update ? { - expr: convertHookToExpr(metadata.hooks.update), + expr: compileScriptExpr(metadata.hooks.update as (...args: never[]) => unknown), } : undefined, } - : undefined, + : metadata.generated && fieldType === "datetime" + ? { + // Auto-generate timestamp hooks for fields created by db.fields.timestamps(). + // Required datetime (createdAt) gets a create hook; + // optional datetime (updatedAt) gets an update hook. + // Record-level hooks may override these per-key in `applyRecordHooksToFields`. + create: metadata.required !== false ? { expr: "new Date()" } : undefined, + update: metadata.required === false ? { expr: "new Date()" } : undefined, + } + : undefined, serial: metadata.serial ? { start: metadata.serial.start, diff --git a/packages/sdk/src/parser/service/tailordb/record-hook-keys.test.ts b/packages/sdk/src/parser/service/tailordb/record-hook-keys.test.ts new file mode 100644 index 000000000..15f87e513 --- /dev/null +++ b/packages/sdk/src/parser/service/tailordb/record-hook-keys.test.ts @@ -0,0 +1,97 @@ +import { describe, expect, it } from "vitest"; +import { extractRecordHookOverrideKeys } from "./record-hook-keys"; + +describe("extractRecordHookOverrideKeys", () => { + describe("supported shapes", () => { + it("extracts keys from an arrow function with parenthesized object body", () => { + const fn = (_args: { data: { name: string } }) => ({ name: "x", age: 1 }); + expect(extractRecordHookOverrideKeys(fn.toString())).toEqual(["name", "age"]); + }); + + it("extracts keys from an arrow function with block body and a single return", () => { + const fn = (_args: { data: { name: string } }) => { + return { name: "x", flag: true }; + }; + expect(extractRecordHookOverrideKeys(fn.toString())).toEqual(["name", "flag"]); + }); + + it("extracts keys from a function expression with a single return", () => { + const source = `function (args) { return { foo: 1, bar: 2 }; }`; + expect(extractRecordHookOverrideKeys(source)).toEqual(["foo", "bar"]); + }); + + it("extracts shorthand property keys", () => { + const name = "x"; + const flag = true; + const fn = () => ({ name, flag }); + expect(extractRecordHookOverrideKeys(fn.toString())).toEqual(["name", "flag"]); + }); + + it("extracts string-literal property keys", () => { + const source = `() => ({ "kebab-key": 1, plain: 2 })`; + expect(extractRecordHookOverrideKeys(source)).toEqual(["kebab-key", "plain"]); + }); + + it("treats a nested return inside an inner function as non-conditional", () => { + // The inner arrow's return must not be counted as a branched return on the outer. + const source = `() => { const helper = () => 1; return { a: helper() }; }`; + expect(extractRecordHookOverrideKeys(source)).toEqual(["a"]); + }); + + it("returns an empty array when the override object literal is empty", () => { + const source = `() => ({})`; + expect(extractRecordHookOverrideKeys(source)).toEqual([]); + }); + }); + + describe("rejected shapes", () => { + it("throws when spread is used inside the return literal", () => { + const source = `(args) => ({ ...args.data, name: "x" })`; + expect(() => extractRecordHookOverrideKeys(source)).toThrow(/cannot use spread/); + }); + + it("throws when computed keys are used", () => { + const source = `(args) => ({ [args.key]: 1 })`; + expect(() => extractRecordHookOverrideKeys(source)).toThrow(/cannot use computed keys/); + }); + + it("throws when the return value is not an object literal", () => { + const source = `() => 42`; + expect(() => extractRecordHookOverrideKeys(source)).toThrow(/must return an object literal/); + }); + + it("throws when an early return exists inside an if-statement (branched return)", () => { + const source = `(args) => { if (args.flag) return { a: 1 }; return { b: 2 }; }`; + expect(() => extractRecordHookOverrideKeys(source)).toThrow( + /single object literal at the top level/, + ); + }); + + it("throws when the value is not a function (e.g. parsing produces a non-function init)", () => { + const source = `42`; + expect(() => extractRecordHookOverrideKeys(source)).toThrow( + /must be a function expression or arrow function/, + ); + }); + + it("throws when a key uses a numeric literal (unsupported key type)", () => { + const source = `() => ({ 0: "x" })`; + expect(() => extractRecordHookOverrideKeys(source)).toThrow(/unsupported key type/); + }); + + it("throws on a getter property in the return literal", () => { + const source = `() => ({ get name() { return "x"; } })`; + expect(() => extractRecordHookOverrideKeys(source)).toThrow(/getter property/); + }); + + it("throws on a setter property in the return literal", () => { + const source = `() => ({ set name(v) { /* noop */ } })`; + expect(() => extractRecordHookOverrideKeys(source)).toThrow(/setter property/); + }); + + it("throws on a method-shorthand property in the return literal", () => { + const source = `() => ({ name() { return "x"; } })`; + expect(() => extractRecordHookOverrideKeys(source)).toThrow(/method shorthand/); + }); + }); +}); diff --git a/packages/sdk/src/parser/service/tailordb/record-hook-keys.ts b/packages/sdk/src/parser/service/tailordb/record-hook-keys.ts new file mode 100644 index 000000000..92109fbee --- /dev/null +++ b/packages/sdk/src/parser/service/tailordb/record-hook-keys.ts @@ -0,0 +1,157 @@ +import { parseSync } from "oxc-parser"; +import type { Node, VariableDeclaration } from "@oxc-project/types"; + +/** + * Extract the static key set returned by a record-level hook function. + * + * Supported shapes: + * - Arrow with expression body: `(...) => ({ key1, key2 })` + * - Arrow with block body containing a single `return { ... }` statement + * - Function expression / method shorthand with a single `return { ... }` + * + * The returned object literal must use static property names (identifiers or + * string literals). Spread elements, computed keys, branched returns, and + * non-object return values throw a clear error so the user can refactor. + * @param fnSource - Stringified function source. + * @returns Set of override keys (insertion order preserved). + */ +export function extractRecordHookOverrideKeys(fnSource: string): string[] { + const { program } = parseSync("_.ts", `const __fn = ${fnSource};`); + + const declarator = (program.body[0] as VariableDeclaration | undefined)?.declarations[0]; + const fnNode = declarator?.init; + if (!fnNode) { + throw new Error(`Failed to parse record-level hook function: ${fnSource}`); + } + + let returnExpr: Node | null | undefined; + + if (fnNode.type === "ArrowFunctionExpression") { + if (fnNode.body.type !== "BlockStatement") { + returnExpr = fnNode.body; + } else { + returnExpr = findSingleReturnExpression(fnNode.body.body); + } + } else if (fnNode.type === "FunctionExpression") { + returnExpr = findSingleReturnExpression(fnNode.body?.body ?? []); + } else { + throw new Error( + `Record-level hook must be a function expression or arrow function. Got: ${fnNode.type}`, + ); + } + + // `({ ... })` parses as ParenthesizedExpression wrapping an ObjectExpression; unwrap. + while (returnExpr && returnExpr.type === "ParenthesizedExpression") { + returnExpr = (returnExpr as unknown as { expression: Node }).expression; + } + + if (!returnExpr) { + throw new Error( + "Record-level hook must return a single object literal at the top level. " + + "Refactor the function so its body is `({ ... })` or `return { ... }` with no branches.\n" + + ` hook: ${fnSource}`, + ); + } + + if (returnExpr.type !== "ObjectExpression") { + throw new Error( + "Record-level hook must return an object literal so override keys can be inferred. " + + `Got: ${returnExpr.type}.\n hook: ${fnSource}`, + ); + } + + const keys: string[] = []; + for (const prop of returnExpr.properties) { + if (prop.type === "SpreadElement") { + throw new Error( + "Record-level hook return literal cannot use spread (`...rest`); list overridden keys explicitly.\n" + + ` hook: ${fnSource}`, + ); + } + if (prop.computed) { + throw new Error( + "Record-level hook return literal cannot use computed keys (`[expr]: ...`); use plain identifiers.\n" + + ` hook: ${fnSource}`, + ); + } + if (prop.kind !== "init") { + throw new Error( + `Record-level hook return literal cannot use a ${prop.kind === "get" ? "getter" : "setter"} property; use \`key: value\` form.\n` + + ` hook: ${fnSource}`, + ); + } + if (prop.method) { + throw new Error( + "Record-level hook return literal cannot use a method shorthand (`key() { ... }`); use `key: value` form.\n" + + ` hook: ${fnSource}`, + ); + } + const key = prop.key; + if (key.type === "Identifier") { + keys.push(key.name); + } else if (key.type === "Literal" && typeof key.value === "string") { + keys.push(key.value); + } else { + throw new Error( + `Record-level hook return literal has an unsupported key type "${key.type}".\n` + + ` hook: ${fnSource}`, + ); + } + } + return keys; +} + +function findSingleReturnExpression(body: Node[]): Node | null { + // Any return nested inside an `if`/loop/switch/try would be conditional, and + // we cannot infer a single static set of override keys from a branched + // function. Reject the whole shape upfront so the caller surfaces a clear + // error instead of silently materializing keys from the unconditional return + // alone. + for (const stmt of body) { + if (stmt.type !== "ReturnStatement" && containsReturnStatement(stmt)) { + return null; + } + } + + let found: Node | null = null; + for (const stmt of body) { + if (stmt.type === "ReturnStatement") { + if (found) return null; + found = (stmt as unknown as { argument: Node | null }).argument; + } + } + return found; +} + +function containsReturnStatement(node: Node): boolean { + // Nested functions have their own return semantics — do not descend into + // them when scanning for the outer function's returns. + if ( + node.type === "ArrowFunctionExpression" || + node.type === "FunctionExpression" || + node.type === "FunctionDeclaration" + ) { + return false; + } + + for (const key of Object.keys(node)) { + if (key === "type" || key === "start" || key === "end" || key === "loc") continue; + const value = (node as unknown as Record)[key]; + if (Array.isArray(value)) { + for (const item of value) { + if (isNode(item) && (item.type === "ReturnStatement" || containsReturnStatement(item))) { + return true; + } + } + } else if (isNode(value)) { + if (value.type === "ReturnStatement" || containsReturnStatement(value)) { + return true; + } + } + } + return false; +} + +function isNode(value: unknown): value is Node { + return typeof value === "object" && value !== null && "type" in value; +} diff --git a/packages/sdk/src/parser/service/tailordb/schema.ts b/packages/sdk/src/parser/service/tailordb/schema.ts index efa72bf1c..f38cfcb49 100644 --- a/packages/sdk/src/parser/service/tailordb/schema.ts +++ b/packages/sdk/src/parser/service/tailordb/schema.ts @@ -100,6 +100,10 @@ export const DBFieldMetadataSchema = z.object({ .max(12) .optional() .describe("Decimal scale (number of digits after decimal point, 0-12)"), + generated: z + .boolean() + .optional() + .describe("Whether the field value is auto-generated (e.g. timestamps)"), }); const RelationTypeSchema = z.enum(relationTypesKeys); @@ -266,6 +270,17 @@ export const TailorDBTypeSchema = z.object({ }), ) .optional(), + validate: z + .array(z.union([functionSchema, z.tuple([functionSchema, z.string()])])) + .optional() + .describe("Record-level validation functions"), + hooks: z + .object({ + create: functionSchema.optional().describe("Record-level hook called on record creation"), + update: functionSchema.optional().describe("Record-level hook called on record update"), + }) + .optional() + .describe("Record-level hooks for create/update"), }), }); diff --git a/packages/sdk/src/parser/service/tailordb/type-parser.test.ts b/packages/sdk/src/parser/service/tailordb/type-parser.test.ts index f5ead9439..0170db7d9 100644 --- a/packages/sdk/src/parser/service/tailordb/type-parser.test.ts +++ b/packages/sdk/src/parser/service/tailordb/type-parser.test.ts @@ -1,6 +1,8 @@ import { describe, expect, it } from "vitest"; +import { createTable } from "@/configure/services/tailordb/createTable"; import { db } from "@/configure/services/tailordb/schema"; import { toSchemaOutputs } from "@/utils/test/internal"; +import { setPrecompiledScriptExpr } from "./hooks-validate-precompiled-expr"; import { parseTypes } from "./type-parser"; describe("parseTypes", () => { @@ -534,4 +536,300 @@ describe("parseTypes", () => { expect(result.User.backwardRelationships).toEqual({}); }); }); + + describe("record-level hooks materialize as field-level hooks per override key", () => { + it("emits a field-level hook for each overridden key and leaves untouched fields alone", () => { + const withRecordHooks = db + .type(["Hooked", "AllHooked"], { + name: db.string(), + fullAddress: db.string(), + ...db.fields.timestamps(), + }) + .hooks({ + create: ({ data }) => ({ fullAddress: data.name, createdAt: new Date() }), + update: ({ data }) => ({ fullAddress: data.name, updatedAt: new Date() }), + }); + + const result = parseTypes(toSchemaOutputs({ Hooked: withRecordHooks }), "test-namespace"); + + // No type-level hooks slot — the parsed TailorDBType only carries field-level hooks now. + expect((result.Hooked as unknown as { hooks?: unknown }).hooks).toBeUndefined(); + + // Overridden fields carry a field-level script that invokes the record-level + // hook and indexes out the key. + const createExpr = result.Hooked.fields.fullAddress.config.hooks?.create?.expr ?? ""; + expect(createExpr).toContain('"fullAddress"'); + expect(createExpr).toContain("({ data: _data, user:"); + + const updateExpr = result.Hooked.fields.fullAddress.config.hooks?.update?.expr ?? ""; + expect(updateExpr).toContain('"fullAddress"'); + + // Record-level hook overrides the auto-generated timestamp hook for the same key. + const createdAtExpr = result.Hooked.fields.createdAt.config.hooks?.create?.expr ?? ""; + expect(createdAtExpr).toContain('"createdAt"'); + // updatedAt only appears in the update hook + expect(result.Hooked.fields.createdAt.config.hooks?.update).toBeUndefined(); + + const updatedAtExpr = result.Hooked.fields.updatedAt.config.hooks?.update?.expr ?? ""; + expect(updatedAtExpr).toContain('"updatedAt"'); + + // Fields not in the override set keep no hook. + expect(result.Hooked.fields.name.config.hooks).toBeUndefined(); + }); + + it("keeps auto-generated timestamp hooks when the type has no record-level hooks", () => { + const withoutRecordHooks = db.type(["Plain", "AllPlain"], { + name: db.string(), + ...db.fields.timestamps(), + }); + + const result = parseTypes(toSchemaOutputs({ Plain: withoutRecordHooks }), "test-namespace"); + + expect((result.Plain as unknown as { hooks?: unknown }).hooks).toBeUndefined(); + expect(result.Plain.fields.createdAt.config.hooks?.create).toEqual({ + expr: "new Date()", + }); + expect(result.Plain.fields.updatedAt.config.hooks?.update).toEqual({ + expr: "new Date()", + }); + }); + + it("throws when a record-level hook overrides an unknown field", () => { + const bad = db + .type(["Bad", "AllBad"], { + name: db.string(), + }) + .hooks({ + // @ts-expect-error - intentionally overriding an unknown field to test runtime validation + create: () => ({ missingField: "x" }), + }); + + expect(() => parseTypes(toSchemaOutputs({ Bad: bad }), "test-namespace")).toThrow( + /overrides unknown field "missingField"/, + ); + }); + + it("throws when a record-level hook overrides the synthetic id field", () => { + const bad = db + .type(["Bad", "AllBad"], { + name: db.string(), + }) + .hooks({ + // @ts-expect-error - id is excluded from record-hook overrides + create: () => ({ id: "fixed-id" }), + }); + + expect(() => parseTypes(toSchemaOutputs({ Bad: bad }), "test-namespace")).toThrow( + /cannot override the synthetic "id" field/, + ); + }); + + it("throws when a record-level hook return value is not a static object literal", () => { + // Branched return: not a single static object literal — the AST extractor must reject this. + const bad = db + .type(["Bad", "AllBad"], { + name: db.string(), + }) + .hooks({ + create: ({ data }) => (data.name === "x" ? { name: "y" } : { name: "z" }), + }); + + expect(() => parseTypes(toSchemaOutputs({ Bad: bad }), "test-namespace")).toThrow( + /Record-level hook must return an object literal/, + ); + }); + + it("throws when a record-level hook uses a branched early-return inside an if statement", () => { + // Regression: `if (...) return X; return Y;` was previously accepted because + // the key extractor only counted top-level ReturnStatements, silently + // dropping the keys from the nested branch. + const bad = db + .type(["Bad", "AllBad"], { + name: db.string(), + fullAddress: db.string(), + }) + .hooks({ + create: ({ data }) => { + if (data.name === "x") { + return { name: "y" }; + } + return { fullAddress: "z" }; + }, + }); + + expect(() => parseTypes(toSchemaOutputs({ Bad: bad }), "test-namespace")).toThrow( + /Record-level hook must return a single object literal at the top level/, + ); + }); + + it("distributes a record-level hook with 3+ override keys to each field", () => { + const multi = db + .type(["Multi", "AllMulti"], { + a: db.string(), + b: db.string(), + c: db.string(), + d: db.string(), + }) + .hooks({ + create: ({ data }) => ({ a: data.b, b: data.c, c: data.d }), + }); + + const result = parseTypes(toSchemaOutputs({ Multi: multi }), "test-namespace"); + + for (const key of ["a", "b", "c"] as const) { + const expr = result.Multi.fields[key].config.hooks?.create?.expr ?? ""; + expect(expr).toContain(`"${key}"`); + expect(expr).toContain("({ data: _data, user:"); + } + // `d` is not overridden, so it carries no hook. + expect(result.Multi.fields.d.config.hooks).toBeUndefined(); + }); + + it("allows a record-level hook to override a relation field without breaking parsing", () => { + const user = db.type("RHRelUser", { name: db.string() }); + const post = db + .type("RHRelPost", { + title: db.string(), + authorId: db.uuid().relation({ + type: "n-1", + toward: { type: user }, + }), + }) + .hooks({ + // Overriding the relation field itself — the hook must materialize on it + // while keeping the relation metadata intact. + create: ({ data }) => ({ authorId: data.title }), + }); + + const result = parseTypes( + toSchemaOutputs({ RHRelUser: user, RHRelPost: post }), + "test-namespace", + ); + + const authorIdConfig = result.RHRelPost.fields.authorId.config; + const expr = authorIdConfig.hooks?.create?.expr ?? ""; + expect(expr).toContain('"authorId"'); + // Relation metadata must still be derived correctly. + expect(authorIdConfig.foreignKey).toBe(true); + expect(authorIdConfig.foreignKeyType).toBe("RHRelUser"); + expect(authorIdConfig.index).toBe(true); + }); + }); + + describe("record-level validators wrap into OperatorValidateConfig[]", () => { + it("emits a default 'failed by ...' message for function-only validators", () => { + const t = db + .type("ValFnOnly", { name: db.string() }) + .validate(({ data }) => data.name.length > 0); + + const result = parseTypes(toSchemaOutputs({ ValFnOnly: t }), "test-namespace"); + expect(result.ValFnOnly.validate).toHaveLength(1); + const [first] = result.ValFnOnly.validate ?? []; + expect(first?.errorMessage).toMatch(/^failed by `/); + // The wrapping turns the predicate into a `(pred) ? {} : { "_record_0": "" }` expression. + expect(first?.script.expr).toContain('"_record_0"'); + expect(first?.script.expr).toContain("? {} :"); + }); + + it("emits the explicit message for [fn, message] tuple validators", () => { + const t = db + .type("ValTuple", { name: db.string() }) + .validate([({ data }) => data.name.length > 0, "Name required"]); + + const result = parseTypes(toSchemaOutputs({ ValTuple: t }), "test-namespace"); + const [first] = result.ValTuple.validate ?? []; + expect(first?.errorMessage).toBe("Name required"); + expect(first?.script.expr).toContain('"Name required"'); + expect(first?.script.expr).toContain('"_record_0"'); + }); + + it("indexes mixed validator arrays as _record_ keys preserving order", () => { + const t = db + .type("ValMixed", { age: db.int() }) + .validate([ + ({ data }) => data.age >= 0, + [({ data }) => data.age < 200, "Age too high"], + ({ data }) => data.age !== 13, + ]); + + const result = parseTypes(toSchemaOutputs({ ValMixed: t }), "test-namespace"); + expect(result.ValMixed.validate).toHaveLength(3); + const [v0, v1, v2] = result.ValMixed.validate ?? []; + expect(v0?.script.expr).toContain('"_record_0"'); + expect(v0?.errorMessage).toMatch(/^failed by `/); + expect(v1?.script.expr).toContain('"_record_1"'); + expect(v1?.errorMessage).toBe("Age too high"); + expect(v2?.script.expr).toContain('"_record_2"'); + expect(v2?.errorMessage).toMatch(/^failed by `/); + }); + + it("does not emit a validate slot when no record-level validators are defined", () => { + const t = db.type("ValNone", { name: db.string() }); + const result = parseTypes(toSchemaOutputs({ ValNone: t }), "test-namespace"); + expect(result.ValNone.validate).toBeUndefined(); + }); + }); + + describe("precompiled script expressions for record-level scopes", () => { + it("uses precompiled expression for record-level hooks when attached", () => { + const createHook = ({ data }: { data: { name: string } }) => ({ name: data.name }); + setPrecompiledScriptExpr(createHook, "PRECOMPILED_RECORD_HOOK_EXPR"); + + const type = db.type("HookPrecomp", { name: db.string() }).hooks({ create: createHook }); + + const result = parseTypes(toSchemaOutputs({ HookPrecomp: type }), "test-namespace"); + // The emitted field-level hook wraps the precompiled invocation and indexes out the key. + expect(result.HookPrecomp.fields.name.config.hooks?.create?.expr).toBe( + '(PRECOMPILED_RECORD_HOOK_EXPR)["name"]', + ); + }); + + it("uses precompiled expression for record-level validators when attached", () => { + const validator = ({ data }: { data: { name: string } }) => data.name.length > 0; + setPrecompiledScriptExpr(validator, "PRECOMPILED_RECORD_VALIDATE_EXPR"); + + const type = db + .type("ValPrecomp", { name: db.string() }) + .validate([validator, "Name required"]); + + const result = parseTypes(toSchemaOutputs({ ValPrecomp: type }), "test-namespace"); + const [first] = result.ValPrecomp.validate ?? []; + expect(first?.script.expr).toContain("PRECOMPILED_RECORD_VALIDATE_EXPR"); + expect(first?.script.expr).toContain('"_record_0"'); + }); + }); + + describe("createTable: hooks and validate coexist after parsing", () => { + it("emits field-level hooks (from record-level hook) and parsed validators together", () => { + const combo = createTable( + "Combo", + { + name: { kind: "string" }, + fullAddress: { kind: "string" }, + }, + { + hooks: { + create: ({ data }) => ({ fullAddress: data.name }), + }, + validate: [ + ({ data }) => data.name.length > 0, + [({ data }) => data.fullAddress.length > 0, "fullAddress required"], + ], + }, + ); + + const result = parseTypes(toSchemaOutputs({ Combo: combo }), "test-namespace"); + + // Record-level hook → field-level hook on the overridden key. + const hookExpr = result.Combo.fields.fullAddress.config.hooks?.create?.expr ?? ""; + expect(hookExpr).toContain('"fullAddress"'); + // Untouched fields stay hook-free. + expect(result.Combo.fields.name.config.hooks).toBeUndefined(); + + // Record-level validators are wrapped into OperatorValidateConfig[]. + expect(result.Combo.validate).toHaveLength(2); + expect(result.Combo.validate?.[0]?.script.expr).toContain('"_record_0"'); + expect(result.Combo.validate?.[1]?.errorMessage).toBe("fullAddress required"); + }); + }); }); diff --git a/packages/sdk/src/parser/service/tailordb/type-parser.ts b/packages/sdk/src/parser/service/tailordb/type-parser.ts index dac0e0a65..46ef5ad92 100644 --- a/packages/sdk/src/parser/service/tailordb/type-parser.ts +++ b/packages/sdk/src/parser/service/tailordb/type-parser.ts @@ -1,7 +1,13 @@ import * as inflection from "inflection"; import { isPluginGeneratedType } from "@/types/tailordb"; -import { parseFieldConfig } from "./field"; +import { + compileScriptExpr, + normalizeValidatorEntry, + parseFieldConfig, + stringifyFunction, +} from "./field"; import { parsePermissions } from "./permission"; +import { extractRecordHookOverrideKeys } from "./record-hook-keys"; import { validateRelationConfig, processRelationMetadata, @@ -14,6 +20,7 @@ import type { ParsedField, ParsedRelationship, TailorDBType, + OperatorValidateConfig, } from "@/types/tailordb"; import type { TailorDBTypeRaw as TailorDBTypeSchemaOutput } from "@/types/tailordb.generated"; @@ -120,6 +127,13 @@ function parseTailorDBType( fields[fieldName] = parsedField; } + applyRecordHooksToFields(fields, metadata.hooks, type.name); + + const recordValidate = + metadata.validate && metadata.validate.length > 0 + ? convertRecordValidators(metadata.validate) + : undefined; + return { name: type.name, pluralForm, @@ -131,7 +145,110 @@ function parseTailorDBType( permissions: parsePermissions(metadata.permissions || {}), indexes: metadata.indexes, files: metadata.files, + ...(recordValidate && { validate: recordValidate }), + }; +} + +/** + * Build a field-level script expression that invokes a record-level hook and + * indexes out the value for one override key. The record map is read from + * `_data` (the field-level binding); the script result becomes the new value + * for the owning field. + * @param fn - Record-level hook function + * @param key - Override key to index out + * @returns JavaScript expression suitable for `FieldHook.create.expr` / `.update.expr` + */ +function buildRecordHookFieldExpr(fn: (...args: never[]) => unknown, key: string): string { + return `(${compileScriptExpr(fn, "recordHook")})[${JSON.stringify(key)}]`; +} + +/** + * Expand a record-level hook into per-field `hooks` entries on each overridden + * field. The platform's `type_hook` and field-level `hooks` are mutually + * exclusive at the wire level; emitting field-level hooks per override key + * lets the platform mark each populated field as optional in the auto-generated + * GraphQL input while preserving the record-level user API. + * + * **Caveat — single-execution semantics are not preserved.** The platform + * runs each emitted FieldHook independently, so a record-level hook that + * returns N keys is invoked N times at write time. Hooks must therefore be + * pure with respect to the returned values: `({ data }) => ({ a: data.x + 1, + * b: data.x * 2 })` is fine, but + * `({ data }) => { const id = crypto.randomUUID(); return { a: id, b: id }; }` + * produces two different ids for `a` and `b`. Until the platform supports + * true type-level hooks, side-effecting expressions (`crypto.randomUUID`, + * `new Date()`, `Math.random`, etc.) should be hoisted into individual + * field-level binding via the deprecated field API or computed by the caller + * before write. + * @param fields - Parsed fields keyed by field name (mutated in place) + * @param hooks - Record-level hook definitions, if any + * @param typeName - Type name (used for error messages) + */ +function applyRecordHooksToFields( + fields: Record, + hooks: NonNullable["hooks"], + typeName: string, +): void { + if (!hooks) return; + + const apply = (op: "create" | "update", fn: unknown): void => { + if (typeof fn !== "function") return; + const typedFn = fn as (...args: never[]) => unknown; + // eslint-disable-next-line @typescript-eslint/no-unsafe-function-type + const fnSource = stringifyFunction(typedFn as unknown as Function); + const keys = extractRecordHookOverrideKeys(fnSource); + for (const key of keys) { + if (key === "id") { + throw new Error( + `Record-level ${op} hook on type "${typeName}" cannot override the synthetic "id" field. ` + + "TailorDB owns id generation, and the deploy manifest strips id from field-level hooks.", + ); + } + const field = fields[key]; + if (!field) { + throw new Error( + `Record-level ${op} hook on type "${typeName}" overrides unknown field "${key}". ` + + "Override keys must match a field defined on the type.", + ); + } + const expr = buildRecordHookFieldExpr(typedFn, key); + field.config.hooks = { + ...(field.config.hooks ?? {}), + [op]: { expr }, + }; + } }; + + apply("create", hooks.create); + apply("update", hooks.update); +} + +/** + * Convert record-level validators to OperatorValidateConfig[]. + * The platform's type_validate script must return a map (`{ key: errorMessage }` + * on failure, `{}` on success). Each SDK-side boolean predicate is wrapped so + * the resulting expression contributes a `_record_` entry only when the + * predicate fails. Per-predicate expressions are merged later when emitting + * the proto manifest so a single failing validator surfaces its message + * without masking the others. + * @param validators - Record-level validator definitions + * @returns Parsed validate configs ready for the apply pipeline + */ +function convertRecordValidators( + validators: NonNullable, +): OperatorValidateConfig[] { + return validators.map((v, index) => { + const { fn, message } = normalizeValidatorEntry(v); + const predicate = compileScriptExpr(fn, "recordValidate"); + const key = `_record_${index}`; + const errorLiteral = JSON.stringify(message); + return { + script: { + expr: `((${predicate}) ? {} : { ${JSON.stringify(key)}: ${errorLiteral} })`, + }, + errorMessage: message, + }; + }); } /** diff --git a/packages/sdk/src/plugin/builtin/kysely-type/index.test.ts b/packages/sdk/src/plugin/builtin/kysely-type/index.test.ts index f13c27b23..c1605dfdf 100644 --- a/packages/sdk/src/plugin/builtin/kysely-type/index.test.ts +++ b/packages/sdk/src/plugin/builtin/kysely-type/index.test.ts @@ -81,7 +81,7 @@ describe("KyselyTypePlugin integration tests", () => { expect(result.typeDef).toContain("lastLogin: Timestamp | null;"); expect(result.typeDef).toContain("tags: string[];"); expect(result.typeDef).toContain("createdAt: Generated;"); - expect(result.typeDef).toContain("updatedAt: Timestamp | null;"); + expect(result.typeDef).toContain("updatedAt: Generated;"); }); it("should have correct id and description", () => { diff --git a/packages/sdk/src/plugin/builtin/kysely-type/type-processor.test.ts b/packages/sdk/src/plugin/builtin/kysely-type/type-processor.test.ts index 9637f144a..ac9f00694 100644 --- a/packages/sdk/src/plugin/builtin/kysely-type/type-processor.test.ts +++ b/packages/sdk/src/plugin/builtin/kysely-type/type-processor.test.ts @@ -284,7 +284,7 @@ describe("Kysely TypeProcessor", () => { expect(result.typeDef).toContain("UserWithTimestamp: {"); expect(result.typeDef).toContain("name: string"); expect(result.typeDef).toContain("createdAt: Generated;"); - expect(result.typeDef).toContain("updatedAt: Timestamp | null;"); + expect(result.typeDef).toContain("updatedAt: Generated;"); }); it("should always include Generated for id field", async () => { @@ -297,6 +297,20 @@ describe("Kysely TypeProcessor", () => { expect(result.typeDef).toContain("id: Generated;"); }); + it("does not wrap non-datetime generated fields in Generated", async () => { + // The deploy manifest does not send `generated` to the platform for + // non-datetime kinds, so the server still requires the value. Wrapping + // in Generated here would let inserts omit a required column. + const stringField = db.string(); + stringField._metadata.generated = true; + const type = db.type("User", { code: stringField }); + + const result = await processKyselyType(parseTailorDBType(toSchemaOutput(type))); + + expect(result.typeDef).toContain("code: string;"); + expect(result.typeDef).not.toContain("code: Generated<"); + }); + it("should correctly track used utility types - basic types only", async () => { const type = db.type("User", { name: db.string(), diff --git a/packages/sdk/src/plugin/builtin/kysely-type/type-processor.ts b/packages/sdk/src/plugin/builtin/kysely-type/type-processor.ts index bd245d22c..f4d100cba 100644 --- a/packages/sdk/src/plugin/builtin/kysely-type/type-processor.ts +++ b/packages/sdk/src/plugin/builtin/kysely-type/type-processor.ts @@ -149,7 +149,15 @@ function generateFieldType(fieldConfig: OperatorFieldConfig): FieldTypeResult { usedUtilityTypes.Serial = true; finalType = `Serial<${finalType}>`; } - if (fieldConfig.hooks?.create) { + // `generated` alone is only honored server-side for datetime fields (the + // parser synthesizes `new Date()` create/update hooks via metadata.generated). + // For non-datetime kinds the deploy manifest never sends `generated`, so the + // platform still requires the value — wrapping in Generated would let + // inserts omit a required column and fail at runtime. + const hasInsertGenerator = + fieldConfig.hooks?.create !== undefined || + (fieldConfig.generated === true && fieldConfig.type === "datetime"); + if (hasInsertGenerator) { finalType = `Generated<${finalType}>`; } diff --git a/packages/sdk/src/plugin/builtin/seed/lines-db-processor.ts b/packages/sdk/src/plugin/builtin/seed/lines-db-processor.ts index 4faf34ac2..2bee85a7a 100644 --- a/packages/sdk/src/plugin/builtin/seed/lines-db-processor.ts +++ b/packages/sdk/src/plugin/builtin/seed/lines-db-processor.ts @@ -126,6 +126,30 @@ function extractFieldMetadata(type: TailorDBType): { return { optionalFields, omitFields, indexes, foreignKeys }; } +/** + * Build the shared `defineSchema(createStandardSchema(...))` block. + * + * Every generated lines-db schema file ends with the same hook + schema export; + * extracting it keeps the per-source-kind branches focused on the differing + * import/binding lines. + * + * The returned string is plain (not dedented) so callers can splice it into + * outer `ml`-tagged templates at the placeholder position and have `ml` + * re-indent it consistently with the surrounding lines. + * @param exportName - The exported TailorDB type binding referenced by the schema + * @param schemaOptionsCode - Pre-rendered options object (foreign keys, indexes) or empty string + * @returns Code snippet to splice into the generated schema file + */ +function buildSchemaExportCode(exportName: string, schemaOptionsCode: string): string { + return [ + `const hook = createTailorDBHook(${exportName});`, + ``, + `export const schema = defineSchema(`, + ` createStandardSchema(schemaType, hook, ${exportName}.metadata?.validate),${schemaOptionsCode}`, + `);`, + ].join("\n"); +} + /** * Generate schema options code for lines-db * @param foreignKeys - Foreign key definitions @@ -185,11 +209,7 @@ export function generateLinesDbSchemaFile(metadata: LinesDbMetadata, importPath: ${schemaTypeCode} - const hook = createTailorDBHook(${exportName}); - - export const schema = defineSchema( - createStandardSchema(schemaType, hook),${schemaOptionsCode} - ); + ${buildSchemaExportCode(exportName, schemaOptionsCode)} `; } @@ -248,11 +268,7 @@ export function generateLinesDbSchemaFileWithPluginAPI( ${schemaTypeCode} - const hook = createTailorDBHook(${exportName}); - - export const schema = defineSchema( - createStandardSchema(schemaType, hook),${schemaOptionsCode} - ); + ${buildSchemaExportCode(exportName, schemaOptionsCode)} `; } @@ -277,11 +293,7 @@ export function generateLinesDbSchemaFileWithPluginAPI( ${schemaTypeCode} - const hook = createTailorDBHook(${exportName}); - - export const schema = defineSchema( - createStandardSchema(schemaType, hook),${schemaOptionsCode} - ); + ${buildSchemaExportCode(exportName, schemaOptionsCode)} `; } diff --git a/packages/sdk/src/types/tailordb.generated.ts b/packages/sdk/src/types/tailordb.generated.ts index 40984ceb8..15bd06cf9 100644 --- a/packages/sdk/src/types/tailordb.generated.ts +++ b/packages/sdk/src/types/tailordb.generated.ts @@ -68,6 +68,8 @@ export type DBFieldMetadata = { | undefined; /** Decimal scale (number of digits after decimal point, 0-12) */ scale?: number | undefined; + /** Whether the field value is auto-generated (e.g. timestamps) */ + generated?: boolean | undefined; }; export type DBFieldMetadataInput = DBFieldMetadata; @@ -1021,6 +1023,13 @@ export type TailorDBTypeRawInput = { }; } | undefined; + validate?: (Function | [Function, string])[] | undefined; + hooks?: + | { + create?: Function | undefined; + update?: Function | undefined; + } + | undefined; }; }; @@ -1062,6 +1071,7 @@ export type TailorDBTypeRaw = { } | undefined; scale?: number | undefined | undefined; + generated?: boolean | undefined | undefined; }; rawRelation?: | { @@ -1092,6 +1102,13 @@ export type TailorDBTypeRaw = { }; } | undefined; + validate?: (Function | [Function, string])[] | undefined; + hooks?: + | { + create?: Function | undefined; + update?: Function | undefined; + } + | undefined; }; }; diff --git a/packages/sdk/src/types/tailordb.ts b/packages/sdk/src/types/tailordb.ts index 0c2d0a136..7ea9caa12 100644 --- a/packages/sdk/src/types/tailordb.ts +++ b/packages/sdk/src/types/tailordb.ts @@ -46,6 +46,8 @@ export interface DBFieldMetadata extends FieldMetadata { serial?: SerialConfig; relation?: boolean; scale?: number; + /** Marks fields that the SDK fills automatically (e.g., timestamps). */ + generated?: boolean; } export interface DefinedDBFieldMetadata extends DefinedFieldMetadata { @@ -61,6 +63,7 @@ export interface DefinedDBFieldMetadata extends DefinedFieldMetadata { }; serial?: boolean; relation?: boolean; + generated?: boolean; } export type IndexDef }> = { @@ -130,12 +133,12 @@ export interface Script { expr: string; } -interface OperatorValidateConfig { +export interface OperatorValidateConfig { script: Script; errorMessage: string; } -interface OperatorFieldHook { +export interface OperatorFieldHook { create?: Script; update?: Script; } @@ -171,6 +174,7 @@ export interface OperatorFieldConfig { format?: string; }; scale?: number; + generated?: boolean; fields?: Record; } @@ -251,6 +255,20 @@ export interface TailorDBTypeMetadata { unique?: boolean; } >; + /** + * Record-level create/update hooks. Each returns an object listing the keys + * to override; the parser expands each override into a field-level `FieldHook` + * so the platform marks the affected fields as optional in GraphQL inputs. + */ + hooks?: { + // eslint-disable-next-line @typescript-eslint/no-unsafe-function-type + create?: Function; + // eslint-disable-next-line @typescript-eslint/no-unsafe-function-type + update?: Function; + }; + /** Record-level validators emitted to the platform as `type_validate`. */ + // eslint-disable-next-line @typescript-eslint/no-unsafe-function-type + validate?: (Function | [Function, string])[]; } export interface ParsedField { @@ -285,4 +303,6 @@ export interface TailorDBType { permissions: Permissions; indexes?: TailorDBTypeMetadata["indexes"]; files?: TailorDBTypeMetadata["files"]; + /** Record-level validators compiled to CEL expressions. */ + validate?: OperatorValidateConfig[]; } diff --git a/packages/sdk/src/types/validation.ts b/packages/sdk/src/types/validation.ts index 521e26520..f60def900 100644 --- a/packages/sdk/src/types/validation.ts +++ b/packages/sdk/src/types/validation.ts @@ -27,6 +27,27 @@ type FieldValidateConfig = ValidateConfig; */ export type FieldValidateInput = FieldValidateFn | FieldValidateConfig; +/** + * Record-level validation function. + * Receives the entire record `data` and returns `true` if valid. + */ +export type RecordValidateFn = (args: { data: TData; user: TailorUser }) => boolean; + +/** + * Record-level validation configuration with a custom error message. + */ +export type RecordValidateConfig = [RecordValidateFn, string]; + +/** + * Single record-level validation input: either a function or `[function, message]` tuple. + */ +export type RecordValidateInput = RecordValidateFn | RecordValidateConfig; + +/** + * Record-level validators: single input or an array of inputs. + */ +export type RecordValidators = RecordValidateInput | RecordValidateInput[]; + /** * Base validators type for field collections * @template F - Record of fields diff --git a/packages/sdk/src/utils/test/index.test.ts b/packages/sdk/src/utils/test/index.test.ts index 53a7dd6a6..ba2ce84e0 100644 --- a/packages/sdk/src/utils/test/index.test.ts +++ b/packages/sdk/src/utils/test/index.test.ts @@ -87,18 +87,6 @@ describe("createTailorDBHook", () => { const result = createTailorDBHook(type)({ nested: { name: "x" } }); expect((result.nested as { id: string }).id).toMatch(UUID_REGEX); }); - - it("invokes nested sub-field hooks", () => { - const type = db.type("Test", { - user: db.object({ - name: db.string().hooks({ - create: ({ value }) => `hooked:${value as string}`, - }), - }), - }); - const result = createTailorDBHook(type)({ user: { name: "alice" } }); - expect(result.user).toMatchObject({ name: "hooked:alice" }); - }); }); describe("nested object array field", () => { @@ -127,28 +115,6 @@ describe("createTailorDBHook", () => { expect((result.lines as { id: string }[])[1].id).toMatch(UUID_REGEX); }); - it("invokes per-element sub-field hooks", () => { - const calls: unknown[] = []; - const type = db.type("Test", { - lines: db.object( - { - stamp: db.string().hooks({ - create: ({ value }) => { - calls.push(value); - return `stamped:${value as string}`; - }, - }), - }, - { array: true }, - ), - }); - const result = createTailorDBHook(type)({ - lines: [{ stamp: "x" }, { stamp: "y" }], - }); - expect(calls).toEqual(["x", "y"]); - expect(result.lines).toEqual([{ stamp: "stamped:x" }, { stamp: "stamped:y" }]); - }); - it("preserves an empty array as an empty array", () => { const type = db.type("Test", { lines: db.object({ kind: db.string() }, { array: true }), @@ -183,50 +149,50 @@ describe("createTailorDBHook", () => { }); }); - describe("create hook on a top-level field", () => { - it("invokes the create hook with value, full data, and the unauthenticated user", () => { - const seen: { value: unknown; data: unknown; userId: string }[] = []; - const type = db.type("Order", { total: db.float(), tax: db.float() }).hooks({ - tax: { - create: ({ value, data, user }) => { - seen.push({ value, data, userId: user.id }); - return (data as { total: number }).total * 0.1; - }, - }, + describe("generated timestamp fields", () => { + it("fills required generated datetime (createdAt) with new Date when not supplied", () => { + const type = db.type("Test", { + name: db.string(), + ...db.fields.timestamps(), + }); + const result = createTailorDBHook(type)({ name: "alice" }); + expect(typeof result.createdAt).toBe("string"); + expect(() => new Date(result.createdAt as string).toISOString()).not.toThrow(); + }); + + it("preserves supplied createdAt value (lets tests pin timestamps deterministically)", () => { + const type = db.type("Test", { + name: db.string(), + ...db.fields.timestamps(), }); - const result = createTailorDBHook(type)({ total: 100, tax: undefined }); - expect(result.tax).toBe(10); - expect(seen).toEqual([ - { - value: undefined, - data: { total: 100, tax: undefined }, - userId: "00000000-0000-0000-0000-000000000000", - }, - ]); + const result = createTailorDBHook(type)({ + name: "alice", + createdAt: "2024-01-01T00:00:00.000Z", + }); + expect(result.createdAt).toBe("2024-01-01T00:00:00.000Z"); }); - it("normalizes a Date returned from the create hook to an ISO string", () => { - const fixed = new Date("2026-04-15T00:00:00.000Z"); - const type = db - .type("Test", { createdAt: db.datetime() }) - .hooks({ createdAt: { create: () => fixed } }); - expect(createTailorDBHook(type)({}).createdAt).toBe("2026-04-15T00:00:00.000Z"); + it("converts supplied Date instance to ISO string", () => { + const type = db.type("Test", { + name: db.string(), + ...db.fields.timestamps(), + }); + const result = createTailorDBHook(type)({ + name: "alice", + createdAt: new Date("2024-01-01T00:00:00.000Z"), + }); + expect(result.createdAt).toBe("2024-01-01T00:00:00.000Z"); }); - it("does not invoke a hook that only defines update (createTailorDBHook is create-only)", () => { - let updateCalled = false; - const type = db.type("Test", { updatedAt: db.datetime() }).hooks({ - updatedAt: { - update: () => { - updateCalled = true; - return new Date(); - }, - }, + it("does not fill optional generated datetime (updatedAt) on create", () => { + const type = db.type("Test", { + name: db.string(), + ...db.fields.timestamps(), }); - const result = createTailorDBHook(type)({ updatedAt: "2026-01-01T00:00:00.000Z" }); - expect(updateCalled).toBe(false); - // Falls through to plain passthrough - expect(result.updatedAt).toBe("2026-01-01T00:00:00.000Z"); + const result = createTailorDBHook(type)({ name: "alice" }); + // updatedAt's synthesized hook is on `update`, not `create` — the platform + // leaves it as the supplied value on create. Same here: undefined when omitted. + expect(result.updatedAt).toBeUndefined(); }); }); }); diff --git a/packages/sdk/src/utils/test/index.ts b/packages/sdk/src/utils/test/index.ts index 23667ca3a..6444d36d5 100644 --- a/packages/sdk/src/utils/test/index.ts +++ b/packages/sdk/src/utils/test/index.ts @@ -3,6 +3,12 @@ import type { TailorDBType } from "@/configure/services/tailordb/schema"; import type { TailorField } from "@/configure/types/type"; import type { StandardSchemaV1 } from "@standard-schema/spec"; +// Matches the public shape of `TailorDBTypeMetadata["validate"]` — kept loose +// here so generated seed code can forward `type.metadata?.validate` without +// extra type assertions. +// eslint-disable-next-line @typescript-eslint/no-unsafe-function-type +type LooseRecordValidator = Function | readonly [Function, string]; + export { WORKFLOW_TEST_ENV_KEY } from "@/configure/services/workflow/job"; export { setupTailordbMock, @@ -34,7 +40,7 @@ export const unauthenticatedTailorUser = { // eslint-disable-next-line @typescript-eslint/no-explicit-any export function createTailorDBHook>(type: T) { return (data: unknown) => { - return Object.entries(type.fields).reduce( + const result = Object.entries(type.fields).reduce( (hooked, [key, value]) => { // eslint-disable-next-line @typescript-eslint/no-explicit-any const field = value as TailorField; @@ -66,13 +72,45 @@ export function createTailorDBHook>(type: T) { if (hooked[key] instanceof Date) { hooked[key] = hooked[key].toISOString(); } + } else if ( + field.metadata.generated && + field.type === "datetime" && + field.metadata.required !== false + ) { + // Mirror the platform's synthesized create hook for required generated + // datetime fields (e.g. `createdAt` from `db.fields.timestamps()` / + // `timestampFields()`). The platform applies `new Date()` server-side + // via `metadata.generated`, but no `metadata.hooks.create` is set on + // the field — without this branch, seed/test records would leave the + // field empty and drift from deployed behavior. + const supplied = + data && typeof data === "object" ? (data as Record)[key] : undefined; + hooked[key] = + supplied instanceof Date + ? supplied.toISOString() + : (supplied ?? new Date().toISOString()); } else if (data && typeof data === "object") { hooked[key] = (data as Record)[key]; } return hooked; }, {} as Record, - ) as Partial>; + ); + + // Apply record-level hooks (e.g., computed fields like fullAddress). + // Hooks return only the fields to override; merge them onto the existing result. + const recordHook = type.metadata?.hooks?.create; + if (recordHook) { + const overrides = recordHook({ data: result, user: unauthenticatedTailorUser }) as Record< + string, + unknown + >; + for (const [key, val] of Object.entries(overrides)) { + result[key] = val instanceof Date ? val.toISOString() : val; + } + } + + return result as Partial>; }; } @@ -82,12 +120,17 @@ export function createTailorDBHook>(type: T) { * @template T - The output type after validation * @param schemaType - TailorDB field schema for validation * @param hook - Hook function to transform data before validation + * @param recordValidators - Optional record-level validators (from + * `type.metadata.validate`). Invoked after field-level validation passes so + * seed/test data is rejected with the same predicate the platform applies + * on the server. * @returns Schema object with ~standard section for defineSchema */ export function createStandardSchema>( // eslint-disable-next-line @typescript-eslint/no-explicit-any schemaType: TailorField, hook: (data: unknown) => Partial, + recordValidators?: readonly LooseRecordValidator[], ) { return { "~standard": { @@ -103,8 +146,41 @@ export function createStandardSchema>( if (result.issues) { return result; } + if (recordValidators && recordValidators.length > 0) { + const issues = runRecordValidators(recordValidators, hooked as T); + if (issues.length > 0) { + return { issues }; + } + } return { value: hooked as T }; }, }, } as const satisfies StandardSchemaV1; } + +type RecordValidatorFn = (args: { data: T; user: TailorUser }) => boolean; + +function isRecordValidatorTuple( + validator: LooseRecordValidator, + // eslint-disable-next-line @typescript-eslint/no-unsafe-function-type +): validator is readonly [Function, string] { + return Array.isArray(validator) && validator.length === 2 && typeof validator[1] === "string"; +} + +function runRecordValidators( + validators: readonly LooseRecordValidator[], + data: T, +): StandardSchemaV1.Issue[] { + const issues: StandardSchemaV1.Issue[] = []; + for (let i = 0; i < validators.length; i++) { + const validator = validators[i]; + const [rawFn, message] = isRecordValidatorTuple(validator) + ? [validator[0], validator[1]] + : [validator, `Record validator ${i} failed`]; + const fn = rawFn as RecordValidatorFn; + if (!fn({ data, user: unauthenticatedTailorUser })) { + issues.push({ message }); + } + } + return issues; +} diff --git a/packages/tailor-proto/src/tailor/v1/service_pb.d.ts b/packages/tailor-proto/src/tailor/v1/service_pb.d.ts index 67049e6c7..19b8579d8 100644 --- a/packages/tailor-proto/src/tailor/v1/service_pb.d.ts +++ b/packages/tailor-proto/src/tailor/v1/service_pb.d.ts @@ -4,7 +4,7 @@ import type { GenFile, GenMessage, GenService } from "@bufbuild/protobuf/codegenv2"; import type { Message } from "@bufbuild/protobuf"; -import type { AddOrganizationTeamMemberRequestSchema, AddOrganizationTeamMemberResponseSchema, CreateOrganizationFolderRequestSchema, CreateOrganizationFolderResponseSchema, CreateOrganizationTeamRequestSchema, CreateOrganizationTeamResponseSchema, CreateWorkspaceRequestSchema, CreateWorkspaceResponseSchema, DeleteOrganizationFolderRequestSchema, DeleteOrganizationFolderResponseSchema, DeleteOrganizationTeamRequestSchema, DeleteOrganizationTeamResponseSchema, DeleteWorkspaceRequestSchema, DeleteWorkspaceResponseSchema, GetOrganizationAccessRequestSchema, GetOrganizationAccessResponseSchema, GetOrganizationFolderAccessRequestSchema, GetOrganizationFolderAccessResponseSchema, GetOrganizationFolderRequestSchema, GetOrganizationFolderResponseSchema, GetOrganizationRequestSchema, GetOrganizationResponseSchema, GetOrganizationTeamMemberRequestSchema, GetOrganizationTeamMemberResponseSchema, GetOrganizationTeamRequestSchema, GetOrganizationTeamResponseSchema, GetPlatformAccountPlanRequestSchema, GetPlatformAccountPlanResponseSchema, GetWorkspacePlatformUserRequestSchema, GetWorkspacePlatformUserResponseSchema, GetWorkspaceRequestSchema, GetWorkspaceResponseSchema, GetWorkspaceRoleRequestSchema, GetWorkspaceRoleResponseSchema, GrantOrganizationAccessRequestSchema, GrantOrganizationAccessResponseSchema, GrantOrganizationFolderAccessRequestSchema, GrantOrganizationFolderAccessResponseSchema, InviteWorkspacePlatformUserRequestSchema, InviteWorkspacePlatformUserResponseSchema, ListAvailableWorkspacePlatformUserRolesRequestSchema, ListAvailableWorkspacePlatformUserRolesResponseSchema, ListAvailableWorkspaceRegionsRequestSchema, ListAvailableWorkspaceRegionsResponseSchema, ListOrganizationAccessesRequestSchema, ListOrganizationAccessesResponseSchema, ListOrganizationFolderAccessesRequestSchema, ListOrganizationFolderAccessesResponseSchema, ListOrganizationFoldersRequestSchema, ListOrganizationFoldersResponseSchema, ListOrganizationsRequestSchema, ListOrganizationsResponseSchema, ListOrganizationTeamMembersRequestSchema, ListOrganizationTeamMembersResponseSchema, ListOrganizationTeamsRequestSchema, ListOrganizationTeamsResponseSchema, ListOrganizationWorkspacesRequestSchema, ListOrganizationWorkspacesResponseSchema, ListUserOrganizationsRequestSchema, ListUserOrganizationsResponseSchema, ListWorkspacePlatformUsersRequestSchema, ListWorkspacePlatformUsersResponseSchema, ListWorkspacesRequestSchema, ListWorkspacesResponseSchema, RemoveOrganizationTeamMemberRequestSchema, RemoveOrganizationTeamMemberResponseSchema, RemoveWorkspacePlatformUserRequestSchema, RemoveWorkspacePlatformUserResponseSchema, RestoreWorkspaceRequestSchema, RestoreWorkspaceResponseSchema, RevokeOrganizationAccessRequestSchema, RevokeOrganizationAccessResponseSchema, RevokeOrganizationFolderAccessRequestSchema, RevokeOrganizationFolderAccessResponseSchema, UpdateOrganizationAccessRequestSchema, UpdateOrganizationAccessResponseSchema, UpdateOrganizationFolderAccessRequestSchema, UpdateOrganizationFolderAccessResponseSchema, UpdateOrganizationFolderRequestSchema, UpdateOrganizationFolderResponseSchema, UpdateOrganizationRequestSchema, UpdateOrganizationResponseSchema, UpdateOrganizationTeamMemberRequestSchema, UpdateOrganizationTeamMemberResponseSchema, UpdateOrganizationTeamRequestSchema, UpdateOrganizationTeamResponseSchema, UpdateWorkspacePlatformUserRequestSchema, UpdateWorkspacePlatformUserResponseSchema, UpdateWorkspaceRequestSchema, UpdateWorkspaceResponseSchema } from "./workspace_pb"; +import type { AddOrganizationTeamMemberRequestSchema, AddOrganizationTeamMemberResponseSchema, CreateOrganizationFolderRequestSchema, CreateOrganizationFolderResponseSchema, CreateOrganizationTeamRequestSchema, CreateOrganizationTeamResponseSchema, CreateWorkspaceRequestSchema, CreateWorkspaceResponseSchema, DeleteOrganizationFolderIPRestrictionRequestSchema, DeleteOrganizationFolderIPRestrictionResponseSchema, DeleteOrganizationFolderRequestSchema, DeleteOrganizationFolderResponseSchema, DeleteOrganizationIPRestrictionRequestSchema, DeleteOrganizationIPRestrictionResponseSchema, DeleteOrganizationTeamRequestSchema, DeleteOrganizationTeamResponseSchema, DeleteWorkspaceRequestSchema, DeleteWorkspaceResponseSchema, GetOrganizationAccessRequestSchema, GetOrganizationAccessResponseSchema, GetOrganizationFolderAccessRequestSchema, GetOrganizationFolderAccessResponseSchema, GetOrganizationFolderIPRestrictionRequestSchema, GetOrganizationFolderIPRestrictionResponseSchema, GetOrganizationFolderRequestSchema, GetOrganizationFolderResponseSchema, GetOrganizationIPRestrictionRequestSchema, GetOrganizationIPRestrictionResponseSchema, GetOrganizationRequestSchema, GetOrganizationResponseSchema, GetOrganizationTeamMemberRequestSchema, GetOrganizationTeamMemberResponseSchema, GetOrganizationTeamRequestSchema, GetOrganizationTeamResponseSchema, GetPlatformAccountPlanRequestSchema, GetPlatformAccountPlanResponseSchema, GetWorkspacePlatformUserRequestSchema, GetWorkspacePlatformUserResponseSchema, GetWorkspaceRequestSchema, GetWorkspaceResponseSchema, GetWorkspaceRoleRequestSchema, GetWorkspaceRoleResponseSchema, GrantOrganizationAccessRequestSchema, GrantOrganizationAccessResponseSchema, GrantOrganizationFolderAccessRequestSchema, GrantOrganizationFolderAccessResponseSchema, InviteWorkspacePlatformUserRequestSchema, InviteWorkspacePlatformUserResponseSchema, ListAvailableWorkspacePlatformUserRolesRequestSchema, ListAvailableWorkspacePlatformUserRolesResponseSchema, ListAvailableWorkspaceRegionsRequestSchema, ListAvailableWorkspaceRegionsResponseSchema, ListOrganizationAccessesRequestSchema, ListOrganizationAccessesResponseSchema, ListOrganizationFolderAccessesRequestSchema, ListOrganizationFolderAccessesResponseSchema, ListOrganizationFoldersRequestSchema, ListOrganizationFoldersResponseSchema, ListOrganizationsRequestSchema, ListOrganizationsResponseSchema, ListOrganizationTeamMembersRequestSchema, ListOrganizationTeamMembersResponseSchema, ListOrganizationTeamsRequestSchema, ListOrganizationTeamsResponseSchema, ListOrganizationWorkspacesRequestSchema, ListOrganizationWorkspacesResponseSchema, ListUserOrganizationsRequestSchema, ListUserOrganizationsResponseSchema, ListWorkspacePlatformUsersRequestSchema, ListWorkspacePlatformUsersResponseSchema, ListWorkspacesRequestSchema, ListWorkspacesResponseSchema, RemoveOrganizationTeamMemberRequestSchema, RemoveOrganizationTeamMemberResponseSchema, RemoveWorkspacePlatformUserRequestSchema, RemoveWorkspacePlatformUserResponseSchema, RestoreWorkspaceRequestSchema, RestoreWorkspaceResponseSchema, RevokeOrganizationAccessRequestSchema, RevokeOrganizationAccessResponseSchema, RevokeOrganizationFolderAccessRequestSchema, RevokeOrganizationFolderAccessResponseSchema, UpdateOrganizationAccessRequestSchema, UpdateOrganizationAccessResponseSchema, UpdateOrganizationFolderAccessRequestSchema, UpdateOrganizationFolderAccessResponseSchema, UpdateOrganizationFolderRequestSchema, UpdateOrganizationFolderResponseSchema, UpdateOrganizationRequestSchema, UpdateOrganizationResponseSchema, UpdateOrganizationTeamMemberRequestSchema, UpdateOrganizationTeamMemberResponseSchema, UpdateOrganizationTeamRequestSchema, UpdateOrganizationTeamResponseSchema, UpdateWorkspacePlatformUserRequestSchema, UpdateWorkspacePlatformUserResponseSchema, UpdateWorkspaceRequestSchema, UpdateWorkspaceResponseSchema, UpsertOrganizationFolderIPRestrictionRequestSchema, UpsertOrganizationFolderIPRestrictionResponseSchema, UpsertOrganizationIPRestrictionRequestSchema, UpsertOrganizationIPRestrictionResponseSchema } from "./workspace_pb"; import type { CreateApplicationRequestSchema, CreateApplicationResponseSchema, DeleteApplicationRequestSchema, DeleteApplicationResponseSchema, GetApplicationRequestSchema, GetApplicationResponseSchema, GetApplicationSchemaHealthRequestSchema, GetApplicationSchemaHealthResponseSchema, ListApplicationsRequestSchema, ListApplicationsResponseSchema, UpdateApplicationRequestSchema, UpdateApplicationResponseSchema } from "./application_pb"; import type { ComposeTailorDBSDLRequestSchema, ComposeTailorDBSDLResponseSchema, CreateTailorDBGQLPermissionRequestSchema, CreateTailorDBGQLPermissionResponseSchema, CreateTailorDBServiceRequestSchema, CreateTailorDBServiceResponseSchema, CreateTailorDBTypeRequestSchema, CreateTailorDBTypeResponseSchema, DeleteTailorDBGQLPermissionRequestSchema, DeleteTailorDBGQLPermissionResponseSchema, DeleteTailorDBServiceRequestSchema, DeleteTailorDBServiceResponseSchema, DeleteTailorDBTypeRequestSchema, DeleteTailorDBTypeResponseSchema, GetTailorDBGQLPermissionRequestSchema, GetTailorDBGQLPermissionResponseSchema, GetTailorDBServiceRequestSchema, GetTailorDBServiceResponseSchema, GetTailorDBTypeRequestSchema, GetTailorDBTypeResponseSchema, ListTailorDBGQLPermissionsRequestSchema, ListTailorDBGQLPermissionsResponseSchema, ListTailorDBServicesRequestSchema, ListTailorDBServicesResponseSchema, ListTailorDBTypesRequestSchema, ListTailorDBTypesResponseSchema, TruncateTailorDBTypeRequestSchema, TruncateTailorDBTypeResponseSchema, TruncateTailorDBTypesRequestSchema, TruncateTailorDBTypesResponseSchema, UpdateTailorDBGQLPermissionRequestSchema, UpdateTailorDBGQLPermissionResponseSchema, UpdateTailorDBServiceRequestSchema, UpdateTailorDBServiceResponseSchema, UpdateTailorDBTypeRequestSchema, UpdateTailorDBTypeResponseSchema } from "./tailordb_pb"; import type { ComposePipelineSDLRequestSchema, ComposePipelineSDLResponseSchema, CreatePipelineResolverRequestSchema, CreatePipelineResolverResponseSchema, CreatePipelineServiceRequestSchema, CreatePipelineServiceResponseSchema, DeletePipelineResolverRequestSchema, DeletePipelineResolverResponseSchema, DeletePipelineServiceRequestSchema, DeletePipelineServiceResponseSchema, GetPipelineResolverExecutionResultRequestSchema, GetPipelineResolverExecutionResultResponseSchema, GetPipelineResolverRequestSchema, GetPipelineResolverResponseSchema, GetPipelineServiceRequestSchema, GetPipelineServiceResponseSchema, ListPipelineResolverExecutionResultsRequestSchema, ListPipelineResolverExecutionResultsResponseSchema, ListPipelineResolversRequestSchema, ListPipelineResolversResponseSchema, ListPipelineServicesRequestSchema, ListPipelineServicesResponseSchema, RestartPipelineResolverRequestSchema, RestartPipelineResolverResponseSchema, UpdatePipelineResolverRequestSchema, UpdatePipelineResolverResponseSchema, UpdatePipelineServiceRequestSchema, UpdatePipelineServiceResponseSchema } from "./pipeline_pb"; @@ -17,7 +17,7 @@ import type { GetFunctionExecutionRequestSchema, GetFunctionExecutionResponseSch import type { CreateFunctionRegistryRequestSchema, CreateFunctionRegistryResponseSchema, DeleteFunctionRegistryRequestSchema, DeleteFunctionRegistryResponseSchema, DownloadFunctionRegistryScriptRequestSchema, DownloadFunctionRegistryScriptResponseSchema, GetFunctionRegistryRequestSchema, GetFunctionRegistryResponseSchema, ListFunctionRegistriesRequestSchema, ListFunctionRegistriesResponseSchema, UpdateFunctionRegistryRequestSchema, UpdateFunctionRegistryResponseSchema } from "./function_registry_pb"; import type { ListMeterEventCountsRequestSchema, ListMeterEventCountsResponseSchema, ListMeterExecutionCountsRequestSchema, ListMeterExecutionCountsResponseSchema, ListMeterRequestCountsRequestSchema, ListMeterRequestCountsResponseSchema } from "./meter_pb"; import type { CreateIdPClientRequestSchema, CreateIdPClientResponseSchema, CreateIdPServiceRequestSchema, CreateIdPServiceResponseSchema, DeleteIdPClientRequestSchema, DeleteIdPClientResponseSchema, DeleteIdPServiceRequestSchema, DeleteIdPServiceResponseSchema, GetIdPClientRequestSchema, GetIdPClientResponseSchema, GetIdPServiceRequestSchema, GetIdPServiceResponseSchema, ListIdPClientsRequestSchema, ListIdPClientsResponseSchema, ListIdPServicesRequestSchema, ListIdPServicesResponseSchema, UpdateIdPServiceRequestSchema, UpdateIdPServiceResponseSchema } from "./idp_pb"; -import type { CreateDeploymentRequestSchema, CreateDeploymentResponseSchema, CreateStaticWebsiteRequestSchema, CreateStaticWebsiteResponseSchema, DeleteStaticWebsiteRequestSchema, DeleteStaticWebsiteResponseSchema, GetStaticWebsiteRequestSchema, GetStaticWebsiteResponseSchema, ListStaticWebsitesRequestSchema, ListStaticWebsitesResponseSchema, PublishDeploymentRequestSchema, PublishDeploymentResponseSchema, UpdateStaticWebsiteRequestSchema, UpdateStaticWebsiteResponseSchema, UploadFileRequestSchema, UploadFileResponseSchema } from "./staticwebsite_pb"; +import type { AddCustomDomainRequestSchema, AddCustomDomainResponseSchema, CreateDeploymentRequestSchema, CreateDeploymentResponseSchema, CreateStaticWebsiteRequestSchema, CreateStaticWebsiteResponseSchema, DeleteStaticWebsiteRequestSchema, DeleteStaticWebsiteResponseSchema, GetCustomDomainRequestSchema, GetCustomDomainResponseSchema, GetStaticWebsiteRequestSchema, GetStaticWebsiteResponseSchema, ListCustomDomainsRequestSchema, ListCustomDomainsResponseSchema, ListStaticWebsitesRequestSchema, ListStaticWebsitesResponseSchema, PublishDeploymentRequestSchema, PublishDeploymentResponseSchema, RemoveCustomDomainRequestSchema, RemoveCustomDomainResponseSchema, UpdateStaticWebsiteRequestSchema, UpdateStaticWebsiteResponseSchema, UploadFileRequestSchema, UploadFileResponseSchema } from "./staticwebsite_pb"; import type { CreateWorkflowJobFunctionRequestSchema, CreateWorkflowJobFunctionResponseSchema, CreateWorkflowRequestSchema, CreateWorkflowResponseSchema, DeleteWorkflowRequestSchema, DeleteWorkflowResponseSchema, GetWorkflowByNameRequestSchema, GetWorkflowByNameResponseSchema, GetWorkflowExecutionRequestSchema, GetWorkflowExecutionResponseSchema, GetWorkflowJobFunctionByNameRequestSchema, GetWorkflowJobFunctionByNameResponseSchema, GetWorkflowJobFunctionRequestSchema, GetWorkflowJobFunctionResponseSchema, GetWorkflowRequestSchema, GetWorkflowResponseSchema, ListWorkflowExecutionsRequestSchema, ListWorkflowExecutionsResponseSchema, ListWorkflowJobFunctionsRequestSchema, ListWorkflowJobFunctionsResponseSchema, ListWorkflowsRequestSchema, ListWorkflowsResponseSchema, TestResumeWorkflowRequestSchema, TestResumeWorkflowResponseSchema, TestStartWorkflowRequestSchema, TestStartWorkflowResponseSchema, UpdateWorkflowJobFunctionRequestSchema, UpdateWorkflowJobFunctionResponseSchema, UpdateWorkflowRequestSchema, UpdateWorkflowResponseSchema } from "./workflow_pb"; import type { GetMetadataRequestSchema, GetMetadataResponseSchema, SetMetadataRequestSchema, SetMetadataResponseSchema } from "./metadata_pb"; import type { CreateResourceAttributesConfigRequestSchema, CreateResourceAttributesConfigResponseSchema, CreateTelemetryExportRequestSchema, CreateTelemetryExportResponseSchema, DeleteResourceAttributesConfigRequestSchema, DeleteResourceAttributesConfigResponseSchema, DeleteTelemetryExportRequestSchema, DeleteTelemetryExportResponseSchema, GetResourceAttributesConfigRequestSchema, GetResourceAttributesConfigResponseSchema, GetTelemetryExportRequestSchema, GetTelemetryExportResponseSchema, ListTelemetryExportsRequestSchema, ListTelemetryExportsResponseSchema, TestTelemetryExportRequestSchema, TestTelemetryExportResponseSchema, UpdateResourceAttributesConfigRequestSchema, UpdateResourceAttributesConfigResponseSchema, UpdateTelemetryExportRequestSchema, UpdateTelemetryExportResponseSchema } from "./telemetryrouter_pb"; @@ -576,6 +576,109 @@ export declare const OperatorService: GenService<{ input: typeof GetOrganizationFolderAccessRequestSchema; output: typeof GetOrganizationFolderAccessResponseSchema; }, + /** + * UpsertOrganizationIPRestriction creates or replaces the IP allowlist + * applied to all inbound dataplane traffic for an organization. + * + * [Errors] + * - Unauthenticated: token is missing, expired, or invalid + * - InvalidArgument: request is invalid (malformed CIDR, private / + * loopback / multicast address, etc.) + * - PermissionDenied: caller does not have permission on the organization + * - NotFound: organization does not exist or can not be accessed + * + * @generated from rpc tailor.v1.OperatorService.UpsertOrganizationIPRestriction + */ + upsertOrganizationIPRestriction: { + methodKind: "unary"; + input: typeof UpsertOrganizationIPRestrictionRequestSchema; + output: typeof UpsertOrganizationIPRestrictionResponseSchema; + }, + /** + * GetOrganizationIPRestriction returns the current IP allowlist for an + * organization, or NotFound if no rule is configured. + * + * [Errors] + * - Unauthenticated: token is missing, expired, or invalid + * - InvalidArgument: request is invalid + * - PermissionDenied: caller does not have permission on the organization + * - NotFound: no IP restriction is configured for the organization + * + * @generated from rpc tailor.v1.OperatorService.GetOrganizationIPRestriction + */ + getOrganizationIPRestriction: { + methodKind: "unary"; + input: typeof GetOrganizationIPRestrictionRequestSchema; + output: typeof GetOrganizationIPRestrictionResponseSchema; + }, + /** + * DeleteOrganizationIPRestriction removes the IP allowlist for an + * organization. NotFound is returned if no rule is configured. + * + * [Errors] + * - Unauthenticated: token is missing, expired, or invalid + * - InvalidArgument: request is invalid + * - PermissionDenied: caller does not have permission on the organization + * - NotFound: no IP restriction is configured for the organization + * + * @generated from rpc tailor.v1.OperatorService.DeleteOrganizationIPRestriction + */ + deleteOrganizationIPRestriction: { + methodKind: "unary"; + input: typeof DeleteOrganizationIPRestrictionRequestSchema; + output: typeof DeleteOrganizationIPRestrictionResponseSchema; + }, + /** + * UpsertOrganizationFolderIPRestriction creates or replaces the IP + * allowlist applied to all inbound dataplane traffic for a folder. + * + * [Errors] + * - Unauthenticated: token is missing, expired, or invalid + * - InvalidArgument: request is invalid + * - PermissionDenied: caller does not have permission on the folder + * - NotFound: folder does not exist or can not be accessed + * + * @generated from rpc tailor.v1.OperatorService.UpsertOrganizationFolderIPRestriction + */ + upsertOrganizationFolderIPRestriction: { + methodKind: "unary"; + input: typeof UpsertOrganizationFolderIPRestrictionRequestSchema; + output: typeof UpsertOrganizationFolderIPRestrictionResponseSchema; + }, + /** + * GetOrganizationFolderIPRestriction returns the current IP allowlist + * for a folder, or NotFound if no rule is configured. + * + * [Errors] + * - Unauthenticated: token is missing, expired, or invalid + * - InvalidArgument: request is invalid + * - PermissionDenied: caller does not have permission on the folder + * - NotFound: no IP restriction is configured for the folder + * + * @generated from rpc tailor.v1.OperatorService.GetOrganizationFolderIPRestriction + */ + getOrganizationFolderIPRestriction: { + methodKind: "unary"; + input: typeof GetOrganizationFolderIPRestrictionRequestSchema; + output: typeof GetOrganizationFolderIPRestrictionResponseSchema; + }, + /** + * DeleteOrganizationFolderIPRestriction removes the IP allowlist for a + * folder. NotFound is returned if no rule is configured. + * + * [Errors] + * - Unauthenticated: token is missing, expired, or invalid + * - InvalidArgument: request is invalid + * - PermissionDenied: caller does not have permission on the folder + * - NotFound: no IP restriction is configured for the folder + * + * @generated from rpc tailor.v1.OperatorService.DeleteOrganizationFolderIPRestriction + */ + deleteOrganizationFolderIPRestriction: { + methodKind: "unary"; + input: typeof DeleteOrganizationFolderIPRestrictionRequestSchema; + output: typeof DeleteOrganizationFolderIPRestrictionResponseSchema; + }, /** * CreateOrganizationTeam creates a team in an organization. * @@ -2644,6 +2747,68 @@ export declare const OperatorService: GenService<{ input: typeof ListStaticWebsitesRequestSchema; output: typeof ListStaticWebsitesResponseSchema; }, + /** + * AddCustomDomain registers a custom domain for a static website. + * + * [Errors] + * - Unauthenticated: token is missing, expired, or invalid + * - InvalidArgument: request is invalid + * - NotFound: static website does not exist + * - AlreadyExists: domain is already registered + * - ResourceExhausted: maximum number of custom domains reached + * + * @generated from rpc tailor.v1.OperatorService.AddCustomDomain + */ + addCustomDomain: { + methodKind: "unary"; + input: typeof AddCustomDomainRequestSchema; + output: typeof AddCustomDomainResponseSchema; + }, + /** + * GetCustomDomain gets a custom domain. + * + * [Errors] + * - Unauthenticated: token is missing, expired, or invalid + * - InvalidArgument: request is invalid + * - NotFound: custom domain does not exist + * + * @generated from rpc tailor.v1.OperatorService.GetCustomDomain + */ + getCustomDomain: { + methodKind: "unary"; + input: typeof GetCustomDomainRequestSchema; + output: typeof GetCustomDomainResponseSchema; + }, + /** + * ListCustomDomains lists custom domains for a static website. + * + * [Errors] + * - Unauthenticated: token is missing, expired, or invalid + * - InvalidArgument: request is invalid + * - NotFound: static website does not exist + * + * @generated from rpc tailor.v1.OperatorService.ListCustomDomains + */ + listCustomDomains: { + methodKind: "unary"; + input: typeof ListCustomDomainsRequestSchema; + output: typeof ListCustomDomainsResponseSchema; + }, + /** + * RemoveCustomDomain removes a custom domain. + * + * [Errors] + * - Unauthenticated: token is missing, expired, or invalid + * - InvalidArgument: request is invalid + * - NotFound: custom domain does not exist + * + * @generated from rpc tailor.v1.OperatorService.RemoveCustomDomain + */ + removeCustomDomain: { + methodKind: "unary"; + input: typeof RemoveCustomDomainRequestSchema; + output: typeof RemoveCustomDomainResponseSchema; + }, /** * CreateDeployment creates a new deployment. * diff --git a/packages/tailor-proto/src/tailor/v1/service_pb.js b/packages/tailor-proto/src/tailor/v1/service_pb.js index 38d5353c7..bb2b41f9e 100644 --- a/packages/tailor-proto/src/tailor/v1/service_pb.js +++ b/packages/tailor-proto/src/tailor/v1/service_pb.js @@ -25,7 +25,7 @@ import { file_tailor_v1_workspace } from "./workspace_pb"; * Describes the file tailor/v1/service.proto. */ export const file_tailor_v1_service = /*@__PURE__*/ - fileDesc("Chd0YWlsb3IvdjEvc2VydmljZS5wcm90bxIJdGFpbG9yLnYxIg0KC1BpbmdSZXF1ZXN0Ig4KDFBpbmdSZXNwb25zZTKdwQEKD09wZXJhdG9yU2VydmljZRI5CgRQaW5nEhYudGFpbG9yLnYxLlBpbmdSZXF1ZXN0GhcudGFpbG9yLnYxLlBpbmdSZXNwb25zZSIAEocBCh1MaXN0QXZhaWxhYmxlV29ya3NwYWNlUmVnaW9ucxIvLnRhaWxvci52MS5MaXN0QXZhaWxhYmxlV29ya3NwYWNlUmVnaW9uc1JlcXVlc3QaMC50YWlsb3IudjEuTGlzdEF2YWlsYWJsZVdvcmtzcGFjZVJlZ2lvbnNSZXNwb25zZSIDkAIBEloKD0NyZWF0ZVdvcmtzcGFjZRIhLnRhaWxvci52MS5DcmVhdGVXb3Jrc3BhY2VSZXF1ZXN0GiIudGFpbG9yLnYxLkNyZWF0ZVdvcmtzcGFjZVJlc3BvbnNlIgASWgoPVXBkYXRlV29ya3NwYWNlEiEudGFpbG9yLnYxLlVwZGF0ZVdvcmtzcGFjZVJlcXVlc3QaIi50YWlsb3IudjEuVXBkYXRlV29ya3NwYWNlUmVzcG9uc2UiABJaCg9EZWxldGVXb3Jrc3BhY2USIS50YWlsb3IudjEuRGVsZXRlV29ya3NwYWNlUmVxdWVzdBoiLnRhaWxvci52MS5EZWxldGVXb3Jrc3BhY2VSZXNwb25zZSIAEloKDkxpc3RXb3Jrc3BhY2VzEiAudGFpbG9yLnYxLkxpc3RXb3Jrc3BhY2VzUmVxdWVzdBohLnRhaWxvci52MS5MaXN0V29ya3NwYWNlc1Jlc3BvbnNlIgOQAgESfgoaTGlzdE9yZ2FuaXphdGlvbldvcmtzcGFjZXMSLC50YWlsb3IudjEuTGlzdE9yZ2FuaXphdGlvbldvcmtzcGFjZXNSZXF1ZXN0Gi0udGFpbG9yLnYxLkxpc3RPcmdhbml6YXRpb25Xb3Jrc3BhY2VzUmVzcG9uc2UiA5ACARJdChBSZXN0b3JlV29ya3NwYWNlEiIudGFpbG9yLnYxLlJlc3RvcmVXb3Jrc3BhY2VSZXF1ZXN0GiMudGFpbG9yLnYxLlJlc3RvcmVXb3Jrc3BhY2VSZXNwb25zZSIAElQKDEdldFdvcmtzcGFjZRIeLnRhaWxvci52MS5HZXRXb3Jrc3BhY2VSZXF1ZXN0Gh8udGFpbG9yLnYxLkdldFdvcmtzcGFjZVJlc3BvbnNlIgOQAgESfgoaTGlzdFdvcmtzcGFjZVBsYXRmb3JtVXNlcnMSLC50YWlsb3IudjEuTGlzdFdvcmtzcGFjZVBsYXRmb3JtVXNlcnNSZXF1ZXN0Gi0udGFpbG9yLnYxLkxpc3RXb3Jrc3BhY2VQbGF0Zm9ybVVzZXJzUmVzcG9uc2UiA5ACARKlAQonTGlzdEF2YWlsYWJsZVdvcmtzcGFjZVBsYXRmb3JtVXNlclJvbGVzEjkudGFpbG9yLnYxLkxpc3RBdmFpbGFibGVXb3Jrc3BhY2VQbGF0Zm9ybVVzZXJSb2xlc1JlcXVlc3QaOi50YWlsb3IudjEuTGlzdEF2YWlsYWJsZVdvcmtzcGFjZVBsYXRmb3JtVXNlclJvbGVzUmVzcG9uc2UiA5ACARJ+ChtJbnZpdGVXb3Jrc3BhY2VQbGF0Zm9ybVVzZXISLS50YWlsb3IudjEuSW52aXRlV29ya3NwYWNlUGxhdGZvcm1Vc2VyUmVxdWVzdBouLnRhaWxvci52MS5JbnZpdGVXb3Jrc3BhY2VQbGF0Zm9ybVVzZXJSZXNwb25zZSIAEn4KG1JlbW92ZVdvcmtzcGFjZVBsYXRmb3JtVXNlchItLnRhaWxvci52MS5SZW1vdmVXb3Jrc3BhY2VQbGF0Zm9ybVVzZXJSZXF1ZXN0Gi4udGFpbG9yLnYxLlJlbW92ZVdvcmtzcGFjZVBsYXRmb3JtVXNlclJlc3BvbnNlIgASfgobVXBkYXRlV29ya3NwYWNlUGxhdGZvcm1Vc2VyEi0udGFpbG9yLnYxLlVwZGF0ZVdvcmtzcGFjZVBsYXRmb3JtVXNlclJlcXVlc3QaLi50YWlsb3IudjEuVXBkYXRlV29ya3NwYWNlUGxhdGZvcm1Vc2VyUmVzcG9uc2UiABJ4ChhHZXRXb3Jrc3BhY2VQbGF0Zm9ybVVzZXISKi50YWlsb3IudjEuR2V0V29ya3NwYWNlUGxhdGZvcm1Vc2VyUmVxdWVzdBorLnRhaWxvci52MS5HZXRXb3Jrc3BhY2VQbGF0Zm9ybVVzZXJSZXNwb25zZSIDkAIBEmAKEEdldFdvcmtzcGFjZVJvbGUSIi50YWlsb3IudjEuR2V0V29ya3NwYWNlUm9sZVJlcXVlc3QaIy50YWlsb3IudjEuR2V0V29ya3NwYWNlUm9sZVJlc3BvbnNlIgOQAgESYwoSVXBkYXRlT3JnYW5pemF0aW9uEiQudGFpbG9yLnYxLlVwZGF0ZU9yZ2FuaXphdGlvblJlcXVlc3QaJS50YWlsb3IudjEuVXBkYXRlT3JnYW5pemF0aW9uUmVzcG9uc2UiABJdCg9HZXRPcmdhbml6YXRpb24SIS50YWlsb3IudjEuR2V0T3JnYW5pemF0aW9uUmVxdWVzdBoiLnRhaWxvci52MS5HZXRPcmdhbml6YXRpb25SZXNwb25zZSIDkAIBEmMKEUxpc3RPcmdhbml6YXRpb25zEiMudGFpbG9yLnYxLkxpc3RPcmdhbml6YXRpb25zUmVxdWVzdBokLnRhaWxvci52MS5MaXN0T3JnYW5pemF0aW9uc1Jlc3BvbnNlIgOQAgESbwoVTGlzdFVzZXJPcmdhbml6YXRpb25zEicudGFpbG9yLnYxLkxpc3RVc2VyT3JnYW5pemF0aW9uc1JlcXVlc3QaKC50YWlsb3IudjEuTGlzdFVzZXJPcmdhbml6YXRpb25zUmVzcG9uc2UiA5ACARJyChdHcmFudE9yZ2FuaXphdGlvbkFjY2VzcxIpLnRhaWxvci52MS5HcmFudE9yZ2FuaXphdGlvbkFjY2Vzc1JlcXVlc3QaKi50YWlsb3IudjEuR3JhbnRPcmdhbml6YXRpb25BY2Nlc3NSZXNwb25zZSIAEnUKGFVwZGF0ZU9yZ2FuaXphdGlvbkFjY2VzcxIqLnRhaWxvci52MS5VcGRhdGVPcmdhbml6YXRpb25BY2Nlc3NSZXF1ZXN0GisudGFpbG9yLnYxLlVwZGF0ZU9yZ2FuaXphdGlvbkFjY2Vzc1Jlc3BvbnNlIgASdQoYUmV2b2tlT3JnYW5pemF0aW9uQWNjZXNzEioudGFpbG9yLnYxLlJldm9rZU9yZ2FuaXphdGlvbkFjY2Vzc1JlcXVlc3QaKy50YWlsb3IudjEuUmV2b2tlT3JnYW5pemF0aW9uQWNjZXNzUmVzcG9uc2UiABJ4ChhMaXN0T3JnYW5pemF0aW9uQWNjZXNzZXMSKi50YWlsb3IudjEuTGlzdE9yZ2FuaXphdGlvbkFjY2Vzc2VzUmVxdWVzdBorLnRhaWxvci52MS5MaXN0T3JnYW5pemF0aW9uQWNjZXNzZXNSZXNwb25zZSIDkAIBEm8KFUdldE9yZ2FuaXphdGlvbkFjY2VzcxInLnRhaWxvci52MS5HZXRPcmdhbml6YXRpb25BY2Nlc3NSZXF1ZXN0GigudGFpbG9yLnYxLkdldE9yZ2FuaXphdGlvbkFjY2Vzc1Jlc3BvbnNlIgOQAgESdQoYQ3JlYXRlT3JnYW5pemF0aW9uRm9sZGVyEioudGFpbG9yLnYxLkNyZWF0ZU9yZ2FuaXphdGlvbkZvbGRlclJlcXVlc3QaKy50YWlsb3IudjEuQ3JlYXRlT3JnYW5pemF0aW9uRm9sZGVyUmVzcG9uc2UiABJ1ChhVcGRhdGVPcmdhbml6YXRpb25Gb2xkZXISKi50YWlsb3IudjEuVXBkYXRlT3JnYW5pemF0aW9uRm9sZGVyUmVxdWVzdBorLnRhaWxvci52MS5VcGRhdGVPcmdhbml6YXRpb25Gb2xkZXJSZXNwb25zZSIAEnUKGERlbGV0ZU9yZ2FuaXphdGlvbkZvbGRlchIqLnRhaWxvci52MS5EZWxldGVPcmdhbml6YXRpb25Gb2xkZXJSZXF1ZXN0GisudGFpbG9yLnYxLkRlbGV0ZU9yZ2FuaXphdGlvbkZvbGRlclJlc3BvbnNlIgASbwoVR2V0T3JnYW5pemF0aW9uRm9sZGVyEicudGFpbG9yLnYxLkdldE9yZ2FuaXphdGlvbkZvbGRlclJlcXVlc3QaKC50YWlsb3IudjEuR2V0T3JnYW5pemF0aW9uRm9sZGVyUmVzcG9uc2UiA5ACARJ1ChdMaXN0T3JnYW5pemF0aW9uRm9sZGVycxIpLnRhaWxvci52MS5MaXN0T3JnYW5pemF0aW9uRm9sZGVyc1JlcXVlc3QaKi50YWlsb3IudjEuTGlzdE9yZ2FuaXphdGlvbkZvbGRlcnNSZXNwb25zZSIDkAIBEoQBCh1HcmFudE9yZ2FuaXphdGlvbkZvbGRlckFjY2VzcxIvLnRhaWxvci52MS5HcmFudE9yZ2FuaXphdGlvbkZvbGRlckFjY2Vzc1JlcXVlc3QaMC50YWlsb3IudjEuR3JhbnRPcmdhbml6YXRpb25Gb2xkZXJBY2Nlc3NSZXNwb25zZSIAEocBCh5VcGRhdGVPcmdhbml6YXRpb25Gb2xkZXJBY2Nlc3MSMC50YWlsb3IudjEuVXBkYXRlT3JnYW5pemF0aW9uRm9sZGVyQWNjZXNzUmVxdWVzdBoxLnRhaWxvci52MS5VcGRhdGVPcmdhbml6YXRpb25Gb2xkZXJBY2Nlc3NSZXNwb25zZSIAEocBCh5SZXZva2VPcmdhbml6YXRpb25Gb2xkZXJBY2Nlc3MSMC50YWlsb3IudjEuUmV2b2tlT3JnYW5pemF0aW9uRm9sZGVyQWNjZXNzUmVxdWVzdBoxLnRhaWxvci52MS5SZXZva2VPcmdhbml6YXRpb25Gb2xkZXJBY2Nlc3NSZXNwb25zZSIAEooBCh5MaXN0T3JnYW5pemF0aW9uRm9sZGVyQWNjZXNzZXMSMC50YWlsb3IudjEuTGlzdE9yZ2FuaXphdGlvbkZvbGRlckFjY2Vzc2VzUmVxdWVzdBoxLnRhaWxvci52MS5MaXN0T3JnYW5pemF0aW9uRm9sZGVyQWNjZXNzZXNSZXNwb25zZSIDkAIBEoEBChtHZXRPcmdhbml6YXRpb25Gb2xkZXJBY2Nlc3MSLS50YWlsb3IudjEuR2V0T3JnYW5pemF0aW9uRm9sZGVyQWNjZXNzUmVxdWVzdBouLnRhaWxvci52MS5HZXRPcmdhbml6YXRpb25Gb2xkZXJBY2Nlc3NSZXNwb25zZSIDkAIBEm8KFkNyZWF0ZU9yZ2FuaXphdGlvblRlYW0SKC50YWlsb3IudjEuQ3JlYXRlT3JnYW5pemF0aW9uVGVhbVJlcXVlc3QaKS50YWlsb3IudjEuQ3JlYXRlT3JnYW5pemF0aW9uVGVhbVJlc3BvbnNlIgASbwoWVXBkYXRlT3JnYW5pemF0aW9uVGVhbRIoLnRhaWxvci52MS5VcGRhdGVPcmdhbml6YXRpb25UZWFtUmVxdWVzdBopLnRhaWxvci52MS5VcGRhdGVPcmdhbml6YXRpb25UZWFtUmVzcG9uc2UiABJvChZEZWxldGVPcmdhbml6YXRpb25UZWFtEigudGFpbG9yLnYxLkRlbGV0ZU9yZ2FuaXphdGlvblRlYW1SZXF1ZXN0GikudGFpbG9yLnYxLkRlbGV0ZU9yZ2FuaXphdGlvblRlYW1SZXNwb25zZSIAEmkKE0dldE9yZ2FuaXphdGlvblRlYW0SJS50YWlsb3IudjEuR2V0T3JnYW5pemF0aW9uVGVhbVJlcXVlc3QaJi50YWlsb3IudjEuR2V0T3JnYW5pemF0aW9uVGVhbVJlc3BvbnNlIgOQAgESbwoVTGlzdE9yZ2FuaXphdGlvblRlYW1zEicudGFpbG9yLnYxLkxpc3RPcmdhbml6YXRpb25UZWFtc1JlcXVlc3QaKC50YWlsb3IudjEuTGlzdE9yZ2FuaXphdGlvblRlYW1zUmVzcG9uc2UiA5ACARJ4ChlBZGRPcmdhbml6YXRpb25UZWFtTWVtYmVyEisudGFpbG9yLnYxLkFkZE9yZ2FuaXphdGlvblRlYW1NZW1iZXJSZXF1ZXN0GiwudGFpbG9yLnYxLkFkZE9yZ2FuaXphdGlvblRlYW1NZW1iZXJSZXNwb25zZSIAEoEBChxVcGRhdGVPcmdhbml6YXRpb25UZWFtTWVtYmVyEi4udGFpbG9yLnYxLlVwZGF0ZU9yZ2FuaXphdGlvblRlYW1NZW1iZXJSZXF1ZXN0Gi8udGFpbG9yLnYxLlVwZGF0ZU9yZ2FuaXphdGlvblRlYW1NZW1iZXJSZXNwb25zZSIAEoEBChxSZW1vdmVPcmdhbml6YXRpb25UZWFtTWVtYmVyEi4udGFpbG9yLnYxLlJlbW92ZU9yZ2FuaXphdGlvblRlYW1NZW1iZXJSZXF1ZXN0Gi8udGFpbG9yLnYxLlJlbW92ZU9yZ2FuaXphdGlvblRlYW1NZW1iZXJSZXNwb25zZSIAEoEBChtMaXN0T3JnYW5pemF0aW9uVGVhbU1lbWJlcnMSLS50YWlsb3IudjEuTGlzdE9yZ2FuaXphdGlvblRlYW1NZW1iZXJzUmVxdWVzdBouLnRhaWxvci52MS5MaXN0T3JnYW5pemF0aW9uVGVhbU1lbWJlcnNSZXNwb25zZSIDkAIBEnsKGUdldE9yZ2FuaXphdGlvblRlYW1NZW1iZXISKy50YWlsb3IudjEuR2V0T3JnYW5pemF0aW9uVGVhbU1lbWJlclJlcXVlc3QaLC50YWlsb3IudjEuR2V0T3JnYW5pemF0aW9uVGVhbU1lbWJlclJlc3BvbnNlIgOQAgEScgoWR2V0UGxhdGZvcm1BY2NvdW50UGxhbhIoLnRhaWxvci52MS5HZXRQbGF0Zm9ybUFjY291bnRQbGFuUmVxdWVzdBopLnRhaWxvci52MS5HZXRQbGF0Zm9ybUFjY291bnRQbGFuUmVzcG9uc2UiA5ACARJgChFDcmVhdGVBcHBsaWNhdGlvbhIjLnRhaWxvci52MS5DcmVhdGVBcHBsaWNhdGlvblJlcXVlc3QaJC50YWlsb3IudjEuQ3JlYXRlQXBwbGljYXRpb25SZXNwb25zZSIAEmAKEVVwZGF0ZUFwcGxpY2F0aW9uEiMudGFpbG9yLnYxLlVwZGF0ZUFwcGxpY2F0aW9uUmVxdWVzdBokLnRhaWxvci52MS5VcGRhdGVBcHBsaWNhdGlvblJlc3BvbnNlIgASYAoRRGVsZXRlQXBwbGljYXRpb24SIy50YWlsb3IudjEuRGVsZXRlQXBwbGljYXRpb25SZXF1ZXN0GiQudGFpbG9yLnYxLkRlbGV0ZUFwcGxpY2F0aW9uUmVzcG9uc2UiABJgChBMaXN0QXBwbGljYXRpb25zEiIudGFpbG9yLnYxLkxpc3RBcHBsaWNhdGlvbnNSZXF1ZXN0GiMudGFpbG9yLnYxLkxpc3RBcHBsaWNhdGlvbnNSZXNwb25zZSIDkAIBEloKDkdldEFwcGxpY2F0aW9uEiAudGFpbG9yLnYxLkdldEFwcGxpY2F0aW9uUmVxdWVzdBohLnRhaWxvci52MS5HZXRBcHBsaWNhdGlvblJlc3BvbnNlIgOQAgESfgoaR2V0QXBwbGljYXRpb25TY2hlbWFIZWFsdGgSLC50YWlsb3IudjEuR2V0QXBwbGljYXRpb25TY2hlbWFIZWFsdGhSZXF1ZXN0Gi0udGFpbG9yLnYxLkdldEFwcGxpY2F0aW9uU2NoZW1hSGVhbHRoUmVzcG9uc2UiA5ACARJjChJDb21wb3NlVGFpbG9yREJTREwSJC50YWlsb3IudjEuQ29tcG9zZVRhaWxvckRCU0RMUmVxdWVzdBolLnRhaWxvci52MS5Db21wb3NlVGFpbG9yREJTRExSZXNwb25zZSIAEmwKFUNyZWF0ZVRhaWxvckRCU2VydmljZRInLnRhaWxvci52MS5DcmVhdGVUYWlsb3JEQlNlcnZpY2VSZXF1ZXN0GigudGFpbG9yLnYxLkNyZWF0ZVRhaWxvckRCU2VydmljZVJlc3BvbnNlIgASbAoVVXBkYXRlVGFpbG9yREJTZXJ2aWNlEicudGFpbG9yLnYxLlVwZGF0ZVRhaWxvckRCU2VydmljZVJlcXVlc3QaKC50YWlsb3IudjEuVXBkYXRlVGFpbG9yREJTZXJ2aWNlUmVzcG9uc2UiABJsChVEZWxldGVUYWlsb3JEQlNlcnZpY2USJy50YWlsb3IudjEuRGVsZXRlVGFpbG9yREJTZXJ2aWNlUmVxdWVzdBooLnRhaWxvci52MS5EZWxldGVUYWlsb3JEQlNlcnZpY2VSZXNwb25zZSIAEmYKEkdldFRhaWxvckRCU2VydmljZRIkLnRhaWxvci52MS5HZXRUYWlsb3JEQlNlcnZpY2VSZXF1ZXN0GiUudGFpbG9yLnYxLkdldFRhaWxvckRCU2VydmljZVJlc3BvbnNlIgOQAgESbAoUTGlzdFRhaWxvckRCU2VydmljZXMSJi50YWlsb3IudjEuTGlzdFRhaWxvckRCU2VydmljZXNSZXF1ZXN0GicudGFpbG9yLnYxLkxpc3RUYWlsb3JEQlNlcnZpY2VzUmVzcG9uc2UiA5ACARJjChJDcmVhdGVUYWlsb3JEQlR5cGUSJC50YWlsb3IudjEuQ3JlYXRlVGFpbG9yREJUeXBlUmVxdWVzdBolLnRhaWxvci52MS5DcmVhdGVUYWlsb3JEQlR5cGVSZXNwb25zZSIAEmMKElVwZGF0ZVRhaWxvckRCVHlwZRIkLnRhaWxvci52MS5VcGRhdGVUYWlsb3JEQlR5cGVSZXF1ZXN0GiUudGFpbG9yLnYxLlVwZGF0ZVRhaWxvckRCVHlwZVJlc3BvbnNlIgASYwoSRGVsZXRlVGFpbG9yREJUeXBlEiQudGFpbG9yLnYxLkRlbGV0ZVRhaWxvckRCVHlwZVJlcXVlc3QaJS50YWlsb3IudjEuRGVsZXRlVGFpbG9yREJUeXBlUmVzcG9uc2UiABJsChVUcnVuY2F0ZVRhaWxvckRCVHlwZXMSJy50YWlsb3IudjEuVHJ1bmNhdGVUYWlsb3JEQlR5cGVzUmVxdWVzdBooLnRhaWxvci52MS5UcnVuY2F0ZVRhaWxvckRCVHlwZXNSZXNwb25zZSIAEmkKFFRydW5jYXRlVGFpbG9yREJUeXBlEiYudGFpbG9yLnYxLlRydW5jYXRlVGFpbG9yREJUeXBlUmVxdWVzdBonLnRhaWxvci52MS5UcnVuY2F0ZVRhaWxvckRCVHlwZVJlc3BvbnNlIgASYwoRTGlzdFRhaWxvckRCVHlwZXMSIy50YWlsb3IudjEuTGlzdFRhaWxvckRCVHlwZXNSZXF1ZXN0GiQudGFpbG9yLnYxLkxpc3RUYWlsb3JEQlR5cGVzUmVzcG9uc2UiA5ACARJdCg9HZXRUYWlsb3JEQlR5cGUSIS50YWlsb3IudjEuR2V0VGFpbG9yREJUeXBlUmVxdWVzdBoiLnRhaWxvci52MS5HZXRUYWlsb3JEQlR5cGVSZXNwb25zZSIDkAIBEn4KG0NyZWF0ZVRhaWxvckRCR1FMUGVybWlzc2lvbhItLnRhaWxvci52MS5DcmVhdGVUYWlsb3JEQkdRTFBlcm1pc3Npb25SZXF1ZXN0Gi4udGFpbG9yLnYxLkNyZWF0ZVRhaWxvckRCR1FMUGVybWlzc2lvblJlc3BvbnNlIgASeAoYR2V0VGFpbG9yREJHUUxQZXJtaXNzaW9uEioudGFpbG9yLnYxLkdldFRhaWxvckRCR1FMUGVybWlzc2lvblJlcXVlc3QaKy50YWlsb3IudjEuR2V0VGFpbG9yREJHUUxQZXJtaXNzaW9uUmVzcG9uc2UiA5ACARJ+ChpMaXN0VGFpbG9yREJHUUxQZXJtaXNzaW9ucxIsLnRhaWxvci52MS5MaXN0VGFpbG9yREJHUUxQZXJtaXNzaW9uc1JlcXVlc3QaLS50YWlsb3IudjEuTGlzdFRhaWxvckRCR1FMUGVybWlzc2lvbnNSZXNwb25zZSIDkAIBEn4KG1VwZGF0ZVRhaWxvckRCR1FMUGVybWlzc2lvbhItLnRhaWxvci52MS5VcGRhdGVUYWlsb3JEQkdRTFBlcm1pc3Npb25SZXF1ZXN0Gi4udGFpbG9yLnYxLlVwZGF0ZVRhaWxvckRCR1FMUGVybWlzc2lvblJlc3BvbnNlIgASfgobRGVsZXRlVGFpbG9yREJHUUxQZXJtaXNzaW9uEi0udGFpbG9yLnYxLkRlbGV0ZVRhaWxvckRCR1FMUGVybWlzc2lvblJlcXVlc3QaLi50YWlsb3IudjEuRGVsZXRlVGFpbG9yREJHUUxQZXJtaXNzaW9uUmVzcG9uc2UiABJsChVDcmVhdGVQaXBlbGluZVNlcnZpY2USJy50YWlsb3IudjEuQ3JlYXRlUGlwZWxpbmVTZXJ2aWNlUmVxdWVzdBooLnRhaWxvci52MS5DcmVhdGVQaXBlbGluZVNlcnZpY2VSZXNwb25zZSIAEmwKFVVwZGF0ZVBpcGVsaW5lU2VydmljZRInLnRhaWxvci52MS5VcGRhdGVQaXBlbGluZVNlcnZpY2VSZXF1ZXN0GigudGFpbG9yLnYxLlVwZGF0ZVBpcGVsaW5lU2VydmljZVJlc3BvbnNlIgASbAoVRGVsZXRlUGlwZWxpbmVTZXJ2aWNlEicudGFpbG9yLnYxLkRlbGV0ZVBpcGVsaW5lU2VydmljZVJlcXVlc3QaKC50YWlsb3IudjEuRGVsZXRlUGlwZWxpbmVTZXJ2aWNlUmVzcG9uc2UiABJmChJHZXRQaXBlbGluZVNlcnZpY2USJC50YWlsb3IudjEuR2V0UGlwZWxpbmVTZXJ2aWNlUmVxdWVzdBolLnRhaWxvci52MS5HZXRQaXBlbGluZVNlcnZpY2VSZXNwb25zZSIDkAIBEmwKFExpc3RQaXBlbGluZVNlcnZpY2VzEiYudGFpbG9yLnYxLkxpc3RQaXBlbGluZVNlcnZpY2VzUmVxdWVzdBonLnRhaWxvci52MS5MaXN0UGlwZWxpbmVTZXJ2aWNlc1Jlc3BvbnNlIgOQAgESaQoTR2V0UGlwZWxpbmVSZXNvbHZlchIlLnRhaWxvci52MS5HZXRQaXBlbGluZVJlc29sdmVyUmVxdWVzdBomLnRhaWxvci52MS5HZXRQaXBlbGluZVJlc29sdmVyUmVzcG9uc2UiA5ACARJvChVMaXN0UGlwZWxpbmVSZXNvbHZlcnMSJy50YWlsb3IudjEuTGlzdFBpcGVsaW5lUmVzb2x2ZXJzUmVxdWVzdBooLnRhaWxvci52MS5MaXN0UGlwZWxpbmVSZXNvbHZlcnNSZXNwb25zZSIDkAIBEm8KFkNyZWF0ZVBpcGVsaW5lUmVzb2x2ZXISKC50YWlsb3IudjEuQ3JlYXRlUGlwZWxpbmVSZXNvbHZlclJlcXVlc3QaKS50YWlsb3IudjEuQ3JlYXRlUGlwZWxpbmVSZXNvbHZlclJlc3BvbnNlIgASbwoWVXBkYXRlUGlwZWxpbmVSZXNvbHZlchIoLnRhaWxvci52MS5VcGRhdGVQaXBlbGluZVJlc29sdmVyUmVxdWVzdBopLnRhaWxvci52MS5VcGRhdGVQaXBlbGluZVJlc29sdmVyUmVzcG9uc2UiABJvChZEZWxldGVQaXBlbGluZVJlc29sdmVyEigudGFpbG9yLnYxLkRlbGV0ZVBpcGVsaW5lUmVzb2x2ZXJSZXF1ZXN0GikudGFpbG9yLnYxLkRlbGV0ZVBpcGVsaW5lUmVzb2x2ZXJSZXNwb25zZSIAEmMKEkNvbXBvc2VQaXBlbGluZVNETBIkLnRhaWxvci52MS5Db21wb3NlUGlwZWxpbmVTRExSZXF1ZXN0GiUudGFpbG9yLnYxLkNvbXBvc2VQaXBlbGluZVNETFJlc3BvbnNlIgASnAEKJExpc3RQaXBlbGluZVJlc29sdmVyRXhlY3V0aW9uUmVzdWx0cxI2LnRhaWxvci52MS5MaXN0UGlwZWxpbmVSZXNvbHZlckV4ZWN1dGlvblJlc3VsdHNSZXF1ZXN0GjcudGFpbG9yLnYxLkxpc3RQaXBlbGluZVJlc29sdmVyRXhlY3V0aW9uUmVzdWx0c1Jlc3BvbnNlIgOQAgESlgEKIkdldFBpcGVsaW5lUmVzb2x2ZXJFeGVjdXRpb25SZXN1bHQSNC50YWlsb3IudjEuR2V0UGlwZWxpbmVSZXNvbHZlckV4ZWN1dGlvblJlc3VsdFJlcXVlc3QaNS50YWlsb3IudjEuR2V0UGlwZWxpbmVSZXNvbHZlckV4ZWN1dGlvblJlc3VsdFJlc3BvbnNlIgOQAgEScgoXUmVzdGFydFBpcGVsaW5lUmVzb2x2ZXISKS50YWlsb3IudjEuUmVzdGFydFBpcGVsaW5lUmVzb2x2ZXJSZXF1ZXN0GioudGFpbG9yLnYxLlJlc3RhcnRQaXBlbGluZVJlc29sdmVyUmVzcG9uc2UiABJvChZDcmVhdGVTdGF0ZWZsb3dTZXJ2aWNlEigudGFpbG9yLnYxLkNyZWF0ZVN0YXRlZmxvd1NlcnZpY2VSZXF1ZXN0GikudGFpbG9yLnYxLkNyZWF0ZVN0YXRlZmxvd1NlcnZpY2VSZXNwb25zZSIAEm8KFlVwZGF0ZVN0YXRlZmxvd1NlcnZpY2USKC50YWlsb3IudjEuVXBkYXRlU3RhdGVmbG93U2VydmljZVJlcXVlc3QaKS50YWlsb3IudjEuVXBkYXRlU3RhdGVmbG93U2VydmljZVJlc3BvbnNlIgASbwoWRGVsZXRlU3RhdGVmbG93U2VydmljZRIoLnRhaWxvci52MS5EZWxldGVTdGF0ZWZsb3dTZXJ2aWNlUmVxdWVzdBopLnRhaWxvci52MS5EZWxldGVTdGF0ZWZsb3dTZXJ2aWNlUmVzcG9uc2UiABJpChNHZXRTdGF0ZWZsb3dTZXJ2aWNlEiUudGFpbG9yLnYxLkdldFN0YXRlZmxvd1NlcnZpY2VSZXF1ZXN0GiYudGFpbG9yLnYxLkdldFN0YXRlZmxvd1NlcnZpY2VSZXNwb25zZSIDkAIBEm8KFUxpc3RTdGF0ZWZsb3dTZXJ2aWNlcxInLnRhaWxvci52MS5MaXN0U3RhdGVmbG93U2VydmljZXNSZXF1ZXN0GigudGFpbG9yLnYxLkxpc3RTdGF0ZWZsb3dTZXJ2aWNlc1Jlc3BvbnNlIgOQAgESbwoWQ3JlYXRlRXhlY3V0b3JFeGVjdXRvchIoLnRhaWxvci52MS5DcmVhdGVFeGVjdXRvckV4ZWN1dG9yUmVxdWVzdBopLnRhaWxvci52MS5DcmVhdGVFeGVjdXRvckV4ZWN1dG9yUmVzcG9uc2UiABJvChZVcGRhdGVFeGVjdXRvckV4ZWN1dG9yEigudGFpbG9yLnYxLlVwZGF0ZUV4ZWN1dG9yRXhlY3V0b3JSZXF1ZXN0GikudGFpbG9yLnYxLlVwZGF0ZUV4ZWN1dG9yRXhlY3V0b3JSZXNwb25zZSIAEmkKE0dldEV4ZWN1dG9yRXhlY3V0b3ISJS50YWlsb3IudjEuR2V0RXhlY3V0b3JFeGVjdXRvclJlcXVlc3QaJi50YWlsb3IudjEuR2V0RXhlY3V0b3JFeGVjdXRvclJlc3BvbnNlIgOQAgESbwoWRGVsZXRlRXhlY3V0b3JFeGVjdXRvchIoLnRhaWxvci52MS5EZWxldGVFeGVjdXRvckV4ZWN1dG9yUmVxdWVzdBopLnRhaWxvci52MS5EZWxldGVFeGVjdXRvckV4ZWN1dG9yUmVzcG9uc2UiABJvChVMaXN0RXhlY3V0b3JFeGVjdXRvcnMSJy50YWlsb3IudjEuTGlzdEV4ZWN1dG9yRXhlY3V0b3JzUmVxdWVzdBooLnRhaWxvci52MS5MaXN0RXhlY3V0b3JFeGVjdXRvcnNSZXNwb25zZSIDkAIBEloKDkdldEV4ZWN1dG9ySm9iEiAudGFpbG9yLnYxLkdldEV4ZWN1dG9ySm9iUmVxdWVzdBohLnRhaWxvci52MS5HZXRFeGVjdXRvckpvYlJlc3BvbnNlIgOQAgESYAoQTGlzdEV4ZWN1dG9ySm9icxIiLnRhaWxvci52MS5MaXN0RXhlY3V0b3JKb2JzUmVxdWVzdBojLnRhaWxvci52MS5MaXN0RXhlY3V0b3JKb2JzUmVzcG9uc2UiA5ACARJ1ChdMaXN0RXhlY3V0b3JKb2JBdHRlbXB0cxIpLnRhaWxvci52MS5MaXN0RXhlY3V0b3JKb2JBdHRlbXB0c1JlcXVlc3QaKi50YWlsb3IudjEuTGlzdEV4ZWN1dG9ySm9iQXR0ZW1wdHNSZXNwb25zZSIDkAIBEoQBChxMaXN0RXhlY3V0b3JJbmNvbWluZ1dlYmhvb2tzEi4udGFpbG9yLnYxLkxpc3RFeGVjdXRvckluY29taW5nV2ViaG9va3NSZXF1ZXN0Gi8udGFpbG9yLnYxLkxpc3RFeGVjdXRvckluY29taW5nV2ViaG9va3NSZXNwb25zZSIDkAIBEn4KGkdldEV4ZWN1dG9ySW5jb21pbmdXZWJob29rEiwudGFpbG9yLnYxLkdldEV4ZWN1dG9ySW5jb21pbmdXZWJob29rUmVxdWVzdBotLnRhaWxvci52MS5HZXRFeGVjdXRvckluY29taW5nV2ViaG9va1Jlc3BvbnNlIgOQAgESWgoPVHJpZ2dlckV4ZWN1dG9yEiEudGFpbG9yLnYxLlRyaWdnZXJFeGVjdXRvclJlcXVlc3QaIi50YWlsb3IudjEuVHJpZ2dlckV4ZWN1dG9yUmVzcG9uc2UiABJ1ChhDcmVhdGVTZWNyZXRNYW5hZ2VyVmF1bHQSKi50YWlsb3IudjEuQ3JlYXRlU2VjcmV0TWFuYWdlclZhdWx0UmVxdWVzdBorLnRhaWxvci52MS5DcmVhdGVTZWNyZXRNYW5hZ2VyVmF1bHRSZXNwb25zZSIAEm8KFUdldFNlY3JldE1hbmFnZXJWYXVsdBInLnRhaWxvci52MS5HZXRTZWNyZXRNYW5hZ2VyVmF1bHRSZXF1ZXN0GigudGFpbG9yLnYxLkdldFNlY3JldE1hbmFnZXJWYXVsdFJlc3BvbnNlIgOQAgESdQoYRGVsZXRlU2VjcmV0TWFuYWdlclZhdWx0EioudGFpbG9yLnYxLkRlbGV0ZVNlY3JldE1hbmFnZXJWYXVsdFJlcXVlc3QaKy50YWlsb3IudjEuRGVsZXRlU2VjcmV0TWFuYWdlclZhdWx0UmVzcG9uc2UiABJ1ChdMaXN0U2VjcmV0TWFuYWdlclZhdWx0cxIpLnRhaWxvci52MS5MaXN0U2VjcmV0TWFuYWdlclZhdWx0c1JlcXVlc3QaKi50YWlsb3IudjEuTGlzdFNlY3JldE1hbmFnZXJWYXVsdHNSZXNwb25zZSIDkAIBEngKGUNyZWF0ZVNlY3JldE1hbmFnZXJTZWNyZXQSKy50YWlsb3IudjEuQ3JlYXRlU2VjcmV0TWFuYWdlclNlY3JldFJlcXVlc3QaLC50YWlsb3IudjEuQ3JlYXRlU2VjcmV0TWFuYWdlclNlY3JldFJlc3BvbnNlIgASeAoZVXBkYXRlU2VjcmV0TWFuYWdlclNlY3JldBIrLnRhaWxvci52MS5VcGRhdGVTZWNyZXRNYW5hZ2VyU2VjcmV0UmVxdWVzdBosLnRhaWxvci52MS5VcGRhdGVTZWNyZXRNYW5hZ2VyU2VjcmV0UmVzcG9uc2UiABJyChZHZXRTZWNyZXRNYW5hZ2VyU2VjcmV0EigudGFpbG9yLnYxLkdldFNlY3JldE1hbmFnZXJTZWNyZXRSZXF1ZXN0GikudGFpbG9yLnYxLkdldFNlY3JldE1hbmFnZXJTZWNyZXRSZXNwb25zZSIDkAIBEngKGURlbGV0ZVNlY3JldE1hbmFnZXJTZWNyZXQSKy50YWlsb3IudjEuRGVsZXRlU2VjcmV0TWFuYWdlclNlY3JldFJlcXVlc3QaLC50YWlsb3IudjEuRGVsZXRlU2VjcmV0TWFuYWdlclNlY3JldFJlc3BvbnNlIgASeAoYTGlzdFNlY3JldE1hbmFnZXJTZWNyZXRzEioudGFpbG9yLnYxLkxpc3RTZWNyZXRNYW5hZ2VyU2VjcmV0c1JlcXVlc3QaKy50YWlsb3IudjEuTGlzdFNlY3JldE1hbmFnZXJTZWNyZXRzUmVzcG9uc2UiA5ACARJgChFDcmVhdGVBdXRoU2VydmljZRIjLnRhaWxvci52MS5DcmVhdGVBdXRoU2VydmljZVJlcXVlc3QaJC50YWlsb3IudjEuQ3JlYXRlQXV0aFNlcnZpY2VSZXNwb25zZSIAEmAKEVVwZGF0ZUF1dGhTZXJ2aWNlEiMudGFpbG9yLnYxLlVwZGF0ZUF1dGhTZXJ2aWNlUmVxdWVzdBokLnRhaWxvci52MS5VcGRhdGVBdXRoU2VydmljZVJlc3BvbnNlIgASYAoRRGVsZXRlQXV0aFNlcnZpY2USIy50YWlsb3IudjEuRGVsZXRlQXV0aFNlcnZpY2VSZXF1ZXN0GiQudGFpbG9yLnYxLkRlbGV0ZUF1dGhTZXJ2aWNlUmVzcG9uc2UiABJaCg5HZXRBdXRoU2VydmljZRIgLnRhaWxvci52MS5HZXRBdXRoU2VydmljZVJlcXVlc3QaIS50YWlsb3IudjEuR2V0QXV0aFNlcnZpY2VSZXNwb25zZSIDkAIBEmAKEExpc3RBdXRoU2VydmljZXMSIi50YWlsb3IudjEuTGlzdEF1dGhTZXJ2aWNlc1JlcXVlc3QaIy50YWlsb3IudjEuTGlzdEF1dGhTZXJ2aWNlc1Jlc3BvbnNlIgOQAgESZgoTQ3JlYXRlQXV0aElEUENvbmZpZxIlLnRhaWxvci52MS5DcmVhdGVBdXRoSURQQ29uZmlnUmVxdWVzdBomLnRhaWxvci52MS5DcmVhdGVBdXRoSURQQ29uZmlnUmVzcG9uc2UiABJmChNVcGRhdGVBdXRoSURQQ29uZmlnEiUudGFpbG9yLnYxLlVwZGF0ZUF1dGhJRFBDb25maWdSZXF1ZXN0GiYudGFpbG9yLnYxLlVwZGF0ZUF1dGhJRFBDb25maWdSZXNwb25zZSIAEmYKE0RlbGV0ZUF1dGhJRFBDb25maWcSJS50YWlsb3IudjEuRGVsZXRlQXV0aElEUENvbmZpZ1JlcXVlc3QaJi50YWlsb3IudjEuRGVsZXRlQXV0aElEUENvbmZpZ1Jlc3BvbnNlIgASYAoQR2V0QXV0aElEUENvbmZpZxIiLnRhaWxvci52MS5HZXRBdXRoSURQQ29uZmlnUmVxdWVzdBojLnRhaWxvci52MS5HZXRBdXRoSURQQ29uZmlnUmVzcG9uc2UiA5ACARJmChJMaXN0QXV0aElEUENvbmZpZ3MSJC50YWlsb3IudjEuTGlzdEF1dGhJRFBDb25maWdzUmVxdWVzdBolLnRhaWxvci52MS5MaXN0QXV0aElEUENvbmZpZ3NSZXNwb25zZSIDkAIBEnIKF0NyZWF0ZVVzZXJQcm9maWxlQ29uZmlnEikudGFpbG9yLnYxLkNyZWF0ZVVzZXJQcm9maWxlQ29uZmlnUmVxdWVzdBoqLnRhaWxvci52MS5DcmVhdGVVc2VyUHJvZmlsZUNvbmZpZ1Jlc3BvbnNlIgAScgoXVXBkYXRlVXNlclByb2ZpbGVDb25maWcSKS50YWlsb3IudjEuVXBkYXRlVXNlclByb2ZpbGVDb25maWdSZXF1ZXN0GioudGFpbG9yLnYxLlVwZGF0ZVVzZXJQcm9maWxlQ29uZmlnUmVzcG9uc2UiABJyChdEZWxldGVVc2VyUHJvZmlsZUNvbmZpZxIpLnRhaWxvci52MS5EZWxldGVVc2VyUHJvZmlsZUNvbmZpZ1JlcXVlc3QaKi50YWlsb3IudjEuRGVsZXRlVXNlclByb2ZpbGVDb25maWdSZXNwb25zZSIAEmwKFEdldFVzZXJQcm9maWxlQ29uZmlnEiYudGFpbG9yLnYxLkdldFVzZXJQcm9maWxlQ29uZmlnUmVxdWVzdBonLnRhaWxvci52MS5HZXRVc2VyUHJvZmlsZUNvbmZpZ1Jlc3BvbnNlIgOQAgESYwoSQ3JlYXRlVGVuYW50Q29uZmlnEiQudGFpbG9yLnYxLkNyZWF0ZVRlbmFudENvbmZpZ1JlcXVlc3QaJS50YWlsb3IudjEuQ3JlYXRlVGVuYW50Q29uZmlnUmVzcG9uc2UiABJjChJVcGRhdGVUZW5hbnRDb25maWcSJC50YWlsb3IudjEuVXBkYXRlVGVuYW50Q29uZmlnUmVxdWVzdBolLnRhaWxvci52MS5VcGRhdGVUZW5hbnRDb25maWdSZXNwb25zZSIAEmMKEkRlbGV0ZVRlbmFudENvbmZpZxIkLnRhaWxvci52MS5EZWxldGVUZW5hbnRDb25maWdSZXF1ZXN0GiUudGFpbG9yLnYxLkRlbGV0ZVRlbmFudENvbmZpZ1Jlc3BvbnNlIgASXQoPR2V0VGVuYW50Q29uZmlnEiEudGFpbG9yLnYxLkdldFRlbmFudENvbmZpZ1JlcXVlc3QaIi50YWlsb3IudjEuR2V0VGVuYW50Q29uZmlnUmVzcG9uc2UiA5ACARJ4ChlDcmVhdGVQZXJzb25hbEFjY2Vzc1Rva2VuEisudGFpbG9yLnYxLkNyZWF0ZVBlcnNvbmFsQWNjZXNzVG9rZW5SZXF1ZXN0GiwudGFpbG9yLnYxLkNyZWF0ZVBlcnNvbmFsQWNjZXNzVG9rZW5SZXNwb25zZSIAEngKGURlbGV0ZVBlcnNvbmFsQWNjZXNzVG9rZW4SKy50YWlsb3IudjEuRGVsZXRlUGVyc29uYWxBY2Nlc3NUb2tlblJlcXVlc3QaLC50YWlsb3IudjEuRGVsZXRlUGVyc29uYWxBY2Nlc3NUb2tlblJlc3BvbnNlIgASeAoYTGlzdFBlcnNvbmFsQWNjZXNzVG9rZW5zEioudGFpbG9yLnYxLkxpc3RQZXJzb25hbEFjY2Vzc1Rva2Vuc1JlcXVlc3QaKy50YWlsb3IudjEuTGlzdFBlcnNvbmFsQWNjZXNzVG9rZW5zUmVzcG9uc2UiA5ACARJsChVDcmVhdGVBdXRoTWFjaGluZVVzZXISJy50YWlsb3IudjEuQ3JlYXRlQXV0aE1hY2hpbmVVc2VyUmVxdWVzdBooLnRhaWxvci52MS5DcmVhdGVBdXRoTWFjaGluZVVzZXJSZXNwb25zZSIAEmwKFVVwZGF0ZUF1dGhNYWNoaW5lVXNlchInLnRhaWxvci52MS5VcGRhdGVBdXRoTWFjaGluZVVzZXJSZXF1ZXN0GigudGFpbG9yLnYxLlVwZGF0ZUF1dGhNYWNoaW5lVXNlclJlc3BvbnNlIgASbAoVRGVsZXRlQXV0aE1hY2hpbmVVc2VyEicudGFpbG9yLnYxLkRlbGV0ZUF1dGhNYWNoaW5lVXNlclJlcXVlc3QaKC50YWlsb3IudjEuRGVsZXRlQXV0aE1hY2hpbmVVc2VyUmVzcG9uc2UiABJmChJHZXRBdXRoTWFjaGluZVVzZXISJC50YWlsb3IudjEuR2V0QXV0aE1hY2hpbmVVc2VyUmVxdWVzdBolLnRhaWxvci52MS5HZXRBdXRoTWFjaGluZVVzZXJSZXNwb25zZSIDkAIBEmwKFExpc3RBdXRoTWFjaGluZVVzZXJzEiYudGFpbG9yLnYxLkxpc3RBdXRoTWFjaGluZVVzZXJzUmVxdWVzdBonLnRhaWxvci52MS5MaXN0QXV0aE1hY2hpbmVVc2Vyc1Jlc3BvbnNlIgOQAgESaQoUQ3JlYXRlQXV0aFNDSU1Db25maWcSJi50YWlsb3IudjEuQ3JlYXRlQXV0aFNDSU1Db25maWdSZXF1ZXN0GicudGFpbG9yLnYxLkNyZWF0ZUF1dGhTQ0lNQ29uZmlnUmVzcG9uc2UiABJpChRVcGRhdGVBdXRoU0NJTUNvbmZpZxImLnRhaWxvci52MS5VcGRhdGVBdXRoU0NJTUNvbmZpZ1JlcXVlc3QaJy50YWlsb3IudjEuVXBkYXRlQXV0aFNDSU1Db25maWdSZXNwb25zZSIAEmkKFERlbGV0ZUF1dGhTQ0lNQ29uZmlnEiYudGFpbG9yLnYxLkRlbGV0ZUF1dGhTQ0lNQ29uZmlnUmVxdWVzdBonLnRhaWxvci52MS5EZWxldGVBdXRoU0NJTUNvbmZpZ1Jlc3BvbnNlIgASYwoRR2V0QXV0aFNDSU1Db25maWcSIy50YWlsb3IudjEuR2V0QXV0aFNDSU1Db25maWdSZXF1ZXN0GiQudGFpbG9yLnYxLkdldEF1dGhTQ0lNQ29uZmlnUmVzcG9uc2UiA5ACARJvChZDcmVhdGVBdXRoU0NJTVJlc291cmNlEigudGFpbG9yLnYxLkNyZWF0ZUF1dGhTQ0lNUmVzb3VyY2VSZXF1ZXN0GikudGFpbG9yLnYxLkNyZWF0ZUF1dGhTQ0lNUmVzb3VyY2VSZXNwb25zZSIAEm8KFlVwZGF0ZUF1dGhTQ0lNUmVzb3VyY2USKC50YWlsb3IudjEuVXBkYXRlQXV0aFNDSU1SZXNvdXJjZVJlcXVlc3QaKS50YWlsb3IudjEuVXBkYXRlQXV0aFNDSU1SZXNvdXJjZVJlc3BvbnNlIgASbwoWRGVsZXRlQXV0aFNDSU1SZXNvdXJjZRIoLnRhaWxvci52MS5EZWxldGVBdXRoU0NJTVJlc291cmNlUmVxdWVzdBopLnRhaWxvci52MS5EZWxldGVBdXRoU0NJTVJlc291cmNlUmVzcG9uc2UiABJpChNHZXRBdXRoU0NJTVJlc291cmNlEiUudGFpbG9yLnYxLkdldEF1dGhTQ0lNUmVzb3VyY2VSZXF1ZXN0GiYudGFpbG9yLnYxLkdldEF1dGhTQ0lNUmVzb3VyY2VSZXNwb25zZSIDkAIBEmwKFEdldEF1dGhTQ0lNUmVzb3VyY2VzEiYudGFpbG9yLnYxLkdldEF1dGhTQ0lNUmVzb3VyY2VzUmVxdWVzdBonLnRhaWxvci52MS5HZXRBdXRoU0NJTVJlc291cmNlc1Jlc3BvbnNlIgOQAgESVwoOQ3JlYXRlQXV0aEhvb2sSIC50YWlsb3IudjEuQ3JlYXRlQXV0aEhvb2tSZXF1ZXN0GiEudGFpbG9yLnYxLkNyZWF0ZUF1dGhIb29rUmVzcG9uc2UiABJXCg5VcGRhdGVBdXRoSG9vaxIgLnRhaWxvci52MS5VcGRhdGVBdXRoSG9va1JlcXVlc3QaIS50YWlsb3IudjEuVXBkYXRlQXV0aEhvb2tSZXNwb25zZSIAElcKDkRlbGV0ZUF1dGhIb29rEiAudGFpbG9yLnYxLkRlbGV0ZUF1dGhIb29rUmVxdWVzdBohLnRhaWxvci52MS5EZWxldGVBdXRoSG9va1Jlc3BvbnNlIgASUQoLR2V0QXV0aEhvb2sSHS50YWlsb3IudjEuR2V0QXV0aEhvb2tSZXF1ZXN0Gh4udGFpbG9yLnYxLkdldEF1dGhIb29rUmVzcG9uc2UiA5ACARJpChRDcmVhdGVBdXRoQ29ubmVjdGlvbhImLnRhaWxvci52MS5DcmVhdGVBdXRoQ29ubmVjdGlvblJlcXVlc3QaJy50YWlsb3IudjEuQ3JlYXRlQXV0aENvbm5lY3Rpb25SZXNwb25zZSIAEmkKE0xpc3RBdXRoQ29ubmVjdGlvbnMSJS50YWlsb3IudjEuTGlzdEF1dGhDb25uZWN0aW9uc1JlcXVlc3QaJi50YWlsb3IudjEuTGlzdEF1dGhDb25uZWN0aW9uc1Jlc3BvbnNlIgOQAgESaQoUUmV2b2tlQXV0aENvbm5lY3Rpb24SJi50YWlsb3IudjEuUmV2b2tlQXV0aENvbm5lY3Rpb25SZXF1ZXN0GicudGFpbG9yLnYxLlJldm9rZUF1dGhDb25uZWN0aW9uUmVzcG9uc2UiABKEAQodUmVnaXN0ZXJBdXRoQ29ubmVjdGlvblNlc3Npb24SLy50YWlsb3IudjEuUmVnaXN0ZXJBdXRoQ29ubmVjdGlvblNlc3Npb25SZXF1ZXN0GjAudGFpbG9yLnYxLlJlZ2lzdGVyQXV0aENvbm5lY3Rpb25TZXNzaW9uUmVzcG9uc2UiABKiAQonRXhjaGFuZ2VBdXRoQ29ubmVjdGlvbkF1dGhvcml6YXRpb25Db2RlEjkudGFpbG9yLnYxLkV4Y2hhbmdlQXV0aENvbm5lY3Rpb25BdXRob3JpemF0aW9uQ29kZVJlcXVlc3QaOi50YWlsb3IudjEuRXhjaGFuZ2VBdXRoQ29ubmVjdGlvbkF1dGhvcml6YXRpb25Db2RlUmVzcG9uc2UiABJvChZDcmVhdGVBdXRoT0F1dGgyQ2xpZW50EigudGFpbG9yLnYxLkNyZWF0ZUF1dGhPQXV0aDJDbGllbnRSZXF1ZXN0GikudGFpbG9yLnYxLkNyZWF0ZUF1dGhPQXV0aDJDbGllbnRSZXNwb25zZSIAEm8KFlVwZGF0ZUF1dGhPQXV0aDJDbGllbnQSKC50YWlsb3IudjEuVXBkYXRlQXV0aE9BdXRoMkNsaWVudFJlcXVlc3QaKS50YWlsb3IudjEuVXBkYXRlQXV0aE9BdXRoMkNsaWVudFJlc3BvbnNlIgASbwoWRGVsZXRlQXV0aE9BdXRoMkNsaWVudBIoLnRhaWxvci52MS5EZWxldGVBdXRoT0F1dGgyQ2xpZW50UmVxdWVzdBopLnRhaWxvci52MS5EZWxldGVBdXRoT0F1dGgyQ2xpZW50UmVzcG9uc2UiABJpChNHZXRBdXRoT0F1dGgyQ2xpZW50EiUudGFpbG9yLnYxLkdldEF1dGhPQXV0aDJDbGllbnRSZXF1ZXN0GiYudGFpbG9yLnYxLkdldEF1dGhPQXV0aDJDbGllbnRSZXNwb25zZSIDkAIBEm8KFUxpc3RBdXRoT0F1dGgyQ2xpZW50cxInLnRhaWxvci52MS5MaXN0QXV0aE9BdXRoMkNsaWVudHNSZXF1ZXN0GigudGFpbG9yLnYxLkxpc3RBdXRoT0F1dGgyQ2xpZW50c1Jlc3BvbnNlIgOQAgESaQoTTGlzdERhdGFwbGFuZUV2ZW50cxIlLnRhaWxvci52MS5MaXN0RGF0YXBsYW5lRXZlbnRzUmVxdWVzdBomLnRhaWxvci52MS5MaXN0RGF0YXBsYW5lRXZlbnRzUmVzcG9uc2UiA5ACARKEAQocTGlzdENvbnRyb2xwbGFuZUFjdGl2aXR5TG9ncxIuLnRhaWxvci52MS5MaXN0Q29udHJvbHBsYW5lQWN0aXZpdHlMb2dzUmVxdWVzdBovLnRhaWxvci52MS5MaXN0Q29udHJvbHBsYW5lQWN0aXZpdHlMb2dzUmVzcG9uc2UiA5ACARJXCg5UZXN0RXhlY1NjcmlwdBIgLnRhaWxvci52MS5UZXN0RXhlY1NjcmlwdFJlcXVlc3QaIS50YWlsb3IudjEuVGVzdEV4ZWNTY3JpcHRSZXNwb25zZSIAEmwKFEdldEZ1bmN0aW9uRXhlY3V0aW9uEiYudGFpbG9yLnYxLkdldEZ1bmN0aW9uRXhlY3V0aW9uUmVxdWVzdBonLnRhaWxvci52MS5HZXRGdW5jdGlvbkV4ZWN1dGlvblJlc3BvbnNlIgOQAgEScgoWTGlzdEZ1bmN0aW9uRXhlY3V0aW9ucxIoLnRhaWxvci52MS5MaXN0RnVuY3Rpb25FeGVjdXRpb25zUmVxdWVzdBopLnRhaWxvci52MS5MaXN0RnVuY3Rpb25FeGVjdXRpb25zUmVzcG9uc2UiA5ACARJxChZDcmVhdGVGdW5jdGlvblJlZ2lzdHJ5EigudGFpbG9yLnYxLkNyZWF0ZUZ1bmN0aW9uUmVnaXN0cnlSZXF1ZXN0GikudGFpbG9yLnYxLkNyZWF0ZUZ1bmN0aW9uUmVnaXN0cnlSZXNwb25zZSIAKAEScQoWVXBkYXRlRnVuY3Rpb25SZWdpc3RyeRIoLnRhaWxvci52MS5VcGRhdGVGdW5jdGlvblJlZ2lzdHJ5UmVxdWVzdBopLnRhaWxvci52MS5VcGRhdGVGdW5jdGlvblJlZ2lzdHJ5UmVzcG9uc2UiACgBEmkKE0dldEZ1bmN0aW9uUmVnaXN0cnkSJS50YWlsb3IudjEuR2V0RnVuY3Rpb25SZWdpc3RyeVJlcXVlc3QaJi50YWlsb3IudjEuR2V0RnVuY3Rpb25SZWdpc3RyeVJlc3BvbnNlIgOQAgEScgoWTGlzdEZ1bmN0aW9uUmVnaXN0cmllcxIoLnRhaWxvci52MS5MaXN0RnVuY3Rpb25SZWdpc3RyaWVzUmVxdWVzdBopLnRhaWxvci52MS5MaXN0RnVuY3Rpb25SZWdpc3RyaWVzUmVzcG9uc2UiA5ACARJvChZEZWxldGVGdW5jdGlvblJlZ2lzdHJ5EigudGFpbG9yLnYxLkRlbGV0ZUZ1bmN0aW9uUmVnaXN0cnlSZXF1ZXN0GikudGFpbG9yLnYxLkRlbGV0ZUZ1bmN0aW9uUmVnaXN0cnlSZXNwb25zZSIAEowBCh5Eb3dubG9hZEZ1bmN0aW9uUmVnaXN0cnlTY3JpcHQSMC50YWlsb3IudjEuRG93bmxvYWRGdW5jdGlvblJlZ2lzdHJ5U2NyaXB0UmVxdWVzdBoxLnRhaWxvci52MS5Eb3dubG9hZEZ1bmN0aW9uUmVnaXN0cnlTY3JpcHRSZXNwb25zZSIDkAIBMAEScgoWTGlzdE1ldGVyUmVxdWVzdENvdW50cxIoLnRhaWxvci52MS5MaXN0TWV0ZXJSZXF1ZXN0Q291bnRzUmVxdWVzdBopLnRhaWxvci52MS5MaXN0TWV0ZXJSZXF1ZXN0Q291bnRzUmVzcG9uc2UiA5ACARJ4ChhMaXN0TWV0ZXJFeGVjdXRpb25Db3VudHMSKi50YWlsb3IudjEuTGlzdE1ldGVyRXhlY3V0aW9uQ291bnRzUmVxdWVzdBorLnRhaWxvci52MS5MaXN0TWV0ZXJFeGVjdXRpb25Db3VudHNSZXNwb25zZSIDkAIBEmwKFExpc3RNZXRlckV2ZW50Q291bnRzEiYudGFpbG9yLnYxLkxpc3RNZXRlckV2ZW50Q291bnRzUmVxdWVzdBonLnRhaWxvci52MS5MaXN0TWV0ZXJFdmVudENvdW50c1Jlc3BvbnNlIgOQAgESXQoPTGlzdElkUFNlcnZpY2VzEiEudGFpbG9yLnYxLkxpc3RJZFBTZXJ2aWNlc1JlcXVlc3QaIi50YWlsb3IudjEuTGlzdElkUFNlcnZpY2VzUmVzcG9uc2UiA5ACARJdChBDcmVhdGVJZFBTZXJ2aWNlEiIudGFpbG9yLnYxLkNyZWF0ZUlkUFNlcnZpY2VSZXF1ZXN0GiMudGFpbG9yLnYxLkNyZWF0ZUlkUFNlcnZpY2VSZXNwb25zZSIAEl0KEFVwZGF0ZUlkUFNlcnZpY2USIi50YWlsb3IudjEuVXBkYXRlSWRQU2VydmljZVJlcXVlc3QaIy50YWlsb3IudjEuVXBkYXRlSWRQU2VydmljZVJlc3BvbnNlIgASXQoQRGVsZXRlSWRQU2VydmljZRIiLnRhaWxvci52MS5EZWxldGVJZFBTZXJ2aWNlUmVxdWVzdBojLnRhaWxvci52MS5EZWxldGVJZFBTZXJ2aWNlUmVzcG9uc2UiABJXCg1HZXRJZFBTZXJ2aWNlEh8udGFpbG9yLnYxLkdldElkUFNlcnZpY2VSZXF1ZXN0GiAudGFpbG9yLnYxLkdldElkUFNlcnZpY2VSZXNwb25zZSIDkAIBEloKDkxpc3RJZFBDbGllbnRzEiAudGFpbG9yLnYxLkxpc3RJZFBDbGllbnRzUmVxdWVzdBohLnRhaWxvci52MS5MaXN0SWRQQ2xpZW50c1Jlc3BvbnNlIgOQAgESWgoPQ3JlYXRlSWRQQ2xpZW50EiEudGFpbG9yLnYxLkNyZWF0ZUlkUENsaWVudFJlcXVlc3QaIi50YWlsb3IudjEuQ3JlYXRlSWRQQ2xpZW50UmVzcG9uc2UiABJaCg9EZWxldGVJZFBDbGllbnQSIS50YWlsb3IudjEuRGVsZXRlSWRQQ2xpZW50UmVxdWVzdBoiLnRhaWxvci52MS5EZWxldGVJZFBDbGllbnRSZXNwb25zZSIAElQKDEdldElkUENsaWVudBIeLnRhaWxvci52MS5HZXRJZFBDbGllbnRSZXF1ZXN0Gh8udGFpbG9yLnYxLkdldElkUENsaWVudFJlc3BvbnNlIgOQAgESZgoTQ3JlYXRlU3RhdGljV2Vic2l0ZRIlLnRhaWxvci52MS5DcmVhdGVTdGF0aWNXZWJzaXRlUmVxdWVzdBomLnRhaWxvci52MS5DcmVhdGVTdGF0aWNXZWJzaXRlUmVzcG9uc2UiABJmChNVcGRhdGVTdGF0aWNXZWJzaXRlEiUudGFpbG9yLnYxLlVwZGF0ZVN0YXRpY1dlYnNpdGVSZXF1ZXN0GiYudGFpbG9yLnYxLlVwZGF0ZVN0YXRpY1dlYnNpdGVSZXNwb25zZSIAEmYKE0RlbGV0ZVN0YXRpY1dlYnNpdGUSJS50YWlsb3IudjEuRGVsZXRlU3RhdGljV2Vic2l0ZVJlcXVlc3QaJi50YWlsb3IudjEuRGVsZXRlU3RhdGljV2Vic2l0ZVJlc3BvbnNlIgASYAoQR2V0U3RhdGljV2Vic2l0ZRIiLnRhaWxvci52MS5HZXRTdGF0aWNXZWJzaXRlUmVxdWVzdBojLnRhaWxvci52MS5HZXRTdGF0aWNXZWJzaXRlUmVzcG9uc2UiA5ACARJmChJMaXN0U3RhdGljV2Vic2l0ZXMSJC50YWlsb3IudjEuTGlzdFN0YXRpY1dlYnNpdGVzUmVxdWVzdBolLnRhaWxvci52MS5MaXN0U3RhdGljV2Vic2l0ZXNSZXNwb25zZSIDkAIBEl0KEENyZWF0ZURlcGxveW1lbnQSIi50YWlsb3IudjEuQ3JlYXRlRGVwbG95bWVudFJlcXVlc3QaIy50YWlsb3IudjEuQ3JlYXRlRGVwbG95bWVudFJlc3BvbnNlIgASTQoKVXBsb2FkRmlsZRIcLnRhaWxvci52MS5VcGxvYWRGaWxlUmVxdWVzdBodLnRhaWxvci52MS5VcGxvYWRGaWxlUmVzcG9uc2UiACgBEmAKEVB1Ymxpc2hEZXBsb3ltZW50EiMudGFpbG9yLnYxLlB1Ymxpc2hEZXBsb3ltZW50UmVxdWVzdBokLnRhaWxvci52MS5QdWJsaXNoRGVwbG95bWVudFJlc3BvbnNlIgASVwoOQ3JlYXRlV29ya2Zsb3cSIC50YWlsb3IudjEuQ3JlYXRlV29ya2Zsb3dSZXF1ZXN0GiEudGFpbG9yLnYxLkNyZWF0ZVdvcmtmbG93UmVzcG9uc2UiABJXCg5VcGRhdGVXb3JrZmxvdxIgLnRhaWxvci52MS5VcGRhdGVXb3JrZmxvd1JlcXVlc3QaIS50YWlsb3IudjEuVXBkYXRlV29ya2Zsb3dSZXNwb25zZSIAElcKDkRlbGV0ZVdvcmtmbG93EiAudGFpbG9yLnYxLkRlbGV0ZVdvcmtmbG93UmVxdWVzdBohLnRhaWxvci52MS5EZWxldGVXb3JrZmxvd1Jlc3BvbnNlIgASUQoLR2V0V29ya2Zsb3cSHS50YWlsb3IudjEuR2V0V29ya2Zsb3dSZXF1ZXN0Gh4udGFpbG9yLnYxLkdldFdvcmtmbG93UmVzcG9uc2UiA5ACARJjChFHZXRXb3JrZmxvd0J5TmFtZRIjLnRhaWxvci52MS5HZXRXb3JrZmxvd0J5TmFtZVJlcXVlc3QaJC50YWlsb3IudjEuR2V0V29ya2Zsb3dCeU5hbWVSZXNwb25zZSIDkAIBElcKDUxpc3RXb3JrZmxvd3MSHy50YWlsb3IudjEuTGlzdFdvcmtmbG93c1JlcXVlc3QaIC50YWlsb3IudjEuTGlzdFdvcmtmbG93c1Jlc3BvbnNlIgOQAgESeAoZQ3JlYXRlV29ya2Zsb3dKb2JGdW5jdGlvbhIrLnRhaWxvci52MS5DcmVhdGVXb3JrZmxvd0pvYkZ1bmN0aW9uUmVxdWVzdBosLnRhaWxvci52MS5DcmVhdGVXb3JrZmxvd0pvYkZ1bmN0aW9uUmVzcG9uc2UiABJ4ChlVcGRhdGVXb3JrZmxvd0pvYkZ1bmN0aW9uEisudGFpbG9yLnYxLlVwZGF0ZVdvcmtmbG93Sm9iRnVuY3Rpb25SZXF1ZXN0GiwudGFpbG9yLnYxLlVwZGF0ZVdvcmtmbG93Sm9iRnVuY3Rpb25SZXNwb25zZSIAEnIKFkdldFdvcmtmbG93Sm9iRnVuY3Rpb24SKC50YWlsb3IudjEuR2V0V29ya2Zsb3dKb2JGdW5jdGlvblJlcXVlc3QaKS50YWlsb3IudjEuR2V0V29ya2Zsb3dKb2JGdW5jdGlvblJlc3BvbnNlIgOQAgEShAEKHEdldFdvcmtmbG93Sm9iRnVuY3Rpb25CeU5hbWUSLi50YWlsb3IudjEuR2V0V29ya2Zsb3dKb2JGdW5jdGlvbkJ5TmFtZVJlcXVlc3QaLy50YWlsb3IudjEuR2V0V29ya2Zsb3dKb2JGdW5jdGlvbkJ5TmFtZVJlc3BvbnNlIgOQAgESeAoYTGlzdFdvcmtmbG93Sm9iRnVuY3Rpb25zEioudGFpbG9yLnYxLkxpc3RXb3JrZmxvd0pvYkZ1bmN0aW9uc1JlcXVlc3QaKy50YWlsb3IudjEuTGlzdFdvcmtmbG93Sm9iRnVuY3Rpb25zUmVzcG9uc2UiA5ACARJsChRHZXRXb3JrZmxvd0V4ZWN1dGlvbhImLnRhaWxvci52MS5HZXRXb3JrZmxvd0V4ZWN1dGlvblJlcXVlc3QaJy50YWlsb3IudjEuR2V0V29ya2Zsb3dFeGVjdXRpb25SZXNwb25zZSIDkAIBEnIKFkxpc3RXb3JrZmxvd0V4ZWN1dGlvbnMSKC50YWlsb3IudjEuTGlzdFdvcmtmbG93RXhlY3V0aW9uc1JlcXVlc3QaKS50YWlsb3IudjEuTGlzdFdvcmtmbG93RXhlY3V0aW9uc1Jlc3BvbnNlIgOQAgESYAoRVGVzdFN0YXJ0V29ya2Zsb3cSIy50YWlsb3IudjEuVGVzdFN0YXJ0V29ya2Zsb3dSZXF1ZXN0GiQudGFpbG9yLnYxLlRlc3RTdGFydFdvcmtmbG93UmVzcG9uc2UiABJjChJUZXN0UmVzdW1lV29ya2Zsb3cSJC50YWlsb3IudjEuVGVzdFJlc3VtZVdvcmtmbG93UmVxdWVzdBolLnRhaWxvci52MS5UZXN0UmVzdW1lV29ya2Zsb3dSZXNwb25zZSIAEk4KC1NldE1ldGFkYXRhEh0udGFpbG9yLnYxLlNldE1ldGFkYXRhUmVxdWVzdBoeLnRhaWxvci52MS5TZXRNZXRhZGF0YVJlc3BvbnNlIgASUQoLR2V0TWV0YWRhdGESHS50YWlsb3IudjEuR2V0TWV0YWRhdGFSZXF1ZXN0Gh4udGFpbG9yLnYxLkdldE1ldGFkYXRhUmVzcG9uc2UiA5ACARKEAQodQ3JlYXRlQ29udHJvbHBsYW5lTWFjaGluZVVzZXISLy50YWlsb3IudjEuQ3JlYXRlQ29udHJvbHBsYW5lTWFjaGluZVVzZXJSZXF1ZXN0GjAudGFpbG9yLnYxLkNyZWF0ZUNvbnRyb2xwbGFuZU1hY2hpbmVVc2VyUmVzcG9uc2UiABKEAQodVXBkYXRlQ29udHJvbHBsYW5lTWFjaGluZVVzZXISLy50YWlsb3IudjEuVXBkYXRlQ29udHJvbHBsYW5lTWFjaGluZVVzZXJSZXF1ZXN0GjAudGFpbG9yLnYxLlVwZGF0ZUNvbnRyb2xwbGFuZU1hY2hpbmVVc2VyUmVzcG9uc2UiABJ+ChpHZXRDb250cm9scGxhbmVNYWNoaW5lVXNlchIsLnRhaWxvci52MS5HZXRDb250cm9scGxhbmVNYWNoaW5lVXNlclJlcXVlc3QaLS50YWlsb3IudjEuR2V0Q29udHJvbHBsYW5lTWFjaGluZVVzZXJSZXNwb25zZSIDkAIBEpABCiBHZXRDb250cm9scGxhbmVNYWNoaW5lVXNlckJ5TmFtZRIyLnRhaWxvci52MS5HZXRDb250cm9scGxhbmVNYWNoaW5lVXNlckJ5TmFtZVJlcXVlc3QaMy50YWlsb3IudjEuR2V0Q29udHJvbHBsYW5lTWFjaGluZVVzZXJCeU5hbWVSZXNwb25zZSIDkAIBEoQBChxMaXN0Q29udHJvbHBsYW5lTWFjaGluZVVzZXJzEi4udGFpbG9yLnYxLkxpc3RDb250cm9scGxhbmVNYWNoaW5lVXNlcnNSZXF1ZXN0Gi8udGFpbG9yLnYxLkxpc3RDb250cm9scGxhbmVNYWNoaW5lVXNlcnNSZXNwb25zZSIDkAIBEoQBCh1EZWxldGVDb250cm9scGxhbmVNYWNoaW5lVXNlchIvLnRhaWxvci52MS5EZWxldGVDb250cm9scGxhbmVNYWNoaW5lVXNlclJlcXVlc3QaMC50YWlsb3IudjEuRGVsZXRlQ29udHJvbHBsYW5lTWFjaGluZVVzZXJSZXNwb25zZSIAEmwKFUNyZWF0ZVRlbGVtZXRyeUV4cG9ydBInLnRhaWxvci52MS5DcmVhdGVUZWxlbWV0cnlFeHBvcnRSZXF1ZXN0GigudGFpbG9yLnYxLkNyZWF0ZVRlbGVtZXRyeUV4cG9ydFJlc3BvbnNlIgASbAoVVXBkYXRlVGVsZW1ldHJ5RXhwb3J0EicudGFpbG9yLnYxLlVwZGF0ZVRlbGVtZXRyeUV4cG9ydFJlcXVlc3QaKC50YWlsb3IudjEuVXBkYXRlVGVsZW1ldHJ5RXhwb3J0UmVzcG9uc2UiABJmChJHZXRUZWxlbWV0cnlFeHBvcnQSJC50YWlsb3IudjEuR2V0VGVsZW1ldHJ5RXhwb3J0UmVxdWVzdBolLnRhaWxvci52MS5HZXRUZWxlbWV0cnlFeHBvcnRSZXNwb25zZSIDkAIBEmwKFExpc3RUZWxlbWV0cnlFeHBvcnRzEiYudGFpbG9yLnYxLkxpc3RUZWxlbWV0cnlFeHBvcnRzUmVxdWVzdBonLnRhaWxvci52MS5MaXN0VGVsZW1ldHJ5RXhwb3J0c1Jlc3BvbnNlIgOQAgESbAoVRGVsZXRlVGVsZW1ldHJ5RXhwb3J0EicudGFpbG9yLnYxLkRlbGV0ZVRlbGVtZXRyeUV4cG9ydFJlcXVlc3QaKC50YWlsb3IudjEuRGVsZXRlVGVsZW1ldHJ5RXhwb3J0UmVzcG9uc2UiABJmChNUZXN0VGVsZW1ldHJ5RXhwb3J0EiUudGFpbG9yLnYxLlRlc3RUZWxlbWV0cnlFeHBvcnRSZXF1ZXN0GiYudGFpbG9yLnYxLlRlc3RUZWxlbWV0cnlFeHBvcnRSZXNwb25zZSIAEocBCh5DcmVhdGVSZXNvdXJjZUF0dHJpYnV0ZXNDb25maWcSMC50YWlsb3IudjEuQ3JlYXRlUmVzb3VyY2VBdHRyaWJ1dGVzQ29uZmlnUmVxdWVzdBoxLnRhaWxvci52MS5DcmVhdGVSZXNvdXJjZUF0dHJpYnV0ZXNDb25maWdSZXNwb25zZSIAEoEBChtHZXRSZXNvdXJjZUF0dHJpYnV0ZXNDb25maWcSLS50YWlsb3IudjEuR2V0UmVzb3VyY2VBdHRyaWJ1dGVzQ29uZmlnUmVxdWVzdBouLnRhaWxvci52MS5HZXRSZXNvdXJjZUF0dHJpYnV0ZXNDb25maWdSZXNwb25zZSIDkAIBEocBCh5VcGRhdGVSZXNvdXJjZUF0dHJpYnV0ZXNDb25maWcSMC50YWlsb3IudjEuVXBkYXRlUmVzb3VyY2VBdHRyaWJ1dGVzQ29uZmlnUmVxdWVzdBoxLnRhaWxvci52MS5VcGRhdGVSZXNvdXJjZUF0dHJpYnV0ZXNDb25maWdSZXNwb25zZSIAEocBCh5EZWxldGVSZXNvdXJjZUF0dHJpYnV0ZXNDb25maWcSMC50YWlsb3IudjEuRGVsZXRlUmVzb3VyY2VBdHRyaWJ1dGVzQ29uZmlnUmVxdWVzdBoxLnRhaWxvci52MS5EZWxldGVSZXNvdXJjZUF0dHJpYnV0ZXNDb25maWdSZXNwb25zZSIAYgZwcm90bzM", [file_tailor_v1_application, file_tailor_v1_auth, file_tailor_v1_events, file_tailor_v1_executor, file_tailor_v1_function, file_tailor_v1_function_registry, file_tailor_v1_idp, file_tailor_v1_metadata, file_tailor_v1_meter, file_tailor_v1_pipeline, file_tailor_v1_secret_manager, file_tailor_v1_stateflow, file_tailor_v1_staticwebsite, file_tailor_v1_tailordb, file_tailor_v1_telemetryrouter, file_tailor_v1_workflow, file_tailor_v1_workspace]); + fileDesc("Chd0YWlsb3IvdjEvc2VydmljZS5wcm90bxIJdGFpbG9yLnYxIg0KC1BpbmdSZXF1ZXN0Ig4KDFBpbmdSZXNwb25zZTKaywEKD09wZXJhdG9yU2VydmljZRI5CgRQaW5nEhYudGFpbG9yLnYxLlBpbmdSZXF1ZXN0GhcudGFpbG9yLnYxLlBpbmdSZXNwb25zZSIAEocBCh1MaXN0QXZhaWxhYmxlV29ya3NwYWNlUmVnaW9ucxIvLnRhaWxvci52MS5MaXN0QXZhaWxhYmxlV29ya3NwYWNlUmVnaW9uc1JlcXVlc3QaMC50YWlsb3IudjEuTGlzdEF2YWlsYWJsZVdvcmtzcGFjZVJlZ2lvbnNSZXNwb25zZSIDkAIBEloKD0NyZWF0ZVdvcmtzcGFjZRIhLnRhaWxvci52MS5DcmVhdGVXb3Jrc3BhY2VSZXF1ZXN0GiIudGFpbG9yLnYxLkNyZWF0ZVdvcmtzcGFjZVJlc3BvbnNlIgASWgoPVXBkYXRlV29ya3NwYWNlEiEudGFpbG9yLnYxLlVwZGF0ZVdvcmtzcGFjZVJlcXVlc3QaIi50YWlsb3IudjEuVXBkYXRlV29ya3NwYWNlUmVzcG9uc2UiABJaCg9EZWxldGVXb3Jrc3BhY2USIS50YWlsb3IudjEuRGVsZXRlV29ya3NwYWNlUmVxdWVzdBoiLnRhaWxvci52MS5EZWxldGVXb3Jrc3BhY2VSZXNwb25zZSIAEloKDkxpc3RXb3Jrc3BhY2VzEiAudGFpbG9yLnYxLkxpc3RXb3Jrc3BhY2VzUmVxdWVzdBohLnRhaWxvci52MS5MaXN0V29ya3NwYWNlc1Jlc3BvbnNlIgOQAgESfgoaTGlzdE9yZ2FuaXphdGlvbldvcmtzcGFjZXMSLC50YWlsb3IudjEuTGlzdE9yZ2FuaXphdGlvbldvcmtzcGFjZXNSZXF1ZXN0Gi0udGFpbG9yLnYxLkxpc3RPcmdhbml6YXRpb25Xb3Jrc3BhY2VzUmVzcG9uc2UiA5ACARJdChBSZXN0b3JlV29ya3NwYWNlEiIudGFpbG9yLnYxLlJlc3RvcmVXb3Jrc3BhY2VSZXF1ZXN0GiMudGFpbG9yLnYxLlJlc3RvcmVXb3Jrc3BhY2VSZXNwb25zZSIAElQKDEdldFdvcmtzcGFjZRIeLnRhaWxvci52MS5HZXRXb3Jrc3BhY2VSZXF1ZXN0Gh8udGFpbG9yLnYxLkdldFdvcmtzcGFjZVJlc3BvbnNlIgOQAgESfgoaTGlzdFdvcmtzcGFjZVBsYXRmb3JtVXNlcnMSLC50YWlsb3IudjEuTGlzdFdvcmtzcGFjZVBsYXRmb3JtVXNlcnNSZXF1ZXN0Gi0udGFpbG9yLnYxLkxpc3RXb3Jrc3BhY2VQbGF0Zm9ybVVzZXJzUmVzcG9uc2UiA5ACARKlAQonTGlzdEF2YWlsYWJsZVdvcmtzcGFjZVBsYXRmb3JtVXNlclJvbGVzEjkudGFpbG9yLnYxLkxpc3RBdmFpbGFibGVXb3Jrc3BhY2VQbGF0Zm9ybVVzZXJSb2xlc1JlcXVlc3QaOi50YWlsb3IudjEuTGlzdEF2YWlsYWJsZVdvcmtzcGFjZVBsYXRmb3JtVXNlclJvbGVzUmVzcG9uc2UiA5ACARJ+ChtJbnZpdGVXb3Jrc3BhY2VQbGF0Zm9ybVVzZXISLS50YWlsb3IudjEuSW52aXRlV29ya3NwYWNlUGxhdGZvcm1Vc2VyUmVxdWVzdBouLnRhaWxvci52MS5JbnZpdGVXb3Jrc3BhY2VQbGF0Zm9ybVVzZXJSZXNwb25zZSIAEn4KG1JlbW92ZVdvcmtzcGFjZVBsYXRmb3JtVXNlchItLnRhaWxvci52MS5SZW1vdmVXb3Jrc3BhY2VQbGF0Zm9ybVVzZXJSZXF1ZXN0Gi4udGFpbG9yLnYxLlJlbW92ZVdvcmtzcGFjZVBsYXRmb3JtVXNlclJlc3BvbnNlIgASfgobVXBkYXRlV29ya3NwYWNlUGxhdGZvcm1Vc2VyEi0udGFpbG9yLnYxLlVwZGF0ZVdvcmtzcGFjZVBsYXRmb3JtVXNlclJlcXVlc3QaLi50YWlsb3IudjEuVXBkYXRlV29ya3NwYWNlUGxhdGZvcm1Vc2VyUmVzcG9uc2UiABJ4ChhHZXRXb3Jrc3BhY2VQbGF0Zm9ybVVzZXISKi50YWlsb3IudjEuR2V0V29ya3NwYWNlUGxhdGZvcm1Vc2VyUmVxdWVzdBorLnRhaWxvci52MS5HZXRXb3Jrc3BhY2VQbGF0Zm9ybVVzZXJSZXNwb25zZSIDkAIBEmAKEEdldFdvcmtzcGFjZVJvbGUSIi50YWlsb3IudjEuR2V0V29ya3NwYWNlUm9sZVJlcXVlc3QaIy50YWlsb3IudjEuR2V0V29ya3NwYWNlUm9sZVJlc3BvbnNlIgOQAgESYwoSVXBkYXRlT3JnYW5pemF0aW9uEiQudGFpbG9yLnYxLlVwZGF0ZU9yZ2FuaXphdGlvblJlcXVlc3QaJS50YWlsb3IudjEuVXBkYXRlT3JnYW5pemF0aW9uUmVzcG9uc2UiABJdCg9HZXRPcmdhbml6YXRpb24SIS50YWlsb3IudjEuR2V0T3JnYW5pemF0aW9uUmVxdWVzdBoiLnRhaWxvci52MS5HZXRPcmdhbml6YXRpb25SZXNwb25zZSIDkAIBEmMKEUxpc3RPcmdhbml6YXRpb25zEiMudGFpbG9yLnYxLkxpc3RPcmdhbml6YXRpb25zUmVxdWVzdBokLnRhaWxvci52MS5MaXN0T3JnYW5pemF0aW9uc1Jlc3BvbnNlIgOQAgESbwoVTGlzdFVzZXJPcmdhbml6YXRpb25zEicudGFpbG9yLnYxLkxpc3RVc2VyT3JnYW5pemF0aW9uc1JlcXVlc3QaKC50YWlsb3IudjEuTGlzdFVzZXJPcmdhbml6YXRpb25zUmVzcG9uc2UiA5ACARJyChdHcmFudE9yZ2FuaXphdGlvbkFjY2VzcxIpLnRhaWxvci52MS5HcmFudE9yZ2FuaXphdGlvbkFjY2Vzc1JlcXVlc3QaKi50YWlsb3IudjEuR3JhbnRPcmdhbml6YXRpb25BY2Nlc3NSZXNwb25zZSIAEnUKGFVwZGF0ZU9yZ2FuaXphdGlvbkFjY2VzcxIqLnRhaWxvci52MS5VcGRhdGVPcmdhbml6YXRpb25BY2Nlc3NSZXF1ZXN0GisudGFpbG9yLnYxLlVwZGF0ZU9yZ2FuaXphdGlvbkFjY2Vzc1Jlc3BvbnNlIgASdQoYUmV2b2tlT3JnYW5pemF0aW9uQWNjZXNzEioudGFpbG9yLnYxLlJldm9rZU9yZ2FuaXphdGlvbkFjY2Vzc1JlcXVlc3QaKy50YWlsb3IudjEuUmV2b2tlT3JnYW5pemF0aW9uQWNjZXNzUmVzcG9uc2UiABJ4ChhMaXN0T3JnYW5pemF0aW9uQWNjZXNzZXMSKi50YWlsb3IudjEuTGlzdE9yZ2FuaXphdGlvbkFjY2Vzc2VzUmVxdWVzdBorLnRhaWxvci52MS5MaXN0T3JnYW5pemF0aW9uQWNjZXNzZXNSZXNwb25zZSIDkAIBEm8KFUdldE9yZ2FuaXphdGlvbkFjY2VzcxInLnRhaWxvci52MS5HZXRPcmdhbml6YXRpb25BY2Nlc3NSZXF1ZXN0GigudGFpbG9yLnYxLkdldE9yZ2FuaXphdGlvbkFjY2Vzc1Jlc3BvbnNlIgOQAgESdQoYQ3JlYXRlT3JnYW5pemF0aW9uRm9sZGVyEioudGFpbG9yLnYxLkNyZWF0ZU9yZ2FuaXphdGlvbkZvbGRlclJlcXVlc3QaKy50YWlsb3IudjEuQ3JlYXRlT3JnYW5pemF0aW9uRm9sZGVyUmVzcG9uc2UiABJ1ChhVcGRhdGVPcmdhbml6YXRpb25Gb2xkZXISKi50YWlsb3IudjEuVXBkYXRlT3JnYW5pemF0aW9uRm9sZGVyUmVxdWVzdBorLnRhaWxvci52MS5VcGRhdGVPcmdhbml6YXRpb25Gb2xkZXJSZXNwb25zZSIAEnUKGERlbGV0ZU9yZ2FuaXphdGlvbkZvbGRlchIqLnRhaWxvci52MS5EZWxldGVPcmdhbml6YXRpb25Gb2xkZXJSZXF1ZXN0GisudGFpbG9yLnYxLkRlbGV0ZU9yZ2FuaXphdGlvbkZvbGRlclJlc3BvbnNlIgASbwoVR2V0T3JnYW5pemF0aW9uRm9sZGVyEicudGFpbG9yLnYxLkdldE9yZ2FuaXphdGlvbkZvbGRlclJlcXVlc3QaKC50YWlsb3IudjEuR2V0T3JnYW5pemF0aW9uRm9sZGVyUmVzcG9uc2UiA5ACARJ1ChdMaXN0T3JnYW5pemF0aW9uRm9sZGVycxIpLnRhaWxvci52MS5MaXN0T3JnYW5pemF0aW9uRm9sZGVyc1JlcXVlc3QaKi50YWlsb3IudjEuTGlzdE9yZ2FuaXphdGlvbkZvbGRlcnNSZXNwb25zZSIDkAIBEoQBCh1HcmFudE9yZ2FuaXphdGlvbkZvbGRlckFjY2VzcxIvLnRhaWxvci52MS5HcmFudE9yZ2FuaXphdGlvbkZvbGRlckFjY2Vzc1JlcXVlc3QaMC50YWlsb3IudjEuR3JhbnRPcmdhbml6YXRpb25Gb2xkZXJBY2Nlc3NSZXNwb25zZSIAEocBCh5VcGRhdGVPcmdhbml6YXRpb25Gb2xkZXJBY2Nlc3MSMC50YWlsb3IudjEuVXBkYXRlT3JnYW5pemF0aW9uRm9sZGVyQWNjZXNzUmVxdWVzdBoxLnRhaWxvci52MS5VcGRhdGVPcmdhbml6YXRpb25Gb2xkZXJBY2Nlc3NSZXNwb25zZSIAEocBCh5SZXZva2VPcmdhbml6YXRpb25Gb2xkZXJBY2Nlc3MSMC50YWlsb3IudjEuUmV2b2tlT3JnYW5pemF0aW9uRm9sZGVyQWNjZXNzUmVxdWVzdBoxLnRhaWxvci52MS5SZXZva2VPcmdhbml6YXRpb25Gb2xkZXJBY2Nlc3NSZXNwb25zZSIAEooBCh5MaXN0T3JnYW5pemF0aW9uRm9sZGVyQWNjZXNzZXMSMC50YWlsb3IudjEuTGlzdE9yZ2FuaXphdGlvbkZvbGRlckFjY2Vzc2VzUmVxdWVzdBoxLnRhaWxvci52MS5MaXN0T3JnYW5pemF0aW9uRm9sZGVyQWNjZXNzZXNSZXNwb25zZSIDkAIBEoEBChtHZXRPcmdhbml6YXRpb25Gb2xkZXJBY2Nlc3MSLS50YWlsb3IudjEuR2V0T3JnYW5pemF0aW9uRm9sZGVyQWNjZXNzUmVxdWVzdBouLnRhaWxvci52MS5HZXRPcmdhbml6YXRpb25Gb2xkZXJBY2Nlc3NSZXNwb25zZSIDkAIBEooBCh9VcHNlcnRPcmdhbml6YXRpb25JUFJlc3RyaWN0aW9uEjEudGFpbG9yLnYxLlVwc2VydE9yZ2FuaXphdGlvbklQUmVzdHJpY3Rpb25SZXF1ZXN0GjIudGFpbG9yLnYxLlVwc2VydE9yZ2FuaXphdGlvbklQUmVzdHJpY3Rpb25SZXNwb25zZSIAEoQBChxHZXRPcmdhbml6YXRpb25JUFJlc3RyaWN0aW9uEi4udGFpbG9yLnYxLkdldE9yZ2FuaXphdGlvbklQUmVzdHJpY3Rpb25SZXF1ZXN0Gi8udGFpbG9yLnYxLkdldE9yZ2FuaXphdGlvbklQUmVzdHJpY3Rpb25SZXNwb25zZSIDkAIBEooBCh9EZWxldGVPcmdhbml6YXRpb25JUFJlc3RyaWN0aW9uEjEudGFpbG9yLnYxLkRlbGV0ZU9yZ2FuaXphdGlvbklQUmVzdHJpY3Rpb25SZXF1ZXN0GjIudGFpbG9yLnYxLkRlbGV0ZU9yZ2FuaXphdGlvbklQUmVzdHJpY3Rpb25SZXNwb25zZSIAEpwBCiVVcHNlcnRPcmdhbml6YXRpb25Gb2xkZXJJUFJlc3RyaWN0aW9uEjcudGFpbG9yLnYxLlVwc2VydE9yZ2FuaXphdGlvbkZvbGRlcklQUmVzdHJpY3Rpb25SZXF1ZXN0GjgudGFpbG9yLnYxLlVwc2VydE9yZ2FuaXphdGlvbkZvbGRlcklQUmVzdHJpY3Rpb25SZXNwb25zZSIAEpYBCiJHZXRPcmdhbml6YXRpb25Gb2xkZXJJUFJlc3RyaWN0aW9uEjQudGFpbG9yLnYxLkdldE9yZ2FuaXphdGlvbkZvbGRlcklQUmVzdHJpY3Rpb25SZXF1ZXN0GjUudGFpbG9yLnYxLkdldE9yZ2FuaXphdGlvbkZvbGRlcklQUmVzdHJpY3Rpb25SZXNwb25zZSIDkAIBEpwBCiVEZWxldGVPcmdhbml6YXRpb25Gb2xkZXJJUFJlc3RyaWN0aW9uEjcudGFpbG9yLnYxLkRlbGV0ZU9yZ2FuaXphdGlvbkZvbGRlcklQUmVzdHJpY3Rpb25SZXF1ZXN0GjgudGFpbG9yLnYxLkRlbGV0ZU9yZ2FuaXphdGlvbkZvbGRlcklQUmVzdHJpY3Rpb25SZXNwb25zZSIAEm8KFkNyZWF0ZU9yZ2FuaXphdGlvblRlYW0SKC50YWlsb3IudjEuQ3JlYXRlT3JnYW5pemF0aW9uVGVhbVJlcXVlc3QaKS50YWlsb3IudjEuQ3JlYXRlT3JnYW5pemF0aW9uVGVhbVJlc3BvbnNlIgASbwoWVXBkYXRlT3JnYW5pemF0aW9uVGVhbRIoLnRhaWxvci52MS5VcGRhdGVPcmdhbml6YXRpb25UZWFtUmVxdWVzdBopLnRhaWxvci52MS5VcGRhdGVPcmdhbml6YXRpb25UZWFtUmVzcG9uc2UiABJvChZEZWxldGVPcmdhbml6YXRpb25UZWFtEigudGFpbG9yLnYxLkRlbGV0ZU9yZ2FuaXphdGlvblRlYW1SZXF1ZXN0GikudGFpbG9yLnYxLkRlbGV0ZU9yZ2FuaXphdGlvblRlYW1SZXNwb25zZSIAEmkKE0dldE9yZ2FuaXphdGlvblRlYW0SJS50YWlsb3IudjEuR2V0T3JnYW5pemF0aW9uVGVhbVJlcXVlc3QaJi50YWlsb3IudjEuR2V0T3JnYW5pemF0aW9uVGVhbVJlc3BvbnNlIgOQAgESbwoVTGlzdE9yZ2FuaXphdGlvblRlYW1zEicudGFpbG9yLnYxLkxpc3RPcmdhbml6YXRpb25UZWFtc1JlcXVlc3QaKC50YWlsb3IudjEuTGlzdE9yZ2FuaXphdGlvblRlYW1zUmVzcG9uc2UiA5ACARJ4ChlBZGRPcmdhbml6YXRpb25UZWFtTWVtYmVyEisudGFpbG9yLnYxLkFkZE9yZ2FuaXphdGlvblRlYW1NZW1iZXJSZXF1ZXN0GiwudGFpbG9yLnYxLkFkZE9yZ2FuaXphdGlvblRlYW1NZW1iZXJSZXNwb25zZSIAEoEBChxVcGRhdGVPcmdhbml6YXRpb25UZWFtTWVtYmVyEi4udGFpbG9yLnYxLlVwZGF0ZU9yZ2FuaXphdGlvblRlYW1NZW1iZXJSZXF1ZXN0Gi8udGFpbG9yLnYxLlVwZGF0ZU9yZ2FuaXphdGlvblRlYW1NZW1iZXJSZXNwb25zZSIAEoEBChxSZW1vdmVPcmdhbml6YXRpb25UZWFtTWVtYmVyEi4udGFpbG9yLnYxLlJlbW92ZU9yZ2FuaXphdGlvblRlYW1NZW1iZXJSZXF1ZXN0Gi8udGFpbG9yLnYxLlJlbW92ZU9yZ2FuaXphdGlvblRlYW1NZW1iZXJSZXNwb25zZSIAEoEBChtMaXN0T3JnYW5pemF0aW9uVGVhbU1lbWJlcnMSLS50YWlsb3IudjEuTGlzdE9yZ2FuaXphdGlvblRlYW1NZW1iZXJzUmVxdWVzdBouLnRhaWxvci52MS5MaXN0T3JnYW5pemF0aW9uVGVhbU1lbWJlcnNSZXNwb25zZSIDkAIBEnsKGUdldE9yZ2FuaXphdGlvblRlYW1NZW1iZXISKy50YWlsb3IudjEuR2V0T3JnYW5pemF0aW9uVGVhbU1lbWJlclJlcXVlc3QaLC50YWlsb3IudjEuR2V0T3JnYW5pemF0aW9uVGVhbU1lbWJlclJlc3BvbnNlIgOQAgEScgoWR2V0UGxhdGZvcm1BY2NvdW50UGxhbhIoLnRhaWxvci52MS5HZXRQbGF0Zm9ybUFjY291bnRQbGFuUmVxdWVzdBopLnRhaWxvci52MS5HZXRQbGF0Zm9ybUFjY291bnRQbGFuUmVzcG9uc2UiA5ACARJgChFDcmVhdGVBcHBsaWNhdGlvbhIjLnRhaWxvci52MS5DcmVhdGVBcHBsaWNhdGlvblJlcXVlc3QaJC50YWlsb3IudjEuQ3JlYXRlQXBwbGljYXRpb25SZXNwb25zZSIAEmAKEVVwZGF0ZUFwcGxpY2F0aW9uEiMudGFpbG9yLnYxLlVwZGF0ZUFwcGxpY2F0aW9uUmVxdWVzdBokLnRhaWxvci52MS5VcGRhdGVBcHBsaWNhdGlvblJlc3BvbnNlIgASYAoRRGVsZXRlQXBwbGljYXRpb24SIy50YWlsb3IudjEuRGVsZXRlQXBwbGljYXRpb25SZXF1ZXN0GiQudGFpbG9yLnYxLkRlbGV0ZUFwcGxpY2F0aW9uUmVzcG9uc2UiABJgChBMaXN0QXBwbGljYXRpb25zEiIudGFpbG9yLnYxLkxpc3RBcHBsaWNhdGlvbnNSZXF1ZXN0GiMudGFpbG9yLnYxLkxpc3RBcHBsaWNhdGlvbnNSZXNwb25zZSIDkAIBEloKDkdldEFwcGxpY2F0aW9uEiAudGFpbG9yLnYxLkdldEFwcGxpY2F0aW9uUmVxdWVzdBohLnRhaWxvci52MS5HZXRBcHBsaWNhdGlvblJlc3BvbnNlIgOQAgESfgoaR2V0QXBwbGljYXRpb25TY2hlbWFIZWFsdGgSLC50YWlsb3IudjEuR2V0QXBwbGljYXRpb25TY2hlbWFIZWFsdGhSZXF1ZXN0Gi0udGFpbG9yLnYxLkdldEFwcGxpY2F0aW9uU2NoZW1hSGVhbHRoUmVzcG9uc2UiA5ACARJjChJDb21wb3NlVGFpbG9yREJTREwSJC50YWlsb3IudjEuQ29tcG9zZVRhaWxvckRCU0RMUmVxdWVzdBolLnRhaWxvci52MS5Db21wb3NlVGFpbG9yREJTRExSZXNwb25zZSIAEmwKFUNyZWF0ZVRhaWxvckRCU2VydmljZRInLnRhaWxvci52MS5DcmVhdGVUYWlsb3JEQlNlcnZpY2VSZXF1ZXN0GigudGFpbG9yLnYxLkNyZWF0ZVRhaWxvckRCU2VydmljZVJlc3BvbnNlIgASbAoVVXBkYXRlVGFpbG9yREJTZXJ2aWNlEicudGFpbG9yLnYxLlVwZGF0ZVRhaWxvckRCU2VydmljZVJlcXVlc3QaKC50YWlsb3IudjEuVXBkYXRlVGFpbG9yREJTZXJ2aWNlUmVzcG9uc2UiABJsChVEZWxldGVUYWlsb3JEQlNlcnZpY2USJy50YWlsb3IudjEuRGVsZXRlVGFpbG9yREJTZXJ2aWNlUmVxdWVzdBooLnRhaWxvci52MS5EZWxldGVUYWlsb3JEQlNlcnZpY2VSZXNwb25zZSIAEmYKEkdldFRhaWxvckRCU2VydmljZRIkLnRhaWxvci52MS5HZXRUYWlsb3JEQlNlcnZpY2VSZXF1ZXN0GiUudGFpbG9yLnYxLkdldFRhaWxvckRCU2VydmljZVJlc3BvbnNlIgOQAgESbAoUTGlzdFRhaWxvckRCU2VydmljZXMSJi50YWlsb3IudjEuTGlzdFRhaWxvckRCU2VydmljZXNSZXF1ZXN0GicudGFpbG9yLnYxLkxpc3RUYWlsb3JEQlNlcnZpY2VzUmVzcG9uc2UiA5ACARJjChJDcmVhdGVUYWlsb3JEQlR5cGUSJC50YWlsb3IudjEuQ3JlYXRlVGFpbG9yREJUeXBlUmVxdWVzdBolLnRhaWxvci52MS5DcmVhdGVUYWlsb3JEQlR5cGVSZXNwb25zZSIAEmMKElVwZGF0ZVRhaWxvckRCVHlwZRIkLnRhaWxvci52MS5VcGRhdGVUYWlsb3JEQlR5cGVSZXF1ZXN0GiUudGFpbG9yLnYxLlVwZGF0ZVRhaWxvckRCVHlwZVJlc3BvbnNlIgASYwoSRGVsZXRlVGFpbG9yREJUeXBlEiQudGFpbG9yLnYxLkRlbGV0ZVRhaWxvckRCVHlwZVJlcXVlc3QaJS50YWlsb3IudjEuRGVsZXRlVGFpbG9yREJUeXBlUmVzcG9uc2UiABJsChVUcnVuY2F0ZVRhaWxvckRCVHlwZXMSJy50YWlsb3IudjEuVHJ1bmNhdGVUYWlsb3JEQlR5cGVzUmVxdWVzdBooLnRhaWxvci52MS5UcnVuY2F0ZVRhaWxvckRCVHlwZXNSZXNwb25zZSIAEmkKFFRydW5jYXRlVGFpbG9yREJUeXBlEiYudGFpbG9yLnYxLlRydW5jYXRlVGFpbG9yREJUeXBlUmVxdWVzdBonLnRhaWxvci52MS5UcnVuY2F0ZVRhaWxvckRCVHlwZVJlc3BvbnNlIgASYwoRTGlzdFRhaWxvckRCVHlwZXMSIy50YWlsb3IudjEuTGlzdFRhaWxvckRCVHlwZXNSZXF1ZXN0GiQudGFpbG9yLnYxLkxpc3RUYWlsb3JEQlR5cGVzUmVzcG9uc2UiA5ACARJdCg9HZXRUYWlsb3JEQlR5cGUSIS50YWlsb3IudjEuR2V0VGFpbG9yREJUeXBlUmVxdWVzdBoiLnRhaWxvci52MS5HZXRUYWlsb3JEQlR5cGVSZXNwb25zZSIDkAIBEn4KG0NyZWF0ZVRhaWxvckRCR1FMUGVybWlzc2lvbhItLnRhaWxvci52MS5DcmVhdGVUYWlsb3JEQkdRTFBlcm1pc3Npb25SZXF1ZXN0Gi4udGFpbG9yLnYxLkNyZWF0ZVRhaWxvckRCR1FMUGVybWlzc2lvblJlc3BvbnNlIgASeAoYR2V0VGFpbG9yREJHUUxQZXJtaXNzaW9uEioudGFpbG9yLnYxLkdldFRhaWxvckRCR1FMUGVybWlzc2lvblJlcXVlc3QaKy50YWlsb3IudjEuR2V0VGFpbG9yREJHUUxQZXJtaXNzaW9uUmVzcG9uc2UiA5ACARJ+ChpMaXN0VGFpbG9yREJHUUxQZXJtaXNzaW9ucxIsLnRhaWxvci52MS5MaXN0VGFpbG9yREJHUUxQZXJtaXNzaW9uc1JlcXVlc3QaLS50YWlsb3IudjEuTGlzdFRhaWxvckRCR1FMUGVybWlzc2lvbnNSZXNwb25zZSIDkAIBEn4KG1VwZGF0ZVRhaWxvckRCR1FMUGVybWlzc2lvbhItLnRhaWxvci52MS5VcGRhdGVUYWlsb3JEQkdRTFBlcm1pc3Npb25SZXF1ZXN0Gi4udGFpbG9yLnYxLlVwZGF0ZVRhaWxvckRCR1FMUGVybWlzc2lvblJlc3BvbnNlIgASfgobRGVsZXRlVGFpbG9yREJHUUxQZXJtaXNzaW9uEi0udGFpbG9yLnYxLkRlbGV0ZVRhaWxvckRCR1FMUGVybWlzc2lvblJlcXVlc3QaLi50YWlsb3IudjEuRGVsZXRlVGFpbG9yREJHUUxQZXJtaXNzaW9uUmVzcG9uc2UiABJsChVDcmVhdGVQaXBlbGluZVNlcnZpY2USJy50YWlsb3IudjEuQ3JlYXRlUGlwZWxpbmVTZXJ2aWNlUmVxdWVzdBooLnRhaWxvci52MS5DcmVhdGVQaXBlbGluZVNlcnZpY2VSZXNwb25zZSIAEmwKFVVwZGF0ZVBpcGVsaW5lU2VydmljZRInLnRhaWxvci52MS5VcGRhdGVQaXBlbGluZVNlcnZpY2VSZXF1ZXN0GigudGFpbG9yLnYxLlVwZGF0ZVBpcGVsaW5lU2VydmljZVJlc3BvbnNlIgASbAoVRGVsZXRlUGlwZWxpbmVTZXJ2aWNlEicudGFpbG9yLnYxLkRlbGV0ZVBpcGVsaW5lU2VydmljZVJlcXVlc3QaKC50YWlsb3IudjEuRGVsZXRlUGlwZWxpbmVTZXJ2aWNlUmVzcG9uc2UiABJmChJHZXRQaXBlbGluZVNlcnZpY2USJC50YWlsb3IudjEuR2V0UGlwZWxpbmVTZXJ2aWNlUmVxdWVzdBolLnRhaWxvci52MS5HZXRQaXBlbGluZVNlcnZpY2VSZXNwb25zZSIDkAIBEmwKFExpc3RQaXBlbGluZVNlcnZpY2VzEiYudGFpbG9yLnYxLkxpc3RQaXBlbGluZVNlcnZpY2VzUmVxdWVzdBonLnRhaWxvci52MS5MaXN0UGlwZWxpbmVTZXJ2aWNlc1Jlc3BvbnNlIgOQAgESaQoTR2V0UGlwZWxpbmVSZXNvbHZlchIlLnRhaWxvci52MS5HZXRQaXBlbGluZVJlc29sdmVyUmVxdWVzdBomLnRhaWxvci52MS5HZXRQaXBlbGluZVJlc29sdmVyUmVzcG9uc2UiA5ACARJvChVMaXN0UGlwZWxpbmVSZXNvbHZlcnMSJy50YWlsb3IudjEuTGlzdFBpcGVsaW5lUmVzb2x2ZXJzUmVxdWVzdBooLnRhaWxvci52MS5MaXN0UGlwZWxpbmVSZXNvbHZlcnNSZXNwb25zZSIDkAIBEm8KFkNyZWF0ZVBpcGVsaW5lUmVzb2x2ZXISKC50YWlsb3IudjEuQ3JlYXRlUGlwZWxpbmVSZXNvbHZlclJlcXVlc3QaKS50YWlsb3IudjEuQ3JlYXRlUGlwZWxpbmVSZXNvbHZlclJlc3BvbnNlIgASbwoWVXBkYXRlUGlwZWxpbmVSZXNvbHZlchIoLnRhaWxvci52MS5VcGRhdGVQaXBlbGluZVJlc29sdmVyUmVxdWVzdBopLnRhaWxvci52MS5VcGRhdGVQaXBlbGluZVJlc29sdmVyUmVzcG9uc2UiABJvChZEZWxldGVQaXBlbGluZVJlc29sdmVyEigudGFpbG9yLnYxLkRlbGV0ZVBpcGVsaW5lUmVzb2x2ZXJSZXF1ZXN0GikudGFpbG9yLnYxLkRlbGV0ZVBpcGVsaW5lUmVzb2x2ZXJSZXNwb25zZSIAEmMKEkNvbXBvc2VQaXBlbGluZVNETBIkLnRhaWxvci52MS5Db21wb3NlUGlwZWxpbmVTRExSZXF1ZXN0GiUudGFpbG9yLnYxLkNvbXBvc2VQaXBlbGluZVNETFJlc3BvbnNlIgASnAEKJExpc3RQaXBlbGluZVJlc29sdmVyRXhlY3V0aW9uUmVzdWx0cxI2LnRhaWxvci52MS5MaXN0UGlwZWxpbmVSZXNvbHZlckV4ZWN1dGlvblJlc3VsdHNSZXF1ZXN0GjcudGFpbG9yLnYxLkxpc3RQaXBlbGluZVJlc29sdmVyRXhlY3V0aW9uUmVzdWx0c1Jlc3BvbnNlIgOQAgESlgEKIkdldFBpcGVsaW5lUmVzb2x2ZXJFeGVjdXRpb25SZXN1bHQSNC50YWlsb3IudjEuR2V0UGlwZWxpbmVSZXNvbHZlckV4ZWN1dGlvblJlc3VsdFJlcXVlc3QaNS50YWlsb3IudjEuR2V0UGlwZWxpbmVSZXNvbHZlckV4ZWN1dGlvblJlc3VsdFJlc3BvbnNlIgOQAgEScgoXUmVzdGFydFBpcGVsaW5lUmVzb2x2ZXISKS50YWlsb3IudjEuUmVzdGFydFBpcGVsaW5lUmVzb2x2ZXJSZXF1ZXN0GioudGFpbG9yLnYxLlJlc3RhcnRQaXBlbGluZVJlc29sdmVyUmVzcG9uc2UiABJvChZDcmVhdGVTdGF0ZWZsb3dTZXJ2aWNlEigudGFpbG9yLnYxLkNyZWF0ZVN0YXRlZmxvd1NlcnZpY2VSZXF1ZXN0GikudGFpbG9yLnYxLkNyZWF0ZVN0YXRlZmxvd1NlcnZpY2VSZXNwb25zZSIAEm8KFlVwZGF0ZVN0YXRlZmxvd1NlcnZpY2USKC50YWlsb3IudjEuVXBkYXRlU3RhdGVmbG93U2VydmljZVJlcXVlc3QaKS50YWlsb3IudjEuVXBkYXRlU3RhdGVmbG93U2VydmljZVJlc3BvbnNlIgASbwoWRGVsZXRlU3RhdGVmbG93U2VydmljZRIoLnRhaWxvci52MS5EZWxldGVTdGF0ZWZsb3dTZXJ2aWNlUmVxdWVzdBopLnRhaWxvci52MS5EZWxldGVTdGF0ZWZsb3dTZXJ2aWNlUmVzcG9uc2UiABJpChNHZXRTdGF0ZWZsb3dTZXJ2aWNlEiUudGFpbG9yLnYxLkdldFN0YXRlZmxvd1NlcnZpY2VSZXF1ZXN0GiYudGFpbG9yLnYxLkdldFN0YXRlZmxvd1NlcnZpY2VSZXNwb25zZSIDkAIBEm8KFUxpc3RTdGF0ZWZsb3dTZXJ2aWNlcxInLnRhaWxvci52MS5MaXN0U3RhdGVmbG93U2VydmljZXNSZXF1ZXN0GigudGFpbG9yLnYxLkxpc3RTdGF0ZWZsb3dTZXJ2aWNlc1Jlc3BvbnNlIgOQAgESbwoWQ3JlYXRlRXhlY3V0b3JFeGVjdXRvchIoLnRhaWxvci52MS5DcmVhdGVFeGVjdXRvckV4ZWN1dG9yUmVxdWVzdBopLnRhaWxvci52MS5DcmVhdGVFeGVjdXRvckV4ZWN1dG9yUmVzcG9uc2UiABJvChZVcGRhdGVFeGVjdXRvckV4ZWN1dG9yEigudGFpbG9yLnYxLlVwZGF0ZUV4ZWN1dG9yRXhlY3V0b3JSZXF1ZXN0GikudGFpbG9yLnYxLlVwZGF0ZUV4ZWN1dG9yRXhlY3V0b3JSZXNwb25zZSIAEmkKE0dldEV4ZWN1dG9yRXhlY3V0b3ISJS50YWlsb3IudjEuR2V0RXhlY3V0b3JFeGVjdXRvclJlcXVlc3QaJi50YWlsb3IudjEuR2V0RXhlY3V0b3JFeGVjdXRvclJlc3BvbnNlIgOQAgESbwoWRGVsZXRlRXhlY3V0b3JFeGVjdXRvchIoLnRhaWxvci52MS5EZWxldGVFeGVjdXRvckV4ZWN1dG9yUmVxdWVzdBopLnRhaWxvci52MS5EZWxldGVFeGVjdXRvckV4ZWN1dG9yUmVzcG9uc2UiABJvChVMaXN0RXhlY3V0b3JFeGVjdXRvcnMSJy50YWlsb3IudjEuTGlzdEV4ZWN1dG9yRXhlY3V0b3JzUmVxdWVzdBooLnRhaWxvci52MS5MaXN0RXhlY3V0b3JFeGVjdXRvcnNSZXNwb25zZSIDkAIBEloKDkdldEV4ZWN1dG9ySm9iEiAudGFpbG9yLnYxLkdldEV4ZWN1dG9ySm9iUmVxdWVzdBohLnRhaWxvci52MS5HZXRFeGVjdXRvckpvYlJlc3BvbnNlIgOQAgESYAoQTGlzdEV4ZWN1dG9ySm9icxIiLnRhaWxvci52MS5MaXN0RXhlY3V0b3JKb2JzUmVxdWVzdBojLnRhaWxvci52MS5MaXN0RXhlY3V0b3JKb2JzUmVzcG9uc2UiA5ACARJ1ChdMaXN0RXhlY3V0b3JKb2JBdHRlbXB0cxIpLnRhaWxvci52MS5MaXN0RXhlY3V0b3JKb2JBdHRlbXB0c1JlcXVlc3QaKi50YWlsb3IudjEuTGlzdEV4ZWN1dG9ySm9iQXR0ZW1wdHNSZXNwb25zZSIDkAIBEoQBChxMaXN0RXhlY3V0b3JJbmNvbWluZ1dlYmhvb2tzEi4udGFpbG9yLnYxLkxpc3RFeGVjdXRvckluY29taW5nV2ViaG9va3NSZXF1ZXN0Gi8udGFpbG9yLnYxLkxpc3RFeGVjdXRvckluY29taW5nV2ViaG9va3NSZXNwb25zZSIDkAIBEn4KGkdldEV4ZWN1dG9ySW5jb21pbmdXZWJob29rEiwudGFpbG9yLnYxLkdldEV4ZWN1dG9ySW5jb21pbmdXZWJob29rUmVxdWVzdBotLnRhaWxvci52MS5HZXRFeGVjdXRvckluY29taW5nV2ViaG9va1Jlc3BvbnNlIgOQAgESWgoPVHJpZ2dlckV4ZWN1dG9yEiEudGFpbG9yLnYxLlRyaWdnZXJFeGVjdXRvclJlcXVlc3QaIi50YWlsb3IudjEuVHJpZ2dlckV4ZWN1dG9yUmVzcG9uc2UiABJ1ChhDcmVhdGVTZWNyZXRNYW5hZ2VyVmF1bHQSKi50YWlsb3IudjEuQ3JlYXRlU2VjcmV0TWFuYWdlclZhdWx0UmVxdWVzdBorLnRhaWxvci52MS5DcmVhdGVTZWNyZXRNYW5hZ2VyVmF1bHRSZXNwb25zZSIAEm8KFUdldFNlY3JldE1hbmFnZXJWYXVsdBInLnRhaWxvci52MS5HZXRTZWNyZXRNYW5hZ2VyVmF1bHRSZXF1ZXN0GigudGFpbG9yLnYxLkdldFNlY3JldE1hbmFnZXJWYXVsdFJlc3BvbnNlIgOQAgESdQoYRGVsZXRlU2VjcmV0TWFuYWdlclZhdWx0EioudGFpbG9yLnYxLkRlbGV0ZVNlY3JldE1hbmFnZXJWYXVsdFJlcXVlc3QaKy50YWlsb3IudjEuRGVsZXRlU2VjcmV0TWFuYWdlclZhdWx0UmVzcG9uc2UiABJ1ChdMaXN0U2VjcmV0TWFuYWdlclZhdWx0cxIpLnRhaWxvci52MS5MaXN0U2VjcmV0TWFuYWdlclZhdWx0c1JlcXVlc3QaKi50YWlsb3IudjEuTGlzdFNlY3JldE1hbmFnZXJWYXVsdHNSZXNwb25zZSIDkAIBEngKGUNyZWF0ZVNlY3JldE1hbmFnZXJTZWNyZXQSKy50YWlsb3IudjEuQ3JlYXRlU2VjcmV0TWFuYWdlclNlY3JldFJlcXVlc3QaLC50YWlsb3IudjEuQ3JlYXRlU2VjcmV0TWFuYWdlclNlY3JldFJlc3BvbnNlIgASeAoZVXBkYXRlU2VjcmV0TWFuYWdlclNlY3JldBIrLnRhaWxvci52MS5VcGRhdGVTZWNyZXRNYW5hZ2VyU2VjcmV0UmVxdWVzdBosLnRhaWxvci52MS5VcGRhdGVTZWNyZXRNYW5hZ2VyU2VjcmV0UmVzcG9uc2UiABJyChZHZXRTZWNyZXRNYW5hZ2VyU2VjcmV0EigudGFpbG9yLnYxLkdldFNlY3JldE1hbmFnZXJTZWNyZXRSZXF1ZXN0GikudGFpbG9yLnYxLkdldFNlY3JldE1hbmFnZXJTZWNyZXRSZXNwb25zZSIDkAIBEngKGURlbGV0ZVNlY3JldE1hbmFnZXJTZWNyZXQSKy50YWlsb3IudjEuRGVsZXRlU2VjcmV0TWFuYWdlclNlY3JldFJlcXVlc3QaLC50YWlsb3IudjEuRGVsZXRlU2VjcmV0TWFuYWdlclNlY3JldFJlc3BvbnNlIgASeAoYTGlzdFNlY3JldE1hbmFnZXJTZWNyZXRzEioudGFpbG9yLnYxLkxpc3RTZWNyZXRNYW5hZ2VyU2VjcmV0c1JlcXVlc3QaKy50YWlsb3IudjEuTGlzdFNlY3JldE1hbmFnZXJTZWNyZXRzUmVzcG9uc2UiA5ACARJgChFDcmVhdGVBdXRoU2VydmljZRIjLnRhaWxvci52MS5DcmVhdGVBdXRoU2VydmljZVJlcXVlc3QaJC50YWlsb3IudjEuQ3JlYXRlQXV0aFNlcnZpY2VSZXNwb25zZSIAEmAKEVVwZGF0ZUF1dGhTZXJ2aWNlEiMudGFpbG9yLnYxLlVwZGF0ZUF1dGhTZXJ2aWNlUmVxdWVzdBokLnRhaWxvci52MS5VcGRhdGVBdXRoU2VydmljZVJlc3BvbnNlIgASYAoRRGVsZXRlQXV0aFNlcnZpY2USIy50YWlsb3IudjEuRGVsZXRlQXV0aFNlcnZpY2VSZXF1ZXN0GiQudGFpbG9yLnYxLkRlbGV0ZUF1dGhTZXJ2aWNlUmVzcG9uc2UiABJaCg5HZXRBdXRoU2VydmljZRIgLnRhaWxvci52MS5HZXRBdXRoU2VydmljZVJlcXVlc3QaIS50YWlsb3IudjEuR2V0QXV0aFNlcnZpY2VSZXNwb25zZSIDkAIBEmAKEExpc3RBdXRoU2VydmljZXMSIi50YWlsb3IudjEuTGlzdEF1dGhTZXJ2aWNlc1JlcXVlc3QaIy50YWlsb3IudjEuTGlzdEF1dGhTZXJ2aWNlc1Jlc3BvbnNlIgOQAgESZgoTQ3JlYXRlQXV0aElEUENvbmZpZxIlLnRhaWxvci52MS5DcmVhdGVBdXRoSURQQ29uZmlnUmVxdWVzdBomLnRhaWxvci52MS5DcmVhdGVBdXRoSURQQ29uZmlnUmVzcG9uc2UiABJmChNVcGRhdGVBdXRoSURQQ29uZmlnEiUudGFpbG9yLnYxLlVwZGF0ZUF1dGhJRFBDb25maWdSZXF1ZXN0GiYudGFpbG9yLnYxLlVwZGF0ZUF1dGhJRFBDb25maWdSZXNwb25zZSIAEmYKE0RlbGV0ZUF1dGhJRFBDb25maWcSJS50YWlsb3IudjEuRGVsZXRlQXV0aElEUENvbmZpZ1JlcXVlc3QaJi50YWlsb3IudjEuRGVsZXRlQXV0aElEUENvbmZpZ1Jlc3BvbnNlIgASYAoQR2V0QXV0aElEUENvbmZpZxIiLnRhaWxvci52MS5HZXRBdXRoSURQQ29uZmlnUmVxdWVzdBojLnRhaWxvci52MS5HZXRBdXRoSURQQ29uZmlnUmVzcG9uc2UiA5ACARJmChJMaXN0QXV0aElEUENvbmZpZ3MSJC50YWlsb3IudjEuTGlzdEF1dGhJRFBDb25maWdzUmVxdWVzdBolLnRhaWxvci52MS5MaXN0QXV0aElEUENvbmZpZ3NSZXNwb25zZSIDkAIBEnIKF0NyZWF0ZVVzZXJQcm9maWxlQ29uZmlnEikudGFpbG9yLnYxLkNyZWF0ZVVzZXJQcm9maWxlQ29uZmlnUmVxdWVzdBoqLnRhaWxvci52MS5DcmVhdGVVc2VyUHJvZmlsZUNvbmZpZ1Jlc3BvbnNlIgAScgoXVXBkYXRlVXNlclByb2ZpbGVDb25maWcSKS50YWlsb3IudjEuVXBkYXRlVXNlclByb2ZpbGVDb25maWdSZXF1ZXN0GioudGFpbG9yLnYxLlVwZGF0ZVVzZXJQcm9maWxlQ29uZmlnUmVzcG9uc2UiABJyChdEZWxldGVVc2VyUHJvZmlsZUNvbmZpZxIpLnRhaWxvci52MS5EZWxldGVVc2VyUHJvZmlsZUNvbmZpZ1JlcXVlc3QaKi50YWlsb3IudjEuRGVsZXRlVXNlclByb2ZpbGVDb25maWdSZXNwb25zZSIAEmwKFEdldFVzZXJQcm9maWxlQ29uZmlnEiYudGFpbG9yLnYxLkdldFVzZXJQcm9maWxlQ29uZmlnUmVxdWVzdBonLnRhaWxvci52MS5HZXRVc2VyUHJvZmlsZUNvbmZpZ1Jlc3BvbnNlIgOQAgESYwoSQ3JlYXRlVGVuYW50Q29uZmlnEiQudGFpbG9yLnYxLkNyZWF0ZVRlbmFudENvbmZpZ1JlcXVlc3QaJS50YWlsb3IudjEuQ3JlYXRlVGVuYW50Q29uZmlnUmVzcG9uc2UiABJjChJVcGRhdGVUZW5hbnRDb25maWcSJC50YWlsb3IudjEuVXBkYXRlVGVuYW50Q29uZmlnUmVxdWVzdBolLnRhaWxvci52MS5VcGRhdGVUZW5hbnRDb25maWdSZXNwb25zZSIAEmMKEkRlbGV0ZVRlbmFudENvbmZpZxIkLnRhaWxvci52MS5EZWxldGVUZW5hbnRDb25maWdSZXF1ZXN0GiUudGFpbG9yLnYxLkRlbGV0ZVRlbmFudENvbmZpZ1Jlc3BvbnNlIgASXQoPR2V0VGVuYW50Q29uZmlnEiEudGFpbG9yLnYxLkdldFRlbmFudENvbmZpZ1JlcXVlc3QaIi50YWlsb3IudjEuR2V0VGVuYW50Q29uZmlnUmVzcG9uc2UiA5ACARJ4ChlDcmVhdGVQZXJzb25hbEFjY2Vzc1Rva2VuEisudGFpbG9yLnYxLkNyZWF0ZVBlcnNvbmFsQWNjZXNzVG9rZW5SZXF1ZXN0GiwudGFpbG9yLnYxLkNyZWF0ZVBlcnNvbmFsQWNjZXNzVG9rZW5SZXNwb25zZSIAEngKGURlbGV0ZVBlcnNvbmFsQWNjZXNzVG9rZW4SKy50YWlsb3IudjEuRGVsZXRlUGVyc29uYWxBY2Nlc3NUb2tlblJlcXVlc3QaLC50YWlsb3IudjEuRGVsZXRlUGVyc29uYWxBY2Nlc3NUb2tlblJlc3BvbnNlIgASeAoYTGlzdFBlcnNvbmFsQWNjZXNzVG9rZW5zEioudGFpbG9yLnYxLkxpc3RQZXJzb25hbEFjY2Vzc1Rva2Vuc1JlcXVlc3QaKy50YWlsb3IudjEuTGlzdFBlcnNvbmFsQWNjZXNzVG9rZW5zUmVzcG9uc2UiA5ACARJsChVDcmVhdGVBdXRoTWFjaGluZVVzZXISJy50YWlsb3IudjEuQ3JlYXRlQXV0aE1hY2hpbmVVc2VyUmVxdWVzdBooLnRhaWxvci52MS5DcmVhdGVBdXRoTWFjaGluZVVzZXJSZXNwb25zZSIAEmwKFVVwZGF0ZUF1dGhNYWNoaW5lVXNlchInLnRhaWxvci52MS5VcGRhdGVBdXRoTWFjaGluZVVzZXJSZXF1ZXN0GigudGFpbG9yLnYxLlVwZGF0ZUF1dGhNYWNoaW5lVXNlclJlc3BvbnNlIgASbAoVRGVsZXRlQXV0aE1hY2hpbmVVc2VyEicudGFpbG9yLnYxLkRlbGV0ZUF1dGhNYWNoaW5lVXNlclJlcXVlc3QaKC50YWlsb3IudjEuRGVsZXRlQXV0aE1hY2hpbmVVc2VyUmVzcG9uc2UiABJmChJHZXRBdXRoTWFjaGluZVVzZXISJC50YWlsb3IudjEuR2V0QXV0aE1hY2hpbmVVc2VyUmVxdWVzdBolLnRhaWxvci52MS5HZXRBdXRoTWFjaGluZVVzZXJSZXNwb25zZSIDkAIBEmwKFExpc3RBdXRoTWFjaGluZVVzZXJzEiYudGFpbG9yLnYxLkxpc3RBdXRoTWFjaGluZVVzZXJzUmVxdWVzdBonLnRhaWxvci52MS5MaXN0QXV0aE1hY2hpbmVVc2Vyc1Jlc3BvbnNlIgOQAgESaQoUQ3JlYXRlQXV0aFNDSU1Db25maWcSJi50YWlsb3IudjEuQ3JlYXRlQXV0aFNDSU1Db25maWdSZXF1ZXN0GicudGFpbG9yLnYxLkNyZWF0ZUF1dGhTQ0lNQ29uZmlnUmVzcG9uc2UiABJpChRVcGRhdGVBdXRoU0NJTUNvbmZpZxImLnRhaWxvci52MS5VcGRhdGVBdXRoU0NJTUNvbmZpZ1JlcXVlc3QaJy50YWlsb3IudjEuVXBkYXRlQXV0aFNDSU1Db25maWdSZXNwb25zZSIAEmkKFERlbGV0ZUF1dGhTQ0lNQ29uZmlnEiYudGFpbG9yLnYxLkRlbGV0ZUF1dGhTQ0lNQ29uZmlnUmVxdWVzdBonLnRhaWxvci52MS5EZWxldGVBdXRoU0NJTUNvbmZpZ1Jlc3BvbnNlIgASYwoRR2V0QXV0aFNDSU1Db25maWcSIy50YWlsb3IudjEuR2V0QXV0aFNDSU1Db25maWdSZXF1ZXN0GiQudGFpbG9yLnYxLkdldEF1dGhTQ0lNQ29uZmlnUmVzcG9uc2UiA5ACARJvChZDcmVhdGVBdXRoU0NJTVJlc291cmNlEigudGFpbG9yLnYxLkNyZWF0ZUF1dGhTQ0lNUmVzb3VyY2VSZXF1ZXN0GikudGFpbG9yLnYxLkNyZWF0ZUF1dGhTQ0lNUmVzb3VyY2VSZXNwb25zZSIAEm8KFlVwZGF0ZUF1dGhTQ0lNUmVzb3VyY2USKC50YWlsb3IudjEuVXBkYXRlQXV0aFNDSU1SZXNvdXJjZVJlcXVlc3QaKS50YWlsb3IudjEuVXBkYXRlQXV0aFNDSU1SZXNvdXJjZVJlc3BvbnNlIgASbwoWRGVsZXRlQXV0aFNDSU1SZXNvdXJjZRIoLnRhaWxvci52MS5EZWxldGVBdXRoU0NJTVJlc291cmNlUmVxdWVzdBopLnRhaWxvci52MS5EZWxldGVBdXRoU0NJTVJlc291cmNlUmVzcG9uc2UiABJpChNHZXRBdXRoU0NJTVJlc291cmNlEiUudGFpbG9yLnYxLkdldEF1dGhTQ0lNUmVzb3VyY2VSZXF1ZXN0GiYudGFpbG9yLnYxLkdldEF1dGhTQ0lNUmVzb3VyY2VSZXNwb25zZSIDkAIBEmwKFEdldEF1dGhTQ0lNUmVzb3VyY2VzEiYudGFpbG9yLnYxLkdldEF1dGhTQ0lNUmVzb3VyY2VzUmVxdWVzdBonLnRhaWxvci52MS5HZXRBdXRoU0NJTVJlc291cmNlc1Jlc3BvbnNlIgOQAgESVwoOQ3JlYXRlQXV0aEhvb2sSIC50YWlsb3IudjEuQ3JlYXRlQXV0aEhvb2tSZXF1ZXN0GiEudGFpbG9yLnYxLkNyZWF0ZUF1dGhIb29rUmVzcG9uc2UiABJXCg5VcGRhdGVBdXRoSG9vaxIgLnRhaWxvci52MS5VcGRhdGVBdXRoSG9va1JlcXVlc3QaIS50YWlsb3IudjEuVXBkYXRlQXV0aEhvb2tSZXNwb25zZSIAElcKDkRlbGV0ZUF1dGhIb29rEiAudGFpbG9yLnYxLkRlbGV0ZUF1dGhIb29rUmVxdWVzdBohLnRhaWxvci52MS5EZWxldGVBdXRoSG9va1Jlc3BvbnNlIgASUQoLR2V0QXV0aEhvb2sSHS50YWlsb3IudjEuR2V0QXV0aEhvb2tSZXF1ZXN0Gh4udGFpbG9yLnYxLkdldEF1dGhIb29rUmVzcG9uc2UiA5ACARJpChRDcmVhdGVBdXRoQ29ubmVjdGlvbhImLnRhaWxvci52MS5DcmVhdGVBdXRoQ29ubmVjdGlvblJlcXVlc3QaJy50YWlsb3IudjEuQ3JlYXRlQXV0aENvbm5lY3Rpb25SZXNwb25zZSIAEmkKE0xpc3RBdXRoQ29ubmVjdGlvbnMSJS50YWlsb3IudjEuTGlzdEF1dGhDb25uZWN0aW9uc1JlcXVlc3QaJi50YWlsb3IudjEuTGlzdEF1dGhDb25uZWN0aW9uc1Jlc3BvbnNlIgOQAgESaQoUUmV2b2tlQXV0aENvbm5lY3Rpb24SJi50YWlsb3IudjEuUmV2b2tlQXV0aENvbm5lY3Rpb25SZXF1ZXN0GicudGFpbG9yLnYxLlJldm9rZUF1dGhDb25uZWN0aW9uUmVzcG9uc2UiABKEAQodUmVnaXN0ZXJBdXRoQ29ubmVjdGlvblNlc3Npb24SLy50YWlsb3IudjEuUmVnaXN0ZXJBdXRoQ29ubmVjdGlvblNlc3Npb25SZXF1ZXN0GjAudGFpbG9yLnYxLlJlZ2lzdGVyQXV0aENvbm5lY3Rpb25TZXNzaW9uUmVzcG9uc2UiABKiAQonRXhjaGFuZ2VBdXRoQ29ubmVjdGlvbkF1dGhvcml6YXRpb25Db2RlEjkudGFpbG9yLnYxLkV4Y2hhbmdlQXV0aENvbm5lY3Rpb25BdXRob3JpemF0aW9uQ29kZVJlcXVlc3QaOi50YWlsb3IudjEuRXhjaGFuZ2VBdXRoQ29ubmVjdGlvbkF1dGhvcml6YXRpb25Db2RlUmVzcG9uc2UiABJvChZDcmVhdGVBdXRoT0F1dGgyQ2xpZW50EigudGFpbG9yLnYxLkNyZWF0ZUF1dGhPQXV0aDJDbGllbnRSZXF1ZXN0GikudGFpbG9yLnYxLkNyZWF0ZUF1dGhPQXV0aDJDbGllbnRSZXNwb25zZSIAEm8KFlVwZGF0ZUF1dGhPQXV0aDJDbGllbnQSKC50YWlsb3IudjEuVXBkYXRlQXV0aE9BdXRoMkNsaWVudFJlcXVlc3QaKS50YWlsb3IudjEuVXBkYXRlQXV0aE9BdXRoMkNsaWVudFJlc3BvbnNlIgASbwoWRGVsZXRlQXV0aE9BdXRoMkNsaWVudBIoLnRhaWxvci52MS5EZWxldGVBdXRoT0F1dGgyQ2xpZW50UmVxdWVzdBopLnRhaWxvci52MS5EZWxldGVBdXRoT0F1dGgyQ2xpZW50UmVzcG9uc2UiABJpChNHZXRBdXRoT0F1dGgyQ2xpZW50EiUudGFpbG9yLnYxLkdldEF1dGhPQXV0aDJDbGllbnRSZXF1ZXN0GiYudGFpbG9yLnYxLkdldEF1dGhPQXV0aDJDbGllbnRSZXNwb25zZSIDkAIBEm8KFUxpc3RBdXRoT0F1dGgyQ2xpZW50cxInLnRhaWxvci52MS5MaXN0QXV0aE9BdXRoMkNsaWVudHNSZXF1ZXN0GigudGFpbG9yLnYxLkxpc3RBdXRoT0F1dGgyQ2xpZW50c1Jlc3BvbnNlIgOQAgESaQoTTGlzdERhdGFwbGFuZUV2ZW50cxIlLnRhaWxvci52MS5MaXN0RGF0YXBsYW5lRXZlbnRzUmVxdWVzdBomLnRhaWxvci52MS5MaXN0RGF0YXBsYW5lRXZlbnRzUmVzcG9uc2UiA5ACARKEAQocTGlzdENvbnRyb2xwbGFuZUFjdGl2aXR5TG9ncxIuLnRhaWxvci52MS5MaXN0Q29udHJvbHBsYW5lQWN0aXZpdHlMb2dzUmVxdWVzdBovLnRhaWxvci52MS5MaXN0Q29udHJvbHBsYW5lQWN0aXZpdHlMb2dzUmVzcG9uc2UiA5ACARJXCg5UZXN0RXhlY1NjcmlwdBIgLnRhaWxvci52MS5UZXN0RXhlY1NjcmlwdFJlcXVlc3QaIS50YWlsb3IudjEuVGVzdEV4ZWNTY3JpcHRSZXNwb25zZSIAEmwKFEdldEZ1bmN0aW9uRXhlY3V0aW9uEiYudGFpbG9yLnYxLkdldEZ1bmN0aW9uRXhlY3V0aW9uUmVxdWVzdBonLnRhaWxvci52MS5HZXRGdW5jdGlvbkV4ZWN1dGlvblJlc3BvbnNlIgOQAgEScgoWTGlzdEZ1bmN0aW9uRXhlY3V0aW9ucxIoLnRhaWxvci52MS5MaXN0RnVuY3Rpb25FeGVjdXRpb25zUmVxdWVzdBopLnRhaWxvci52MS5MaXN0RnVuY3Rpb25FeGVjdXRpb25zUmVzcG9uc2UiA5ACARJxChZDcmVhdGVGdW5jdGlvblJlZ2lzdHJ5EigudGFpbG9yLnYxLkNyZWF0ZUZ1bmN0aW9uUmVnaXN0cnlSZXF1ZXN0GikudGFpbG9yLnYxLkNyZWF0ZUZ1bmN0aW9uUmVnaXN0cnlSZXNwb25zZSIAKAEScQoWVXBkYXRlRnVuY3Rpb25SZWdpc3RyeRIoLnRhaWxvci52MS5VcGRhdGVGdW5jdGlvblJlZ2lzdHJ5UmVxdWVzdBopLnRhaWxvci52MS5VcGRhdGVGdW5jdGlvblJlZ2lzdHJ5UmVzcG9uc2UiACgBEmkKE0dldEZ1bmN0aW9uUmVnaXN0cnkSJS50YWlsb3IudjEuR2V0RnVuY3Rpb25SZWdpc3RyeVJlcXVlc3QaJi50YWlsb3IudjEuR2V0RnVuY3Rpb25SZWdpc3RyeVJlc3BvbnNlIgOQAgEScgoWTGlzdEZ1bmN0aW9uUmVnaXN0cmllcxIoLnRhaWxvci52MS5MaXN0RnVuY3Rpb25SZWdpc3RyaWVzUmVxdWVzdBopLnRhaWxvci52MS5MaXN0RnVuY3Rpb25SZWdpc3RyaWVzUmVzcG9uc2UiA5ACARJvChZEZWxldGVGdW5jdGlvblJlZ2lzdHJ5EigudGFpbG9yLnYxLkRlbGV0ZUZ1bmN0aW9uUmVnaXN0cnlSZXF1ZXN0GikudGFpbG9yLnYxLkRlbGV0ZUZ1bmN0aW9uUmVnaXN0cnlSZXNwb25zZSIAEowBCh5Eb3dubG9hZEZ1bmN0aW9uUmVnaXN0cnlTY3JpcHQSMC50YWlsb3IudjEuRG93bmxvYWRGdW5jdGlvblJlZ2lzdHJ5U2NyaXB0UmVxdWVzdBoxLnRhaWxvci52MS5Eb3dubG9hZEZ1bmN0aW9uUmVnaXN0cnlTY3JpcHRSZXNwb25zZSIDkAIBMAEScgoWTGlzdE1ldGVyUmVxdWVzdENvdW50cxIoLnRhaWxvci52MS5MaXN0TWV0ZXJSZXF1ZXN0Q291bnRzUmVxdWVzdBopLnRhaWxvci52MS5MaXN0TWV0ZXJSZXF1ZXN0Q291bnRzUmVzcG9uc2UiA5ACARJ4ChhMaXN0TWV0ZXJFeGVjdXRpb25Db3VudHMSKi50YWlsb3IudjEuTGlzdE1ldGVyRXhlY3V0aW9uQ291bnRzUmVxdWVzdBorLnRhaWxvci52MS5MaXN0TWV0ZXJFeGVjdXRpb25Db3VudHNSZXNwb25zZSIDkAIBEmwKFExpc3RNZXRlckV2ZW50Q291bnRzEiYudGFpbG9yLnYxLkxpc3RNZXRlckV2ZW50Q291bnRzUmVxdWVzdBonLnRhaWxvci52MS5MaXN0TWV0ZXJFdmVudENvdW50c1Jlc3BvbnNlIgOQAgESXQoPTGlzdElkUFNlcnZpY2VzEiEudGFpbG9yLnYxLkxpc3RJZFBTZXJ2aWNlc1JlcXVlc3QaIi50YWlsb3IudjEuTGlzdElkUFNlcnZpY2VzUmVzcG9uc2UiA5ACARJdChBDcmVhdGVJZFBTZXJ2aWNlEiIudGFpbG9yLnYxLkNyZWF0ZUlkUFNlcnZpY2VSZXF1ZXN0GiMudGFpbG9yLnYxLkNyZWF0ZUlkUFNlcnZpY2VSZXNwb25zZSIAEl0KEFVwZGF0ZUlkUFNlcnZpY2USIi50YWlsb3IudjEuVXBkYXRlSWRQU2VydmljZVJlcXVlc3QaIy50YWlsb3IudjEuVXBkYXRlSWRQU2VydmljZVJlc3BvbnNlIgASXQoQRGVsZXRlSWRQU2VydmljZRIiLnRhaWxvci52MS5EZWxldGVJZFBTZXJ2aWNlUmVxdWVzdBojLnRhaWxvci52MS5EZWxldGVJZFBTZXJ2aWNlUmVzcG9uc2UiABJXCg1HZXRJZFBTZXJ2aWNlEh8udGFpbG9yLnYxLkdldElkUFNlcnZpY2VSZXF1ZXN0GiAudGFpbG9yLnYxLkdldElkUFNlcnZpY2VSZXNwb25zZSIDkAIBEloKDkxpc3RJZFBDbGllbnRzEiAudGFpbG9yLnYxLkxpc3RJZFBDbGllbnRzUmVxdWVzdBohLnRhaWxvci52MS5MaXN0SWRQQ2xpZW50c1Jlc3BvbnNlIgOQAgESWgoPQ3JlYXRlSWRQQ2xpZW50EiEudGFpbG9yLnYxLkNyZWF0ZUlkUENsaWVudFJlcXVlc3QaIi50YWlsb3IudjEuQ3JlYXRlSWRQQ2xpZW50UmVzcG9uc2UiABJaCg9EZWxldGVJZFBDbGllbnQSIS50YWlsb3IudjEuRGVsZXRlSWRQQ2xpZW50UmVxdWVzdBoiLnRhaWxvci52MS5EZWxldGVJZFBDbGllbnRSZXNwb25zZSIAElQKDEdldElkUENsaWVudBIeLnRhaWxvci52MS5HZXRJZFBDbGllbnRSZXF1ZXN0Gh8udGFpbG9yLnYxLkdldElkUENsaWVudFJlc3BvbnNlIgOQAgESZgoTQ3JlYXRlU3RhdGljV2Vic2l0ZRIlLnRhaWxvci52MS5DcmVhdGVTdGF0aWNXZWJzaXRlUmVxdWVzdBomLnRhaWxvci52MS5DcmVhdGVTdGF0aWNXZWJzaXRlUmVzcG9uc2UiABJmChNVcGRhdGVTdGF0aWNXZWJzaXRlEiUudGFpbG9yLnYxLlVwZGF0ZVN0YXRpY1dlYnNpdGVSZXF1ZXN0GiYudGFpbG9yLnYxLlVwZGF0ZVN0YXRpY1dlYnNpdGVSZXNwb25zZSIAEmYKE0RlbGV0ZVN0YXRpY1dlYnNpdGUSJS50YWlsb3IudjEuRGVsZXRlU3RhdGljV2Vic2l0ZVJlcXVlc3QaJi50YWlsb3IudjEuRGVsZXRlU3RhdGljV2Vic2l0ZVJlc3BvbnNlIgASYAoQR2V0U3RhdGljV2Vic2l0ZRIiLnRhaWxvci52MS5HZXRTdGF0aWNXZWJzaXRlUmVxdWVzdBojLnRhaWxvci52MS5HZXRTdGF0aWNXZWJzaXRlUmVzcG9uc2UiA5ACARJmChJMaXN0U3RhdGljV2Vic2l0ZXMSJC50YWlsb3IudjEuTGlzdFN0YXRpY1dlYnNpdGVzUmVxdWVzdBolLnRhaWxvci52MS5MaXN0U3RhdGljV2Vic2l0ZXNSZXNwb25zZSIDkAIBEloKD0FkZEN1c3RvbURvbWFpbhIhLnRhaWxvci52MS5BZGRDdXN0b21Eb21haW5SZXF1ZXN0GiIudGFpbG9yLnYxLkFkZEN1c3RvbURvbWFpblJlc3BvbnNlIgASXQoPR2V0Q3VzdG9tRG9tYWluEiEudGFpbG9yLnYxLkdldEN1c3RvbURvbWFpblJlcXVlc3QaIi50YWlsb3IudjEuR2V0Q3VzdG9tRG9tYWluUmVzcG9uc2UiA5ACARJjChFMaXN0Q3VzdG9tRG9tYWlucxIjLnRhaWxvci52MS5MaXN0Q3VzdG9tRG9tYWluc1JlcXVlc3QaJC50YWlsb3IudjEuTGlzdEN1c3RvbURvbWFpbnNSZXNwb25zZSIDkAIBEmMKElJlbW92ZUN1c3RvbURvbWFpbhIkLnRhaWxvci52MS5SZW1vdmVDdXN0b21Eb21haW5SZXF1ZXN0GiUudGFpbG9yLnYxLlJlbW92ZUN1c3RvbURvbWFpblJlc3BvbnNlIgASXQoQQ3JlYXRlRGVwbG95bWVudBIiLnRhaWxvci52MS5DcmVhdGVEZXBsb3ltZW50UmVxdWVzdBojLnRhaWxvci52MS5DcmVhdGVEZXBsb3ltZW50UmVzcG9uc2UiABJNCgpVcGxvYWRGaWxlEhwudGFpbG9yLnYxLlVwbG9hZEZpbGVSZXF1ZXN0Gh0udGFpbG9yLnYxLlVwbG9hZEZpbGVSZXNwb25zZSIAKAESYAoRUHVibGlzaERlcGxveW1lbnQSIy50YWlsb3IudjEuUHVibGlzaERlcGxveW1lbnRSZXF1ZXN0GiQudGFpbG9yLnYxLlB1Ymxpc2hEZXBsb3ltZW50UmVzcG9uc2UiABJXCg5DcmVhdGVXb3JrZmxvdxIgLnRhaWxvci52MS5DcmVhdGVXb3JrZmxvd1JlcXVlc3QaIS50YWlsb3IudjEuQ3JlYXRlV29ya2Zsb3dSZXNwb25zZSIAElcKDlVwZGF0ZVdvcmtmbG93EiAudGFpbG9yLnYxLlVwZGF0ZVdvcmtmbG93UmVxdWVzdBohLnRhaWxvci52MS5VcGRhdGVXb3JrZmxvd1Jlc3BvbnNlIgASVwoORGVsZXRlV29ya2Zsb3cSIC50YWlsb3IudjEuRGVsZXRlV29ya2Zsb3dSZXF1ZXN0GiEudGFpbG9yLnYxLkRlbGV0ZVdvcmtmbG93UmVzcG9uc2UiABJRCgtHZXRXb3JrZmxvdxIdLnRhaWxvci52MS5HZXRXb3JrZmxvd1JlcXVlc3QaHi50YWlsb3IudjEuR2V0V29ya2Zsb3dSZXNwb25zZSIDkAIBEmMKEUdldFdvcmtmbG93QnlOYW1lEiMudGFpbG9yLnYxLkdldFdvcmtmbG93QnlOYW1lUmVxdWVzdBokLnRhaWxvci52MS5HZXRXb3JrZmxvd0J5TmFtZVJlc3BvbnNlIgOQAgESVwoNTGlzdFdvcmtmbG93cxIfLnRhaWxvci52MS5MaXN0V29ya2Zsb3dzUmVxdWVzdBogLnRhaWxvci52MS5MaXN0V29ya2Zsb3dzUmVzcG9uc2UiA5ACARJ4ChlDcmVhdGVXb3JrZmxvd0pvYkZ1bmN0aW9uEisudGFpbG9yLnYxLkNyZWF0ZVdvcmtmbG93Sm9iRnVuY3Rpb25SZXF1ZXN0GiwudGFpbG9yLnYxLkNyZWF0ZVdvcmtmbG93Sm9iRnVuY3Rpb25SZXNwb25zZSIAEngKGVVwZGF0ZVdvcmtmbG93Sm9iRnVuY3Rpb24SKy50YWlsb3IudjEuVXBkYXRlV29ya2Zsb3dKb2JGdW5jdGlvblJlcXVlc3QaLC50YWlsb3IudjEuVXBkYXRlV29ya2Zsb3dKb2JGdW5jdGlvblJlc3BvbnNlIgAScgoWR2V0V29ya2Zsb3dKb2JGdW5jdGlvbhIoLnRhaWxvci52MS5HZXRXb3JrZmxvd0pvYkZ1bmN0aW9uUmVxdWVzdBopLnRhaWxvci52MS5HZXRXb3JrZmxvd0pvYkZ1bmN0aW9uUmVzcG9uc2UiA5ACARKEAQocR2V0V29ya2Zsb3dKb2JGdW5jdGlvbkJ5TmFtZRIuLnRhaWxvci52MS5HZXRXb3JrZmxvd0pvYkZ1bmN0aW9uQnlOYW1lUmVxdWVzdBovLnRhaWxvci52MS5HZXRXb3JrZmxvd0pvYkZ1bmN0aW9uQnlOYW1lUmVzcG9uc2UiA5ACARJ4ChhMaXN0V29ya2Zsb3dKb2JGdW5jdGlvbnMSKi50YWlsb3IudjEuTGlzdFdvcmtmbG93Sm9iRnVuY3Rpb25zUmVxdWVzdBorLnRhaWxvci52MS5MaXN0V29ya2Zsb3dKb2JGdW5jdGlvbnNSZXNwb25zZSIDkAIBEmwKFEdldFdvcmtmbG93RXhlY3V0aW9uEiYudGFpbG9yLnYxLkdldFdvcmtmbG93RXhlY3V0aW9uUmVxdWVzdBonLnRhaWxvci52MS5HZXRXb3JrZmxvd0V4ZWN1dGlvblJlc3BvbnNlIgOQAgEScgoWTGlzdFdvcmtmbG93RXhlY3V0aW9ucxIoLnRhaWxvci52MS5MaXN0V29ya2Zsb3dFeGVjdXRpb25zUmVxdWVzdBopLnRhaWxvci52MS5MaXN0V29ya2Zsb3dFeGVjdXRpb25zUmVzcG9uc2UiA5ACARJgChFUZXN0U3RhcnRXb3JrZmxvdxIjLnRhaWxvci52MS5UZXN0U3RhcnRXb3JrZmxvd1JlcXVlc3QaJC50YWlsb3IudjEuVGVzdFN0YXJ0V29ya2Zsb3dSZXNwb25zZSIAEmMKElRlc3RSZXN1bWVXb3JrZmxvdxIkLnRhaWxvci52MS5UZXN0UmVzdW1lV29ya2Zsb3dSZXF1ZXN0GiUudGFpbG9yLnYxLlRlc3RSZXN1bWVXb3JrZmxvd1Jlc3BvbnNlIgASTgoLU2V0TWV0YWRhdGESHS50YWlsb3IudjEuU2V0TWV0YWRhdGFSZXF1ZXN0Gh4udGFpbG9yLnYxLlNldE1ldGFkYXRhUmVzcG9uc2UiABJRCgtHZXRNZXRhZGF0YRIdLnRhaWxvci52MS5HZXRNZXRhZGF0YVJlcXVlc3QaHi50YWlsb3IudjEuR2V0TWV0YWRhdGFSZXNwb25zZSIDkAIBEoQBCh1DcmVhdGVDb250cm9scGxhbmVNYWNoaW5lVXNlchIvLnRhaWxvci52MS5DcmVhdGVDb250cm9scGxhbmVNYWNoaW5lVXNlclJlcXVlc3QaMC50YWlsb3IudjEuQ3JlYXRlQ29udHJvbHBsYW5lTWFjaGluZVVzZXJSZXNwb25zZSIAEoQBCh1VcGRhdGVDb250cm9scGxhbmVNYWNoaW5lVXNlchIvLnRhaWxvci52MS5VcGRhdGVDb250cm9scGxhbmVNYWNoaW5lVXNlclJlcXVlc3QaMC50YWlsb3IudjEuVXBkYXRlQ29udHJvbHBsYW5lTWFjaGluZVVzZXJSZXNwb25zZSIAEn4KGkdldENvbnRyb2xwbGFuZU1hY2hpbmVVc2VyEiwudGFpbG9yLnYxLkdldENvbnRyb2xwbGFuZU1hY2hpbmVVc2VyUmVxdWVzdBotLnRhaWxvci52MS5HZXRDb250cm9scGxhbmVNYWNoaW5lVXNlclJlc3BvbnNlIgOQAgESkAEKIEdldENvbnRyb2xwbGFuZU1hY2hpbmVVc2VyQnlOYW1lEjIudGFpbG9yLnYxLkdldENvbnRyb2xwbGFuZU1hY2hpbmVVc2VyQnlOYW1lUmVxdWVzdBozLnRhaWxvci52MS5HZXRDb250cm9scGxhbmVNYWNoaW5lVXNlckJ5TmFtZVJlc3BvbnNlIgOQAgEShAEKHExpc3RDb250cm9scGxhbmVNYWNoaW5lVXNlcnMSLi50YWlsb3IudjEuTGlzdENvbnRyb2xwbGFuZU1hY2hpbmVVc2Vyc1JlcXVlc3QaLy50YWlsb3IudjEuTGlzdENvbnRyb2xwbGFuZU1hY2hpbmVVc2Vyc1Jlc3BvbnNlIgOQAgEShAEKHURlbGV0ZUNvbnRyb2xwbGFuZU1hY2hpbmVVc2VyEi8udGFpbG9yLnYxLkRlbGV0ZUNvbnRyb2xwbGFuZU1hY2hpbmVVc2VyUmVxdWVzdBowLnRhaWxvci52MS5EZWxldGVDb250cm9scGxhbmVNYWNoaW5lVXNlclJlc3BvbnNlIgASbAoVQ3JlYXRlVGVsZW1ldHJ5RXhwb3J0EicudGFpbG9yLnYxLkNyZWF0ZVRlbGVtZXRyeUV4cG9ydFJlcXVlc3QaKC50YWlsb3IudjEuQ3JlYXRlVGVsZW1ldHJ5RXhwb3J0UmVzcG9uc2UiABJsChVVcGRhdGVUZWxlbWV0cnlFeHBvcnQSJy50YWlsb3IudjEuVXBkYXRlVGVsZW1ldHJ5RXhwb3J0UmVxdWVzdBooLnRhaWxvci52MS5VcGRhdGVUZWxlbWV0cnlFeHBvcnRSZXNwb25zZSIAEmYKEkdldFRlbGVtZXRyeUV4cG9ydBIkLnRhaWxvci52MS5HZXRUZWxlbWV0cnlFeHBvcnRSZXF1ZXN0GiUudGFpbG9yLnYxLkdldFRlbGVtZXRyeUV4cG9ydFJlc3BvbnNlIgOQAgESbAoUTGlzdFRlbGVtZXRyeUV4cG9ydHMSJi50YWlsb3IudjEuTGlzdFRlbGVtZXRyeUV4cG9ydHNSZXF1ZXN0GicudGFpbG9yLnYxLkxpc3RUZWxlbWV0cnlFeHBvcnRzUmVzcG9uc2UiA5ACARJsChVEZWxldGVUZWxlbWV0cnlFeHBvcnQSJy50YWlsb3IudjEuRGVsZXRlVGVsZW1ldHJ5RXhwb3J0UmVxdWVzdBooLnRhaWxvci52MS5EZWxldGVUZWxlbWV0cnlFeHBvcnRSZXNwb25zZSIAEmYKE1Rlc3RUZWxlbWV0cnlFeHBvcnQSJS50YWlsb3IudjEuVGVzdFRlbGVtZXRyeUV4cG9ydFJlcXVlc3QaJi50YWlsb3IudjEuVGVzdFRlbGVtZXRyeUV4cG9ydFJlc3BvbnNlIgAShwEKHkNyZWF0ZVJlc291cmNlQXR0cmlidXRlc0NvbmZpZxIwLnRhaWxvci52MS5DcmVhdGVSZXNvdXJjZUF0dHJpYnV0ZXNDb25maWdSZXF1ZXN0GjEudGFpbG9yLnYxLkNyZWF0ZVJlc291cmNlQXR0cmlidXRlc0NvbmZpZ1Jlc3BvbnNlIgASgQEKG0dldFJlc291cmNlQXR0cmlidXRlc0NvbmZpZxItLnRhaWxvci52MS5HZXRSZXNvdXJjZUF0dHJpYnV0ZXNDb25maWdSZXF1ZXN0Gi4udGFpbG9yLnYxLkdldFJlc291cmNlQXR0cmlidXRlc0NvbmZpZ1Jlc3BvbnNlIgOQAgEShwEKHlVwZGF0ZVJlc291cmNlQXR0cmlidXRlc0NvbmZpZxIwLnRhaWxvci52MS5VcGRhdGVSZXNvdXJjZUF0dHJpYnV0ZXNDb25maWdSZXF1ZXN0GjEudGFpbG9yLnYxLlVwZGF0ZVJlc291cmNlQXR0cmlidXRlc0NvbmZpZ1Jlc3BvbnNlIgAShwEKHkRlbGV0ZVJlc291cmNlQXR0cmlidXRlc0NvbmZpZxIwLnRhaWxvci52MS5EZWxldGVSZXNvdXJjZUF0dHJpYnV0ZXNDb25maWdSZXF1ZXN0GjEudGFpbG9yLnYxLkRlbGV0ZVJlc291cmNlQXR0cmlidXRlc0NvbmZpZ1Jlc3BvbnNlIgBiBnByb3RvMw", [file_tailor_v1_application, file_tailor_v1_auth, file_tailor_v1_events, file_tailor_v1_executor, file_tailor_v1_function, file_tailor_v1_function_registry, file_tailor_v1_idp, file_tailor_v1_metadata, file_tailor_v1_meter, file_tailor_v1_pipeline, file_tailor_v1_secret_manager, file_tailor_v1_stateflow, file_tailor_v1_staticwebsite, file_tailor_v1_tailordb, file_tailor_v1_telemetryrouter, file_tailor_v1_workflow, file_tailor_v1_workspace]); /** * Describes the message tailor.v1.PingRequest. diff --git a/packages/tailor-proto/src/tailor/v1/staticwebsite_pb.d.ts b/packages/tailor-proto/src/tailor/v1/staticwebsite_pb.d.ts index 5103a4b00..4bdaf6e15 100644 --- a/packages/tailor-proto/src/tailor/v1/staticwebsite_pb.d.ts +++ b/packages/tailor-proto/src/tailor/v1/staticwebsite_pb.d.ts @@ -4,7 +4,7 @@ import type { GenFile, GenMessage } from "@bufbuild/protobuf/codegenv2"; import type { Message } from "@bufbuild/protobuf"; -import type { StaticWebsite } from "./staticwebsite_resource_pb"; +import type { CustomDomain, StaticWebsite } from "./staticwebsite_resource_pb"; import type { PageDirection } from "./resource_pb"; /** @@ -358,3 +358,152 @@ export declare type PublishDeploymentResponse = Message<"tailor.v1.PublishDeploy */ export declare const PublishDeploymentResponseSchema: GenMessage; +/** + * @generated from message tailor.v1.AddCustomDomainRequest + */ +export declare type AddCustomDomainRequest = Message<"tailor.v1.AddCustomDomainRequest"> & { + /** + * @generated from field: string workspace_id = 1; + */ + workspaceId: string; + + /** + * @generated from field: string static_website_name = 2; + */ + staticWebsiteName: string; + + /** + * @generated from field: string domain = 3; + */ + domain: string; +}; + +/** + * Describes the message tailor.v1.AddCustomDomainRequest. + * Use `create(AddCustomDomainRequestSchema)` to create a new message. + */ +export declare const AddCustomDomainRequestSchema: GenMessage; + +/** + * @generated from message tailor.v1.AddCustomDomainResponse + */ +export declare type AddCustomDomainResponse = Message<"tailor.v1.AddCustomDomainResponse"> & { + /** + * @generated from field: tailor.v1.CustomDomain custom_domain = 1; + */ + customDomain?: CustomDomain; +}; + +/** + * Describes the message tailor.v1.AddCustomDomainResponse. + * Use `create(AddCustomDomainResponseSchema)` to create a new message. + */ +export declare const AddCustomDomainResponseSchema: GenMessage; + +/** + * @generated from message tailor.v1.GetCustomDomainRequest + */ +export declare type GetCustomDomainRequest = Message<"tailor.v1.GetCustomDomainRequest"> & { + /** + * @generated from field: string workspace_id = 1; + */ + workspaceId: string; + + /** + * @generated from field: string domain = 2; + */ + domain: string; +}; + +/** + * Describes the message tailor.v1.GetCustomDomainRequest. + * Use `create(GetCustomDomainRequestSchema)` to create a new message. + */ +export declare const GetCustomDomainRequestSchema: GenMessage; + +/** + * @generated from message tailor.v1.GetCustomDomainResponse + */ +export declare type GetCustomDomainResponse = Message<"tailor.v1.GetCustomDomainResponse"> & { + /** + * @generated from field: tailor.v1.CustomDomain custom_domain = 1; + */ + customDomain?: CustomDomain; +}; + +/** + * Describes the message tailor.v1.GetCustomDomainResponse. + * Use `create(GetCustomDomainResponseSchema)` to create a new message. + */ +export declare const GetCustomDomainResponseSchema: GenMessage; + +/** + * @generated from message tailor.v1.ListCustomDomainsRequest + */ +export declare type ListCustomDomainsRequest = Message<"tailor.v1.ListCustomDomainsRequest"> & { + /** + * @generated from field: string workspace_id = 1; + */ + workspaceId: string; + + /** + * @generated from field: string static_website_name = 2; + */ + staticWebsiteName: string; +}; + +/** + * Describes the message tailor.v1.ListCustomDomainsRequest. + * Use `create(ListCustomDomainsRequestSchema)` to create a new message. + */ +export declare const ListCustomDomainsRequestSchema: GenMessage; + +/** + * @generated from message tailor.v1.ListCustomDomainsResponse + */ +export declare type ListCustomDomainsResponse = Message<"tailor.v1.ListCustomDomainsResponse"> & { + /** + * @generated from field: repeated tailor.v1.CustomDomain custom_domains = 1; + */ + customDomains: CustomDomain[]; +}; + +/** + * Describes the message tailor.v1.ListCustomDomainsResponse. + * Use `create(ListCustomDomainsResponseSchema)` to create a new message. + */ +export declare const ListCustomDomainsResponseSchema: GenMessage; + +/** + * @generated from message tailor.v1.RemoveCustomDomainRequest + */ +export declare type RemoveCustomDomainRequest = Message<"tailor.v1.RemoveCustomDomainRequest"> & { + /** + * @generated from field: string workspace_id = 1; + */ + workspaceId: string; + + /** + * @generated from field: string domain = 2; + */ + domain: string; +}; + +/** + * Describes the message tailor.v1.RemoveCustomDomainRequest. + * Use `create(RemoveCustomDomainRequestSchema)` to create a new message. + */ +export declare const RemoveCustomDomainRequestSchema: GenMessage; + +/** + * @generated from message tailor.v1.RemoveCustomDomainResponse + */ +export declare type RemoveCustomDomainResponse = Message<"tailor.v1.RemoveCustomDomainResponse"> & { +}; + +/** + * Describes the message tailor.v1.RemoveCustomDomainResponse. + * Use `create(RemoveCustomDomainResponseSchema)` to create a new message. + */ +export declare const RemoveCustomDomainResponseSchema: GenMessage; + diff --git a/packages/tailor-proto/src/tailor/v1/staticwebsite_pb.js b/packages/tailor-proto/src/tailor/v1/staticwebsite_pb.js index 3c8316e82..a5ca6af2f 100644 --- a/packages/tailor-proto/src/tailor/v1/staticwebsite_pb.js +++ b/packages/tailor-proto/src/tailor/v1/staticwebsite_pb.js @@ -11,7 +11,7 @@ import { file_tailor_v1_staticwebsite_resource } from "./staticwebsite_resource_ * Describes the file tailor/v1/staticwebsite.proto. */ export const file_tailor_v1_staticwebsite = /*@__PURE__*/ - fileDesc("Ch10YWlsb3IvdjEvc3RhdGljd2Vic2l0ZS5wcm90bxIJdGFpbG9yLnYxInUKGkNyZWF0ZVN0YXRpY1dlYnNpdGVSZXF1ZXN0Eh4KDHdvcmtzcGFjZV9pZBgBIAEoCUIIukgFcgOwAQESNwoNc3RhdGljd2Vic2l0ZRgCIAEoCzIYLnRhaWxvci52MS5TdGF0aWNXZWJzaXRlQga6SAPIAQEiTgobQ3JlYXRlU3RhdGljV2Vic2l0ZVJlc3BvbnNlEi8KDXN0YXRpY3dlYnNpdGUYASABKAsyGC50YWlsb3IudjEuU3RhdGljV2Vic2l0ZSJ1ChpVcGRhdGVTdGF0aWNXZWJzaXRlUmVxdWVzdBIeCgx3b3Jrc3BhY2VfaWQYASABKAlCCLpIBXIDsAEBEjcKDXN0YXRpY3dlYnNpdGUYAiABKAsyGC50YWlsb3IudjEuU3RhdGljV2Vic2l0ZUIGukgDyAEBIk4KG1VwZGF0ZVN0YXRpY1dlYnNpdGVSZXNwb25zZRIvCg1zdGF0aWN3ZWJzaXRlGAEgASgLMhgudGFpbG9yLnYxLlN0YXRpY1dlYnNpdGUicQoXR2V0U3RhdGljV2Vic2l0ZVJlcXVlc3QSHgoMd29ya3NwYWNlX2lkGAEgASgJQgi6SAVyA7ABARI2CgRuYW1lGAIgASgJQii6SCVyIzIhXlthLXowLTldW2EtejAtOS1dezEsNjF9W2EtejAtOV0kIksKGEdldFN0YXRpY1dlYnNpdGVSZXNwb25zZRIvCg1zdGF0aWN3ZWJzaXRlGAEgASgLMhgudGFpbG9yLnYxLlN0YXRpY1dlYnNpdGUidAoaRGVsZXRlU3RhdGljV2Vic2l0ZVJlcXVlc3QSHgoMd29ya3NwYWNlX2lkGAEgASgJQgi6SAVyA7ABARI2CgRuYW1lGAIgASgJQii6SCVyIzIhXlthLXowLTldW2EtejAtOS1dezEsNjF9W2EtejAtOV0kIh0KG0RlbGV0ZVN0YXRpY1dlYnNpdGVSZXNwb25zZSKUAQoZTGlzdFN0YXRpY1dlYnNpdGVzUmVxdWVzdBIeCgx3b3Jrc3BhY2VfaWQYASABKAlCCLpIBXIDsAEBEhIKCnBhZ2VfdG9rZW4YAiABKAkSEQoJcGFnZV9zaXplGAMgASgNEjAKDnBhZ2VfZGlyZWN0aW9uGAQgASgOMhgudGFpbG9yLnYxLlBhZ2VEaXJlY3Rpb24ifAoaTGlzdFN0YXRpY1dlYnNpdGVzUmVzcG9uc2USMAoOc3RhdGljd2Vic2l0ZXMYASADKAsyGC50YWlsb3IudjEuU3RhdGljV2Vic2l0ZRIXCg9uZXh0X3BhZ2VfdG9rZW4YAiABKAkSEwoLdG90YWxfY291bnQYAyABKAMicQoXQ3JlYXRlRGVwbG95bWVudFJlcXVlc3QSHgoMd29ya3NwYWNlX2lkGAEgASgJQgi6SAVyA7ABARI2CgRuYW1lGAIgASgJQii6SCVyIzIhXlthLXowLTldW2EtejAtOS1dezEsNjF9W2EtejAtOV0kIjsKGENyZWF0ZURlcGxveW1lbnRSZXNwb25zZRIfCg1kZXBsb3ltZW50X2lkGAEgASgJQgi6SAVyA7ABASK9AgoRVXBsb2FkRmlsZVJlcXVlc3QSTgoQaW5pdGlhbF9tZXRhZGF0YRgBIAEoCzIyLnRhaWxvci52MS5VcGxvYWRGaWxlUmVxdWVzdC5Jbml0aWFsVXBsb2FkTWV0YWRhdGFIABIUCgpjaHVua19kYXRhGAIgASgMSAAatgEKFUluaXRpYWxVcGxvYWRNZXRhZGF0YRIeCgx3b3Jrc3BhY2VfaWQYASABKAlCCLpIBXIDsAEBEh8KDWRlcGxveW1lbnRfaWQYAiABKAlCCLpIBXIDsAEBEioKCWZpbGVfcGF0aBgDIAEoCUIXukgUchIyEF5bXi9dKygvW14vXSspKiQSMAoMY29udGVudF90eXBlGAQgASgJQhq6SBdyFTITXlteL10rL1teL10rKDsuKik/JEIJCgdwYXlsb2FkIhQKElVwbG9hZEZpbGVSZXNwb25zZSJbChhQdWJsaXNoRGVwbG95bWVudFJlcXVlc3QSHgoMd29ya3NwYWNlX2lkGAEgASgJQgi6SAVyA7ABARIfCg1kZXBsb3ltZW50X2lkGAIgASgJQgi6SAVyA7ABASIoChlQdWJsaXNoRGVwbG95bWVudFJlc3BvbnNlEgsKA3VybBgBIAEoCWIGcHJvdG8z", [file_buf_validate_validate, file_tailor_v1_resource, file_tailor_v1_staticwebsite_resource]); + fileDesc("Ch10YWlsb3IvdjEvc3RhdGljd2Vic2l0ZS5wcm90bxIJdGFpbG9yLnYxInUKGkNyZWF0ZVN0YXRpY1dlYnNpdGVSZXF1ZXN0Eh4KDHdvcmtzcGFjZV9pZBgBIAEoCUIIukgFcgOwAQESNwoNc3RhdGljd2Vic2l0ZRgCIAEoCzIYLnRhaWxvci52MS5TdGF0aWNXZWJzaXRlQga6SAPIAQEiTgobQ3JlYXRlU3RhdGljV2Vic2l0ZVJlc3BvbnNlEi8KDXN0YXRpY3dlYnNpdGUYASABKAsyGC50YWlsb3IudjEuU3RhdGljV2Vic2l0ZSJ1ChpVcGRhdGVTdGF0aWNXZWJzaXRlUmVxdWVzdBIeCgx3b3Jrc3BhY2VfaWQYASABKAlCCLpIBXIDsAEBEjcKDXN0YXRpY3dlYnNpdGUYAiABKAsyGC50YWlsb3IudjEuU3RhdGljV2Vic2l0ZUIGukgDyAEBIk4KG1VwZGF0ZVN0YXRpY1dlYnNpdGVSZXNwb25zZRIvCg1zdGF0aWN3ZWJzaXRlGAEgASgLMhgudGFpbG9yLnYxLlN0YXRpY1dlYnNpdGUicQoXR2V0U3RhdGljV2Vic2l0ZVJlcXVlc3QSHgoMd29ya3NwYWNlX2lkGAEgASgJQgi6SAVyA7ABARI2CgRuYW1lGAIgASgJQii6SCVyIzIhXlthLXowLTldW2EtejAtOS1dezEsNjF9W2EtejAtOV0kIksKGEdldFN0YXRpY1dlYnNpdGVSZXNwb25zZRIvCg1zdGF0aWN3ZWJzaXRlGAEgASgLMhgudGFpbG9yLnYxLlN0YXRpY1dlYnNpdGUidAoaRGVsZXRlU3RhdGljV2Vic2l0ZVJlcXVlc3QSHgoMd29ya3NwYWNlX2lkGAEgASgJQgi6SAVyA7ABARI2CgRuYW1lGAIgASgJQii6SCVyIzIhXlthLXowLTldW2EtejAtOS1dezEsNjF9W2EtejAtOV0kIh0KG0RlbGV0ZVN0YXRpY1dlYnNpdGVSZXNwb25zZSKUAQoZTGlzdFN0YXRpY1dlYnNpdGVzUmVxdWVzdBIeCgx3b3Jrc3BhY2VfaWQYASABKAlCCLpIBXIDsAEBEhIKCnBhZ2VfdG9rZW4YAiABKAkSEQoJcGFnZV9zaXplGAMgASgNEjAKDnBhZ2VfZGlyZWN0aW9uGAQgASgOMhgudGFpbG9yLnYxLlBhZ2VEaXJlY3Rpb24ifAoaTGlzdFN0YXRpY1dlYnNpdGVzUmVzcG9uc2USMAoOc3RhdGljd2Vic2l0ZXMYASADKAsyGC50YWlsb3IudjEuU3RhdGljV2Vic2l0ZRIXCg9uZXh0X3BhZ2VfdG9rZW4YAiABKAkSEwoLdG90YWxfY291bnQYAyABKAMicQoXQ3JlYXRlRGVwbG95bWVudFJlcXVlc3QSHgoMd29ya3NwYWNlX2lkGAEgASgJQgi6SAVyA7ABARI2CgRuYW1lGAIgASgJQii6SCVyIzIhXlthLXowLTldW2EtejAtOS1dezEsNjF9W2EtejAtOV0kIjsKGENyZWF0ZURlcGxveW1lbnRSZXNwb25zZRIfCg1kZXBsb3ltZW50X2lkGAEgASgJQgi6SAVyA7ABASK9AgoRVXBsb2FkRmlsZVJlcXVlc3QSTgoQaW5pdGlhbF9tZXRhZGF0YRgBIAEoCzIyLnRhaWxvci52MS5VcGxvYWRGaWxlUmVxdWVzdC5Jbml0aWFsVXBsb2FkTWV0YWRhdGFIABIUCgpjaHVua19kYXRhGAIgASgMSAAatgEKFUluaXRpYWxVcGxvYWRNZXRhZGF0YRIeCgx3b3Jrc3BhY2VfaWQYASABKAlCCLpIBXIDsAEBEh8KDWRlcGxveW1lbnRfaWQYAiABKAlCCLpIBXIDsAEBEioKCWZpbGVfcGF0aBgDIAEoCUIXukgUchIyEF5bXi9dKygvW14vXSspKiQSMAoMY29udGVudF90eXBlGAQgASgJQhq6SBdyFTITXlteL10rL1teL10rKDsuKik/JEIJCgdwYXlsb2FkIhQKElVwbG9hZEZpbGVSZXNwb25zZSJbChhQdWJsaXNoRGVwbG95bWVudFJlcXVlc3QSHgoMd29ya3NwYWNlX2lkGAEgASgJQgi6SAVyA7ABARIfCg1kZXBsb3ltZW50X2lkGAIgASgJQgi6SAVyA7ABASIoChlQdWJsaXNoRGVwbG95bWVudFJlc3BvbnNlEgsKA3VybBgBIAEoCSLmAQoWQWRkQ3VzdG9tRG9tYWluUmVxdWVzdBIeCgx3b3Jrc3BhY2VfaWQYASABKAlCCLpIBXIDsAEBEkUKE3N0YXRpY193ZWJzaXRlX25hbWUYAiABKAlCKLpIJXIjMiFeW2EtejAtOV1bYS16MC05LV17MSw2MX1bYS16MC05XSQSZQoGZG9tYWluGAMgASgJQlW6SFJyUBj9ATJLXlthLXowLTldKFthLXowLTktXXswLDYxfVthLXowLTldKT8oXC5bYS16MC05XShbYS16MC05LV17MCw2MX1bYS16MC05XSk/KSskIkkKF0FkZEN1c3RvbURvbWFpblJlc3BvbnNlEi4KDWN1c3RvbV9kb21haW4YASABKAsyFy50YWlsb3IudjEuQ3VzdG9tRG9tYWluIp8BChZHZXRDdXN0b21Eb21haW5SZXF1ZXN0Eh4KDHdvcmtzcGFjZV9pZBgBIAEoCUIIukgFcgOwAQESZQoGZG9tYWluGAIgASgJQlW6SFJyUBj9ATJLXlthLXowLTldKFthLXowLTktXXswLDYxfVthLXowLTldKT8oXC5bYS16MC05XShbYS16MC05LV17MCw2MX1bYS16MC05XSk/KSskIkkKF0dldEN1c3RvbURvbWFpblJlc3BvbnNlEi4KDWN1c3RvbV9kb21haW4YASABKAsyFy50YWlsb3IudjEuQ3VzdG9tRG9tYWluIoEBChhMaXN0Q3VzdG9tRG9tYWluc1JlcXVlc3QSHgoMd29ya3NwYWNlX2lkGAEgASgJQgi6SAVyA7ABARJFChNzdGF0aWNfd2Vic2l0ZV9uYW1lGAIgASgJQii6SCVyIzIhXlthLXowLTldW2EtejAtOS1dezEsNjF9W2EtejAtOV0kIkwKGUxpc3RDdXN0b21Eb21haW5zUmVzcG9uc2USLwoOY3VzdG9tX2RvbWFpbnMYASADKAsyFy50YWlsb3IudjEuQ3VzdG9tRG9tYWluIqIBChlSZW1vdmVDdXN0b21Eb21haW5SZXF1ZXN0Eh4KDHdvcmtzcGFjZV9pZBgBIAEoCUIIukgFcgOwAQESZQoGZG9tYWluGAIgASgJQlW6SFJyUBj9ATJLXlthLXowLTldKFthLXowLTktXXswLDYxfVthLXowLTldKT8oXC5bYS16MC05XShbYS16MC05LV17MCw2MX1bYS16MC05XSk/KSskIhwKGlJlbW92ZUN1c3RvbURvbWFpblJlc3BvbnNlYgZwcm90bzM", [file_buf_validate_validate, file_tailor_v1_resource, file_tailor_v1_staticwebsite_resource]); /** * Describes the message tailor.v1.CreateStaticWebsiteRequest. @@ -132,3 +132,59 @@ export const PublishDeploymentRequestSchema = /*@__PURE__*/ export const PublishDeploymentResponseSchema = /*@__PURE__*/ messageDesc(file_tailor_v1_staticwebsite, 15); +/** + * Describes the message tailor.v1.AddCustomDomainRequest. + * Use `create(AddCustomDomainRequestSchema)` to create a new message. + */ +export const AddCustomDomainRequestSchema = /*@__PURE__*/ + messageDesc(file_tailor_v1_staticwebsite, 16); + +/** + * Describes the message tailor.v1.AddCustomDomainResponse. + * Use `create(AddCustomDomainResponseSchema)` to create a new message. + */ +export const AddCustomDomainResponseSchema = /*@__PURE__*/ + messageDesc(file_tailor_v1_staticwebsite, 17); + +/** + * Describes the message tailor.v1.GetCustomDomainRequest. + * Use `create(GetCustomDomainRequestSchema)` to create a new message. + */ +export const GetCustomDomainRequestSchema = /*@__PURE__*/ + messageDesc(file_tailor_v1_staticwebsite, 18); + +/** + * Describes the message tailor.v1.GetCustomDomainResponse. + * Use `create(GetCustomDomainResponseSchema)` to create a new message. + */ +export const GetCustomDomainResponseSchema = /*@__PURE__*/ + messageDesc(file_tailor_v1_staticwebsite, 19); + +/** + * Describes the message tailor.v1.ListCustomDomainsRequest. + * Use `create(ListCustomDomainsRequestSchema)` to create a new message. + */ +export const ListCustomDomainsRequestSchema = /*@__PURE__*/ + messageDesc(file_tailor_v1_staticwebsite, 20); + +/** + * Describes the message tailor.v1.ListCustomDomainsResponse. + * Use `create(ListCustomDomainsResponseSchema)` to create a new message. + */ +export const ListCustomDomainsResponseSchema = /*@__PURE__*/ + messageDesc(file_tailor_v1_staticwebsite, 21); + +/** + * Describes the message tailor.v1.RemoveCustomDomainRequest. + * Use `create(RemoveCustomDomainRequestSchema)` to create a new message. + */ +export const RemoveCustomDomainRequestSchema = /*@__PURE__*/ + messageDesc(file_tailor_v1_staticwebsite, 22); + +/** + * Describes the message tailor.v1.RemoveCustomDomainResponse. + * Use `create(RemoveCustomDomainResponseSchema)` to create a new message. + */ +export const RemoveCustomDomainResponseSchema = /*@__PURE__*/ + messageDesc(file_tailor_v1_staticwebsite, 23); + diff --git a/packages/tailor-proto/src/tailor/v1/staticwebsite_resource_pb.d.ts b/packages/tailor-proto/src/tailor/v1/staticwebsite_resource_pb.d.ts index d115f7665..694aaddb2 100644 --- a/packages/tailor-proto/src/tailor/v1/staticwebsite_resource_pb.d.ts +++ b/packages/tailor-proto/src/tailor/v1/staticwebsite_resource_pb.d.ts @@ -2,8 +2,9 @@ // @generated from file tailor/v1/staticwebsite_resource.proto (package tailor.v1, syntax proto3) /* eslint-disable */ -import type { GenFile, GenMessage } from "@bufbuild/protobuf/codegenv2"; +import type { GenEnum, GenFile, GenMessage } from "@bufbuild/protobuf/codegenv2"; import type { Message } from "@bufbuild/protobuf"; +import type { Timestamp } from "@bufbuild/protobuf/wkt"; /** * Describes the file tailor/v1/staticwebsite_resource.proto. @@ -41,3 +42,89 @@ export declare type StaticWebsite = Message<"tailor.v1.StaticWebsite"> & { */ export declare const StaticWebsiteSchema: GenMessage; +/** + * @generated from message tailor.v1.CustomDomain + */ +export declare type CustomDomain = Message<"tailor.v1.CustomDomain"> & { + /** + * @generated from field: string domain = 1; + */ + domain: string; + + /** + * @generated from field: tailor.v1.CustomDomainStatus status = 2; + */ + status: CustomDomainStatus; + + /** + * @generated from field: string traffic_cname_target = 3; + */ + trafficCnameTarget: string; + + /** + * @generated from field: string certificate_cname_target = 4; + */ + certificateCnameTarget: string; + + /** + * @generated from field: string error_message = 5; + */ + errorMessage: string; + + /** + * @generated from field: google.protobuf.Timestamp created_at = 6; + */ + createdAt?: Timestamp; + + /** + * @generated from field: google.protobuf.Timestamp updated_at = 7; + */ + updatedAt?: Timestamp; +}; + +/** + * Describes the message tailor.v1.CustomDomain. + * Use `create(CustomDomainSchema)` to create a new message. + */ +export declare const CustomDomainSchema: GenMessage; + +/** + * @generated from enum tailor.v1.CustomDomainStatus + */ +export enum CustomDomainStatus { + /** + * @generated from enum value: CUSTOM_DOMAIN_STATUS_UNSPECIFIED = 0; + */ + UNSPECIFIED = 0, + + /** + * @generated from enum value: CUSTOM_DOMAIN_STATUS_PENDING = 1; + */ + PENDING = 1, + + /** + * @generated from enum value: CUSTOM_DOMAIN_STATUS_VERIFYING = 2; + */ + VERIFYING = 2, + + /** + * @generated from enum value: CUSTOM_DOMAIN_STATUS_CERT_ISSUED = 3; + */ + CERT_ISSUED = 3, + + /** + * @generated from enum value: CUSTOM_DOMAIN_STATUS_ACTIVE = 4; + */ + ACTIVE = 4, + + /** + * @generated from enum value: CUSTOM_DOMAIN_STATUS_FAILED = 5; + */ + FAILED = 5, +} + +/** + * Describes the enum tailor.v1.CustomDomainStatus. + */ +export declare const CustomDomainStatusSchema: GenEnum; + diff --git a/packages/tailor-proto/src/tailor/v1/staticwebsite_resource_pb.js b/packages/tailor-proto/src/tailor/v1/staticwebsite_resource_pb.js index 7371a0a66..3c821d30e 100644 --- a/packages/tailor-proto/src/tailor/v1/staticwebsite_resource_pb.js +++ b/packages/tailor-proto/src/tailor/v1/staticwebsite_resource_pb.js @@ -2,15 +2,16 @@ // @generated from file tailor/v1/staticwebsite_resource.proto (package tailor.v1, syntax proto3) /* eslint-disable */ -import { fileDesc, messageDesc } from "@bufbuild/protobuf/codegenv2"; +import { enumDesc, fileDesc, messageDesc, tsEnum } from "@bufbuild/protobuf/codegenv2"; import { file_buf_validate_validate } from "../../buf/validate/validate_pb"; import { file_google_api_field_behavior } from "../../google/api/field_behavior_pb"; +import { file_google_protobuf_timestamp } from "@bufbuild/protobuf/wkt"; /** * Describes the file tailor/v1/staticwebsite_resource.proto. */ export const file_tailor_v1_staticwebsite_resource = /*@__PURE__*/ - fileDesc("CiZ0YWlsb3IvdjEvc3RhdGljd2Vic2l0ZV9yZXNvdXJjZS5wcm90bxIJdGFpbG9yLnYxIowBCg1TdGF0aWNXZWJzaXRlEjYKBG5hbWUYASABKAlCKLpIJXIjMiFeW2EtejAtOV1bYS16MC05LV17MSw2MX1bYS16MC05XSQSEwoLZGVzY3JpcHRpb24YAiABKAkSHAoUYWxsb3dlZF9pcF9hZGRyZXNzZXMYAyADKAkSEAoDdXJsGAQgASgJQgPgQQNiBnByb3RvMw", [file_buf_validate_validate, file_google_api_field_behavior]); + fileDesc("CiZ0YWlsb3IvdjEvc3RhdGljd2Vic2l0ZV9yZXNvdXJjZS5wcm90bxIJdGFpbG9yLnYxIowBCg1TdGF0aWNXZWJzaXRlEjYKBG5hbWUYASABKAlCKLpIJXIjMiFeW2EtejAtOV1bYS16MC05LV17MSw2MX1bYS16MC05XSQSEwoLZGVzY3JpcHRpb24YAiABKAkSHAoUYWxsb3dlZF9pcF9hZGRyZXNzZXMYAyADKAkSEAoDdXJsGAQgASgJQgPgQQMiogIKDEN1c3RvbURvbWFpbhIOCgZkb21haW4YASABKAkSMgoGc3RhdHVzGAIgASgOMh0udGFpbG9yLnYxLkN1c3RvbURvbWFpblN0YXR1c0ID4EEDEiEKFHRyYWZmaWNfY25hbWVfdGFyZ2V0GAMgASgJQgPgQQMSJQoYY2VydGlmaWNhdGVfY25hbWVfdGFyZ2V0GAQgASgJQgPgQQMSGgoNZXJyb3JfbWVzc2FnZRgFIAEoCUID4EEDEjMKCmNyZWF0ZWRfYXQYBiABKAsyGi5nb29nbGUucHJvdG9idWYuVGltZXN0YW1wQgPgQQMSMwoKdXBkYXRlZF9hdBgHIAEoCzIaLmdvb2dsZS5wcm90b2J1Zi5UaW1lc3RhbXBCA+BBAyroAQoSQ3VzdG9tRG9tYWluU3RhdHVzEiQKIENVU1RPTV9ET01BSU5fU1RBVFVTX1VOU1BFQ0lGSUVEEAASIAocQ1VTVE9NX0RPTUFJTl9TVEFUVVNfUEVORElORxABEiIKHkNVU1RPTV9ET01BSU5fU1RBVFVTX1ZFUklGWUlORxACEiQKIENVU1RPTV9ET01BSU5fU1RBVFVTX0NFUlRfSVNTVUVEEAMSHwobQ1VTVE9NX0RPTUFJTl9TVEFUVVNfQUNUSVZFEAQSHwobQ1VTVE9NX0RPTUFJTl9TVEFUVVNfRkFJTEVEEAViBnByb3RvMw", [file_buf_validate_validate, file_google_api_field_behavior, file_google_protobuf_timestamp]); /** * Describes the message tailor.v1.StaticWebsite. @@ -19,3 +20,22 @@ export const file_tailor_v1_staticwebsite_resource = /*@__PURE__*/ export const StaticWebsiteSchema = /*@__PURE__*/ messageDesc(file_tailor_v1_staticwebsite_resource, 0); +/** + * Describes the message tailor.v1.CustomDomain. + * Use `create(CustomDomainSchema)` to create a new message. + */ +export const CustomDomainSchema = /*@__PURE__*/ + messageDesc(file_tailor_v1_staticwebsite_resource, 1); + +/** + * Describes the enum tailor.v1.CustomDomainStatus. + */ +export const CustomDomainStatusSchema = /*@__PURE__*/ + enumDesc(file_tailor_v1_staticwebsite_resource, 0); + +/** + * @generated from enum tailor.v1.CustomDomainStatus + */ +export const CustomDomainStatus = /*@__PURE__*/ + tsEnum(CustomDomainStatusSchema); + diff --git a/packages/tailor-proto/src/tailor/v1/tailordb_resource_pb.d.ts b/packages/tailor-proto/src/tailor/v1/tailordb_resource_pb.d.ts index 4b533d500..4524ef93a 100644 --- a/packages/tailor-proto/src/tailor/v1/tailordb_resource_pb.d.ts +++ b/packages/tailor-proto/src/tailor/v1/tailordb_resource_pb.d.ts @@ -112,6 +112,16 @@ export declare type TailorDBType_TypeConfig = Message<"tailor.v1.TailorDBType.Ty * @generated from field: map files = 11; */ files: { [key: string]: TailorDBType_FileConfig }; + + /** + * @generated from field: optional tailor.v1.TailorDBType.TypeHook type_hook = 12; + */ + typeHook?: TailorDBType_TypeHook; + + /** + * @generated from field: optional tailor.v1.TailorDBType.TypeValidate type_validate = 13; + */ + typeValidate?: TailorDBType_TypeValidate; }; /** @@ -465,6 +475,48 @@ export declare type TailorDBType_FieldHook = Message<"tailor.v1.TailorDBType.Fie */ export declare const TailorDBType_FieldHookSchema: GenMessage; +/** + * @generated from message tailor.v1.TailorDBType.TypeHook + */ +export declare type TailorDBType_TypeHook = Message<"tailor.v1.TailorDBType.TypeHook"> & { + /** + * @generated from field: tailor.v1.Script create = 1; + */ + create?: Script; + + /** + * @generated from field: tailor.v1.Script update = 2; + */ + update?: Script; +}; + +/** + * Describes the message tailor.v1.TailorDBType.TypeHook. + * Use `create(TailorDBType_TypeHookSchema)` to create a new message. + */ +export declare const TailorDBType_TypeHookSchema: GenMessage; + +/** + * @generated from message tailor.v1.TailorDBType.TypeValidate + */ +export declare type TailorDBType_TypeValidate = Message<"tailor.v1.TailorDBType.TypeValidate"> & { + /** + * @generated from field: tailor.v1.Script create = 1; + */ + create?: Script; + + /** + * @generated from field: tailor.v1.Script update = 2; + */ + update?: Script; +}; + +/** + * Describes the message tailor.v1.TailorDBType.TypeValidate. + * Use `create(TailorDBType_TypeValidateSchema)` to create a new message. + */ +export declare const TailorDBType_TypeValidateSchema: GenMessage; + /** * @generated from message tailor.v1.TailorDBType.Serial */ diff --git a/packages/tailor-proto/src/tailor/v1/tailordb_resource_pb.js b/packages/tailor-proto/src/tailor/v1/tailordb_resource_pb.js index bef27a82e..3c87f6799 100644 --- a/packages/tailor-proto/src/tailor/v1/tailordb_resource_pb.js +++ b/packages/tailor-proto/src/tailor/v1/tailordb_resource_pb.js @@ -12,7 +12,7 @@ import { file_tailor_v1_resource } from "./resource_pb"; * Describes the file tailor/v1/tailordb_resource.proto. */ export const file_tailor_v1_tailordb_resource = /*@__PURE__*/ - fileDesc("CiF0YWlsb3IvdjEvdGFpbG9yZGJfcmVzb3VyY2UucHJvdG8SCXRhaWxvci52MSJUCg9UYWlsb3JEQlNlcnZpY2USJwoJbmFtZXNwYWNlGAEgASgLMhQudGFpbG9yLnYxLk5hbWVzcGFjZRIYChBkZWZhdWx0X3RpbWV6b25lGAIgASgJIr4nCgxUYWlsb3JEQlR5cGUSLQoEbmFtZRgBIAEoCUIfukgcchoyGF5bQS1aXVthLXpBLVowLTldezAsNjJ9JBIyCgZzY2hlbWEYAiABKAsyIi50YWlsb3IudjEuVGFpbG9yREJUeXBlLlR5cGVDb25maWca3gcKClR5cGVDb25maWcSPgoGZmllbGRzGAEgAygLMi4udGFpbG9yLnYxLlRhaWxvckRCVHlwZS5UeXBlQ29uZmlnLkZpZWxkc0VudHJ5EhMKC2Rlc2NyaXB0aW9uGAIgASgJEjUKCHNldHRpbmdzGAMgASgLMiMudGFpbG9yLnYxLlRhaWxvckRCVHlwZS5UeXBlU2V0dGluZxI/Cg90eXBlX3Blcm1pc3Npb24YBCABKAsyJi50YWlsb3IudjEuVGFpbG9yREJUeXBlLlR5cGVQZXJtaXNzaW9uEg8KB2V4dGVuZHMYBSABKAgSNQoKZGlyZWN0aXZlcxgGIAMoCzIhLnRhaWxvci52MS5UYWlsb3JEQlR5cGUuRGlyZWN0aXZlEkAKB2luZGV4ZXMYByADKAsyLy50YWlsb3IudjEuVGFpbG9yREJUeXBlLlR5cGVDb25maWcuSW5kZXhlc0VudHJ5EkgKEXJlY29yZF9wZXJtaXNzaW9uGAggASgLMigudGFpbG9yLnYxLlRhaWxvckRCVHlwZS5SZWNvcmRQZXJtaXNzaW9uSACIAQESTAoNcmVsYXRpb25zaGlwcxgJIAMoCzI1LnRhaWxvci52MS5UYWlsb3JEQlR5cGUuVHlwZUNvbmZpZy5SZWxhdGlvbnNoaXBzRW50cnkSNgoKcGVybWlzc2lvbhgKIAEoCzIiLnRhaWxvci52MS5UYWlsb3JEQlR5cGUuUGVybWlzc2lvbhI8CgVmaWxlcxgLIAMoCzItLnRhaWxvci52MS5UYWlsb3JEQlR5cGUuVHlwZUNvbmZpZy5GaWxlc0VudHJ5GlIKC0ZpZWxkc0VudHJ5EgsKA2tleRgBIAEoCRIyCgV2YWx1ZRgCIAEoCzIjLnRhaWxvci52MS5UYWlsb3JEQlR5cGUuRmllbGRDb25maWc6AjgBGk0KDEluZGV4ZXNFbnRyeRILCgNrZXkYASABKAkSLAoFdmFsdWUYAiABKAsyHS50YWlsb3IudjEuVGFpbG9yREJUeXBlLkluZGV4OgI4ARpgChJSZWxhdGlvbnNoaXBzRW50cnkSCwoDa2V5GAEgASgJEjkKBXZhbHVlGAIgASgLMioudGFpbG9yLnYxLlRhaWxvckRCVHlwZS5SZWxhdGlvbnNoaXBDb25maWc6AjgBGlAKCkZpbGVzRW50cnkSCwoDa2V5GAEgASgJEjEKBXZhbHVlGAIgASgLMiIudGFpbG9yLnYxLlRhaWxvckRCVHlwZS5GaWxlQ29uZmlnOgI4AUIUChJfcmVjb3JkX3Blcm1pc3Npb24ahwMKC1R5cGVTZXR0aW5nEhMKC2FnZ3JlZ2F0aW9uGAIgASgIEhMKC2J1bGtfdXBzZXJ0GAMgASgIEiUKGGRlZmF1bHRfcXVlcnlfbGltaXRfc2l6ZRgEIAEoA0gAiAEBEiEKFG1heF9idWxrX3Vwc2VydF9zaXplGAUgASgDSAGIAQESPAoLcGx1cmFsX2Zvcm0YBiABKAlCIrpIH3IdMhteJHxeW2Etel1bYS16QS1aMC05XXswLDYyfSRIAogBARIdChVwdWJsaXNoX3JlY29yZF9ldmVudHMYByABKAgSDQoFZHJhZnQYCCABKAgSTAoWZGlzYWJsZV9ncWxfb3BlcmF0aW9ucxgJIAEoCzIsLnRhaWxvci52MS5UYWlsb3JEQlR5cGUuRGlzYWJsZUdxbE9wZXJhdGlvbnNCGwoZX2RlZmF1bHRfcXVlcnlfbGltaXRfc2l6ZUIXChVfbWF4X2J1bGtfdXBzZXJ0X3NpemVCDgoMX3BsdXJhbF9mb3JtSgQIARACGlQKFERpc2FibGVHcWxPcGVyYXRpb25zEg4KBmNyZWF0ZRgBIAEoCBIOCgZ1cGRhdGUYAiABKAgSDgoGZGVsZXRlGAMgASgIEgwKBHJlYWQYBCABKAgaTQoJRGlyZWN0aXZlEgwKBG5hbWUYASABKAkSMgoEYXJncxgCIAMoCzIkLnRhaWxvci52MS5UYWlsb3JEQlR5cGUuRGlyZWN0aXZlQXJnGisKDERpcmVjdGl2ZUFyZxIMCgRuYW1lGAEgASgJEg0KBXZhbHVlGAIgASgJGiwKBUluZGV4EhMKC2ZpZWxkX25hbWVzGAEgAygJEg4KBnVuaXF1ZRgCIAEoCBrLBQoLRmllbGRDb25maWcSDAoEdHlwZRgBIAEoCRITCgtkZXNjcmlwdGlvbhgDIAEoCRIWCglzb3VyY2VfaWQYBCABKAlIAIgBARIQCghyZXF1aXJlZBgFIAEoCBINCgVhcnJheRgGIAEoCBI4Cgh2YWxpZGF0ZRgHIAMoCzImLnRhaWxvci52MS5UYWlsb3JEQlR5cGUuVmFsaWRhdGVDb25maWcSPwoGZmllbGRzGAggAygLMi8udGFpbG9yLnYxLlRhaWxvckRCVHlwZS5GaWVsZENvbmZpZy5GaWVsZHNFbnRyeRINCgVpbmRleBgJIAEoCBIOCgZ1bmlxdWUYCiABKAgSEwoLZm9yZWlnbl9rZXkYCyABKAgSHQoQZm9yZWlnbl9rZXlfdHlwZRgMIAEoCUgBiAEBEh4KEWZvcmVpZ25fa2V5X2ZpZWxkGA0gASgJSAKIAQESNQoFaG9va3MYDiABKAsyIS50YWlsb3IudjEuVGFpbG9yREJUeXBlLkZpZWxkSG9va0gDiAEBEjUKDmFsbG93ZWRfdmFsdWVzGA8gAygLMh0udGFpbG9yLnYxLlRhaWxvckRCVHlwZS5WYWx1ZRIOCgZ2ZWN0b3IYECABKAgSLgoGc2VyaWFsGBEgASgLMh4udGFpbG9yLnYxLlRhaWxvckRCVHlwZS5TZXJpYWwSHQoFc2NhbGUYEiABKAVCCbpIBhoEGAwoAEgEiAEBGlIKC0ZpZWxkc0VudHJ5EgsKA2tleRgBIAEoCRIyCgV2YWx1ZRgCIAEoCzIjLnRhaWxvci52MS5UYWlsb3JEQlR5cGUuRmllbGRDb25maWc6AjgBQgwKCl9zb3VyY2VfaWRCEwoRX2ZvcmVpZ25fa2V5X3R5cGVCFAoSX2ZvcmVpZ25fa2V5X2ZpZWxkQggKBl9ob29rc0IICgZfc2NhbGVKBAgCEAMacAoSUmVsYXRpb25zaGlwQ29uZmlnEhAKCHJlZl90eXBlGAEgASgJEhEKCXJlZl9maWVsZBgCIAEoCRIRCglzcmNfZmllbGQYAyABKAkSDQoFYXJyYXkYBCABKAgSEwoLZGVzY3JpcHRpb24YBSABKAkaIQoKRmlsZUNvbmZpZxITCgtkZXNjcmlwdGlvbhgBIAEoCRpjCgVWYWx1ZRJFCgV2YWx1ZRgBIAEoCUI2ukgzcjEyHF5bYS16QS1aXVthLXpBLVowLTlfXXswLDYyfSRaBHRydWVaBWZhbHNlWgRudWxsEhMKC2Rlc2NyaXB0aW9uGAIgASgJGqUBCglGaWVsZEhvb2sSGAoLY3JlYXRlX2V4cHIYASABKAlIAIgBARIYCgt1cGRhdGVfZXhwchgCIAEoCUgBiAEBEiEKBmNyZWF0ZRgDIAEoCzIRLnRhaWxvci52MS5TY3JpcHQSIQoGdXBkYXRlGAQgASgLMhEudGFpbG9yLnYxLlNjcmlwdEIOCgxfY3JlYXRlX2V4cHJCDgoMX3VwZGF0ZV9leHByGq4BCgZTZXJpYWwSFgoFc3RhcnQYASABKANCB7pIBCICIAASHwoJbWF4X3ZhbHVlGAIgASgDQge6SAQiAiAASACIAQESUgoGZm9ybWF0GAMgASgJQj26SDpyOBggMjReKD86KD86JSV8W14lXSkqKSUoPzpbMC05XSspP1tkb3hYXSg/Oig/OiUlfFteJV0pKikkSAGIAQFCDAoKX21heF92YWx1ZUIJCgdfZm9ybWF0GrEBCg5WYWxpZGF0ZUNvbmZpZxIMCgRleHByGAEgASgJEkAKBmFjdGlvbhgCIAEoDjIkLnRhaWxvci52MS5UYWlsb3JEQlR5cGUuUGVybWl0QWN0aW9uQgq6SAeCAQQQASAAEhoKDWVycm9yX21lc3NhZ2UYAyABKAlIAIgBARIhCgZzY3JpcHQYBCABKAsyES50YWlsb3IudjEuU2NyaXB0QhAKDl9lcnJvcl9tZXNzYWdlGqUCCg5UeXBlUGVybWlzc2lvbhI2CgZjcmVhdGUYASADKAsyJi50YWlsb3IudjEuVGFpbG9yREJUeXBlLlBlcm1pc3Npb25JdGVtEjQKBHJlYWQYAiADKAsyJi50YWlsb3IudjEuVGFpbG9yREJUeXBlLlBlcm1pc3Npb25JdGVtEjYKBnVwZGF0ZRgDIAMoCzImLnRhaWxvci52MS5UYWlsb3JEQlR5cGUuUGVybWlzc2lvbkl0ZW0SNgoGZGVsZXRlGAQgAygLMiYudGFpbG9yLnYxLlRhaWxvckRCVHlwZS5QZXJtaXNzaW9uSXRlbRI1CgVhZG1pbhgFIAMoCzImLnRhaWxvci52MS5UYWlsb3JEQlR5cGUuUGVybWlzc2lvbkl0ZW0auAEKEFJlY29yZFBlcm1pc3Npb24SNAoEcmVhZBgBIAMoCzImLnRhaWxvci52MS5UYWlsb3JEQlR5cGUuUGVybWlzc2lvbkl0ZW0SNgoGdXBkYXRlGAIgAygLMiYudGFpbG9yLnYxLlRhaWxvckRCVHlwZS5QZXJtaXNzaW9uSXRlbRI2CgZkZWxldGUYAyADKAsyJi50YWlsb3IudjEuVGFpbG9yREJUeXBlLlBlcm1pc3Npb25JdGVtGmsKDlBlcm1pc3Npb25JdGVtEkAKBnBlcm1pdBgBIAEoDjIkLnRhaWxvci52MS5UYWlsb3JEQlR5cGUuUGVybWl0QWN0aW9uQgq6SAeCAQQQASAAEgoKAmlkGAIgASgJEgsKA2lkcxgDIAMoCRq+CAoKUGVybWlzc2lvbhI5CgZjcmVhdGUYASADKAsyKS50YWlsb3IudjEuVGFpbG9yREJUeXBlLlBlcm1pc3Npb24uUG9saWN5EjcKBHJlYWQYAiADKAsyKS50YWlsb3IudjEuVGFpbG9yREJUeXBlLlBlcm1pc3Npb24uUG9saWN5EjkKBnVwZGF0ZRgDIAMoCzIpLnRhaWxvci52MS5UYWlsb3JEQlR5cGUuUGVybWlzc2lvbi5Qb2xpY3kSOQoGZGVsZXRlGAQgAygLMikudGFpbG9yLnYxLlRhaWxvckRCVHlwZS5QZXJtaXNzaW9uLlBvbGljeRq7AQoGUG9saWN5EkAKCmNvbmRpdGlvbnMYASADKAsyLC50YWlsb3IudjEuVGFpbG9yREJUeXBlLlBlcm1pc3Npb24uQ29uZGl0aW9uEkUKBnBlcm1pdBgCIAEoDjIpLnRhaWxvci52MS5UYWlsb3JEQlR5cGUuUGVybWlzc2lvbi5QZXJtaXRCCrpIB4IBBBABIAASGAoLZGVzY3JpcHRpb24YAyABKAlIAIgBAUIOCgxfZGVzY3JpcHRpb24a2wEKCUNvbmRpdGlvbhJACgRsZWZ0GAEgASgLMioudGFpbG9yLnYxLlRhaWxvckRCVHlwZS5QZXJtaXNzaW9uLk9wZXJhbmRCBrpIA8gBARJJCghvcGVyYXRvchgCIAEoDjIrLnRhaWxvci52MS5UYWlsb3JEQlR5cGUuUGVybWlzc2lvbi5PcGVyYXRvckIKukgHggEEEAEgABJBCgVyaWdodBgDIAEoCzIqLnRhaWxvci52MS5UYWlsb3JEQlR5cGUuUGVybWlzc2lvbi5PcGVyYW5kQga6SAPIAQEaywEKB09wZXJhbmQSHQoKdXNlcl9maWVsZBgBIAEoCUIHukgEcgIQAUgAEh8KDHJlY29yZF9maWVsZBgCIAEoCUIHukgEcgIQAUgAEiMKEG9sZF9yZWNvcmRfZmllbGQYAyABKAlCB7pIBHICEAFIABIjChBuZXdfcmVjb3JkX2ZpZWxkGAQgASgJQge6SARyAhABSAASJwoFdmFsdWUYBSABKAsyFi5nb29nbGUucHJvdG9idWYuVmFsdWVIAEINCgRraW5kEgW6SAIIASKWAQoIT3BlcmF0b3ISGAoUT1BFUkFUT1JfVU5TUEVDSUZJRUQQABIPCgtPUEVSQVRPUl9FURABEg8KC09QRVJBVE9SX05FEAISDwoLT1BFUkFUT1JfSU4QAxIQCgxPUEVSQVRPUl9OSU4QBBIUChBPUEVSQVRPUl9IQVNfQU5ZEAUSFQoRT1BFUkFUT1JfTkhBU19BTlkQBiJDCgZQZXJtaXQSFgoSUEVSTUlUX1VOU1BFQ0lGSUVEEAASEAoMUEVSTUlUX0FMTE9XEAESDwoLUEVSTUlUX0RFTlkQAiJ6CgxQZXJtaXRBY3Rpb24SHQoZUEVSTUlUX0FDVElPTl9VTlNQRUNJRklFRBAAEhcKE1BFUk1JVF9BQ1RJT05fQUxMT1cQARIaChJQRVJNSVRfQUNUSU9OX1NLSVAQAhoCCAESFgoSUEVSTUlUX0FDVElPTl9ERU5ZEAMirQgKFVRhaWxvckRCR1FMUGVybWlzc2lvbhIPCgJpZBgBIAEoCUID4EEDEjkKCHBvbGljaWVzGAIgAygLMicudGFpbG9yLnYxLlRhaWxvckRCR1FMUGVybWlzc2lvbi5Qb2xpY3kahAIKBlBvbGljeRI+Cgpjb25kaXRpb25zGAEgAygLMioudGFpbG9yLnYxLlRhaWxvckRCR1FMUGVybWlzc2lvbi5Db25kaXRpb24SSwoHYWN0aW9ucxgCIAMoDjInLnRhaWxvci52MS5UYWlsb3JEQkdRTFBlcm1pc3Npb24uQWN0aW9uQhG6SA6SAQsIASIHggEEEAEgABJDCgZwZXJtaXQYAyABKA4yJy50YWlsb3IudjEuVGFpbG9yREJHUUxQZXJtaXNzaW9uLlBlcm1pdEIKukgHggEEEAEgABIYCgtkZXNjcmlwdGlvbhgEIAEoCUgAiAEBQg4KDF9kZXNjcmlwdGlvbhrVAQoJQ29uZGl0aW9uEj4KBGxlZnQYASABKAsyKC50YWlsb3IudjEuVGFpbG9yREJHUUxQZXJtaXNzaW9uLk9wZXJhbmRCBrpIA8gBARJHCghvcGVyYXRvchgCIAEoDjIpLnRhaWxvci52MS5UYWlsb3JEQkdRTFBlcm1pc3Npb24uT3BlcmF0b3JCCrpIB4IBBBABIAASPwoFcmlnaHQYAyABKAsyKC50YWlsb3IudjEuVGFpbG9yREJHUUxQZXJtaXNzaW9uLk9wZXJhbmRCBrpIA8gBARpgCgdPcGVyYW5kEh0KCnVzZXJfZmllbGQYASABKAlCB7pIBHICEAFIABInCgV2YWx1ZRgFIAEoCzIWLmdvb2dsZS5wcm90b2J1Zi5WYWx1ZUgAQg0KBGtpbmQSBbpIAggBIpYBCghPcGVyYXRvchIYChRPUEVSQVRPUl9VTlNQRUNJRklFRBAAEg8KC09QRVJBVE9SX0VREAESDwoLT1BFUkFUT1JfTkUQAhIPCgtPUEVSQVRPUl9JThADEhAKDE9QRVJBVE9SX05JThAEEhQKEE9QRVJBVE9SX0hBU19BTlkQBRIVChFPUEVSQVRPUl9OSEFTX0FOWRAGIqgBCgZBY3Rpb24SFgoSQUNUSU9OX1VOU1BFQ0lGSUVEEAASDgoKQUNUSU9OX0FMTBABEhEKDUFDVElPTl9DUkVBVEUQAhIPCgtBQ1RJT05fUkVBRBADEhEKDUFDVElPTl9VUERBVEUQBBIRCg1BQ1RJT05fREVMRVRFEAUSFAoQQUNUSU9OX0FHR1JFR0FURRAGEhYKEkFDVElPTl9CVUxLX1VQU0VSVBAHIkMKBlBlcm1pdBIWChJQRVJNSVRfVU5TUEVDSUZJRUQQABIQCgxQRVJNSVRfQUxMT1cQARIPCgtQRVJNSVRfREVOWRACYgZwcm90bzM", [file_buf_validate_validate, file_google_api_field_behavior, file_google_protobuf_struct, file_tailor_v1_resource]); + fileDesc("CiF0YWlsb3IvdjEvdGFpbG9yZGJfcmVzb3VyY2UucHJvdG8SCXRhaWxvci52MSJUCg9UYWlsb3JEQlNlcnZpY2USJwoJbmFtZXNwYWNlGAEgASgLMhQudGFpbG9yLnYxLk5hbWVzcGFjZRIYChBkZWZhdWx0X3RpbWV6b25lGAIgASgJIrIuCgxUYWlsb3JEQlR5cGUSLQoEbmFtZRgBIAEoCUIfukgcchoyGF5bQS1aXVthLXpBLVowLTldezAsNjJ9JBIyCgZzY2hlbWEYAiABKAsyIi50YWlsb3IudjEuVGFpbG9yREJUeXBlLlR5cGVDb25maWcaqg0KClR5cGVDb25maWcSPgoGZmllbGRzGAEgAygLMi4udGFpbG9yLnYxLlRhaWxvckRCVHlwZS5UeXBlQ29uZmlnLkZpZWxkc0VudHJ5EhMKC2Rlc2NyaXB0aW9uGAIgASgJEjUKCHNldHRpbmdzGAMgASgLMiMudGFpbG9yLnYxLlRhaWxvckRCVHlwZS5UeXBlU2V0dGluZxI/Cg90eXBlX3Blcm1pc3Npb24YBCABKAsyJi50YWlsb3IudjEuVGFpbG9yREJUeXBlLlR5cGVQZXJtaXNzaW9uEg8KB2V4dGVuZHMYBSABKAgSNQoKZGlyZWN0aXZlcxgGIAMoCzIhLnRhaWxvci52MS5UYWlsb3JEQlR5cGUuRGlyZWN0aXZlEkAKB2luZGV4ZXMYByADKAsyLy50YWlsb3IudjEuVGFpbG9yREJUeXBlLlR5cGVDb25maWcuSW5kZXhlc0VudHJ5EkgKEXJlY29yZF9wZXJtaXNzaW9uGAggASgLMigudGFpbG9yLnYxLlRhaWxvckRCVHlwZS5SZWNvcmRQZXJtaXNzaW9uSACIAQESTAoNcmVsYXRpb25zaGlwcxgJIAMoCzI1LnRhaWxvci52MS5UYWlsb3JEQlR5cGUuVHlwZUNvbmZpZy5SZWxhdGlvbnNoaXBzRW50cnkSNgoKcGVybWlzc2lvbhgKIAEoCzIiLnRhaWxvci52MS5UYWlsb3JEQlR5cGUuUGVybWlzc2lvbhI8CgVmaWxlcxgLIAMoCzItLnRhaWxvci52MS5UYWlsb3JEQlR5cGUuVHlwZUNvbmZpZy5GaWxlc0VudHJ5EjgKCXR5cGVfaG9vaxgMIAEoCzIgLnRhaWxvci52MS5UYWlsb3JEQlR5cGUuVHlwZUhvb2tIAYgBARJACg10eXBlX3ZhbGlkYXRlGA0gASgLMiQudGFpbG9yLnYxLlRhaWxvckRCVHlwZS5UeXBlVmFsaWRhdGVIAogBARpSCgtGaWVsZHNFbnRyeRILCgNrZXkYASABKAkSMgoFdmFsdWUYAiABKAsyIy50YWlsb3IudjEuVGFpbG9yREJUeXBlLkZpZWxkQ29uZmlnOgI4ARpNCgxJbmRleGVzRW50cnkSCwoDa2V5GAEgASgJEiwKBXZhbHVlGAIgASgLMh0udGFpbG9yLnYxLlRhaWxvckRCVHlwZS5JbmRleDoCOAEaYAoSUmVsYXRpb25zaGlwc0VudHJ5EgsKA2tleRgBIAEoCRI5CgV2YWx1ZRgCIAEoCzIqLnRhaWxvci52MS5UYWlsb3JEQlR5cGUuUmVsYXRpb25zaGlwQ29uZmlnOgI4ARpQCgpGaWxlc0VudHJ5EgsKA2tleRgBIAEoCRIxCgV2YWx1ZRgCIAEoCzIiLnRhaWxvci52MS5UYWlsb3JEQlR5cGUuRmlsZUNvbmZpZzoCOAE6rQS6SKkEGoICChV0eXBlX2hvb2tfZXhjbHVzaXZpdHkSQnR5cGVfaG9vayBhbmQgZmllbGQtbGV2ZWwgaG9va3MgY2Fubm90IGJlIGRlZmluZWQgYXQgdGhlIHNhbWUgdGltZRqkASFoYXModGhpcy50eXBlX2hvb2spIHx8ICh0aGlzLmZpZWxkcy5hbGwoaywgIWhhcyh0aGlzLmZpZWxkc1trXS5ob29rcykpICYmIHRoaXMuZmllbGRzLmFsbChrLCB0aGlzLmZpZWxkc1trXS5maWVsZHMuYWxsKG5rLCAhaGFzKHRoaXMuZmllbGRzW2tdLmZpZWxkc1tua10uaG9va3MpKSkpGqECChl0eXBlX3ZhbGlkYXRlX2V4Y2x1c2l2aXR5Ekl0eXBlX3ZhbGlkYXRlIGFuZCBmaWVsZC1sZXZlbCB2YWxpZGF0ZSBjYW5ub3QgYmUgZGVmaW5lZCBhdCB0aGUgc2FtZSB0aW1lGrgBIWhhcyh0aGlzLnR5cGVfdmFsaWRhdGUpIHx8ICh0aGlzLmZpZWxkcy5hbGwoaywgc2l6ZSh0aGlzLmZpZWxkc1trXS52YWxpZGF0ZSkgPT0gMCkgJiYgdGhpcy5maWVsZHMuYWxsKGssIHRoaXMuZmllbGRzW2tdLmZpZWxkcy5hbGwobmssIHNpemUodGhpcy5maWVsZHNba10uZmllbGRzW25rXS52YWxpZGF0ZSkgPT0gMCkpKUIUChJfcmVjb3JkX3Blcm1pc3Npb25CDAoKX3R5cGVfaG9va0IQCg5fdHlwZV92YWxpZGF0ZRqHAwoLVHlwZVNldHRpbmcSEwoLYWdncmVnYXRpb24YAiABKAgSEwoLYnVsa191cHNlcnQYAyABKAgSJQoYZGVmYXVsdF9xdWVyeV9saW1pdF9zaXplGAQgASgDSACIAQESIQoUbWF4X2J1bGtfdXBzZXJ0X3NpemUYBSABKANIAYgBARI8CgtwbHVyYWxfZm9ybRgGIAEoCUIiukgfch0yG14kfF5bYS16XVthLXpBLVowLTldezAsNjJ9JEgCiAEBEh0KFXB1Ymxpc2hfcmVjb3JkX2V2ZW50cxgHIAEoCBINCgVkcmFmdBgIIAEoCBJMChZkaXNhYmxlX2dxbF9vcGVyYXRpb25zGAkgASgLMiwudGFpbG9yLnYxLlRhaWxvckRCVHlwZS5EaXNhYmxlR3FsT3BlcmF0aW9uc0IbChlfZGVmYXVsdF9xdWVyeV9saW1pdF9zaXplQhcKFV9tYXhfYnVsa191cHNlcnRfc2l6ZUIOCgxfcGx1cmFsX2Zvcm1KBAgBEAIaVAoURGlzYWJsZUdxbE9wZXJhdGlvbnMSDgoGY3JlYXRlGAEgASgIEg4KBnVwZGF0ZRgCIAEoCBIOCgZkZWxldGUYAyABKAgSDAoEcmVhZBgEIAEoCBpNCglEaXJlY3RpdmUSDAoEbmFtZRgBIAEoCRIyCgRhcmdzGAIgAygLMiQudGFpbG9yLnYxLlRhaWxvckRCVHlwZS5EaXJlY3RpdmVBcmcaKwoMRGlyZWN0aXZlQXJnEgwKBG5hbWUYASABKAkSDQoFdmFsdWUYAiABKAkaLAoFSW5kZXgSEwoLZmllbGRfbmFtZXMYASADKAkSDgoGdW5pcXVlGAIgASgIGssFCgtGaWVsZENvbmZpZxIMCgR0eXBlGAEgASgJEhMKC2Rlc2NyaXB0aW9uGAMgASgJEhYKCXNvdXJjZV9pZBgEIAEoCUgAiAEBEhAKCHJlcXVpcmVkGAUgASgIEg0KBWFycmF5GAYgASgIEjgKCHZhbGlkYXRlGAcgAygLMiYudGFpbG9yLnYxLlRhaWxvckRCVHlwZS5WYWxpZGF0ZUNvbmZpZxI/CgZmaWVsZHMYCCADKAsyLy50YWlsb3IudjEuVGFpbG9yREJUeXBlLkZpZWxkQ29uZmlnLkZpZWxkc0VudHJ5Eg0KBWluZGV4GAkgASgIEg4KBnVuaXF1ZRgKIAEoCBITCgtmb3JlaWduX2tleRgLIAEoCBIdChBmb3JlaWduX2tleV90eXBlGAwgASgJSAGIAQESHgoRZm9yZWlnbl9rZXlfZmllbGQYDSABKAlIAogBARI1CgVob29rcxgOIAEoCzIhLnRhaWxvci52MS5UYWlsb3JEQlR5cGUuRmllbGRIb29rSAOIAQESNQoOYWxsb3dlZF92YWx1ZXMYDyADKAsyHS50YWlsb3IudjEuVGFpbG9yREJUeXBlLlZhbHVlEg4KBnZlY3RvchgQIAEoCBIuCgZzZXJpYWwYESABKAsyHi50YWlsb3IudjEuVGFpbG9yREJUeXBlLlNlcmlhbBIdCgVzY2FsZRgSIAEoBUIJukgGGgQYDCgASASIAQEaUgoLRmllbGRzRW50cnkSCwoDa2V5GAEgASgJEjIKBXZhbHVlGAIgASgLMiMudGFpbG9yLnYxLlRhaWxvckRCVHlwZS5GaWVsZENvbmZpZzoCOAFCDAoKX3NvdXJjZV9pZEITChFfZm9yZWlnbl9rZXlfdHlwZUIUChJfZm9yZWlnbl9rZXlfZmllbGRCCAoGX2hvb2tzQggKBl9zY2FsZUoECAIQAxpwChJSZWxhdGlvbnNoaXBDb25maWcSEAoIcmVmX3R5cGUYASABKAkSEQoJcmVmX2ZpZWxkGAIgASgJEhEKCXNyY19maWVsZBgDIAEoCRINCgVhcnJheRgEIAEoCBITCgtkZXNjcmlwdGlvbhgFIAEoCRohCgpGaWxlQ29uZmlnEhMKC2Rlc2NyaXB0aW9uGAEgASgJGmMKBVZhbHVlEkUKBXZhbHVlGAEgASgJQja6SDNyMTIcXlthLXpBLVpdW2EtekEtWjAtOV9dezAsNjJ9JFoEdHJ1ZVoFZmFsc2VaBG51bGwSEwoLZGVzY3JpcHRpb24YAiABKAkapQEKCUZpZWxkSG9vaxIYCgtjcmVhdGVfZXhwchgBIAEoCUgAiAEBEhgKC3VwZGF0ZV9leHByGAIgASgJSAGIAQESIQoGY3JlYXRlGAMgASgLMhEudGFpbG9yLnYxLlNjcmlwdBIhCgZ1cGRhdGUYBCABKAsyES50YWlsb3IudjEuU2NyaXB0Qg4KDF9jcmVhdGVfZXhwckIOCgxfdXBkYXRlX2V4cHIaUAoIVHlwZUhvb2sSIQoGY3JlYXRlGAEgASgLMhEudGFpbG9yLnYxLlNjcmlwdBIhCgZ1cGRhdGUYAiABKAsyES50YWlsb3IudjEuU2NyaXB0GlQKDFR5cGVWYWxpZGF0ZRIhCgZjcmVhdGUYASABKAsyES50YWlsb3IudjEuU2NyaXB0EiEKBnVwZGF0ZRgCIAEoCzIRLnRhaWxvci52MS5TY3JpcHQargEKBlNlcmlhbBIWCgVzdGFydBgBIAEoA0IHukgEIgIgABIfCgltYXhfdmFsdWUYAiABKANCB7pIBCICIABIAIgBARJSCgZmb3JtYXQYAyABKAlCPbpIOnI4GCAyNF4oPzooPzolJXxbXiVdKSopJSg/OlswLTldKyk/W2RveFhdKD86KD86JSV8W14lXSkqKSRIAYgBAUIMCgpfbWF4X3ZhbHVlQgkKB19mb3JtYXQasQEKDlZhbGlkYXRlQ29uZmlnEgwKBGV4cHIYASABKAkSQAoGYWN0aW9uGAIgASgOMiQudGFpbG9yLnYxLlRhaWxvckRCVHlwZS5QZXJtaXRBY3Rpb25CCrpIB4IBBBABIAASGgoNZXJyb3JfbWVzc2FnZRgDIAEoCUgAiAEBEiEKBnNjcmlwdBgEIAEoCzIRLnRhaWxvci52MS5TY3JpcHRCEAoOX2Vycm9yX21lc3NhZ2UapQIKDlR5cGVQZXJtaXNzaW9uEjYKBmNyZWF0ZRgBIAMoCzImLnRhaWxvci52MS5UYWlsb3JEQlR5cGUuUGVybWlzc2lvbkl0ZW0SNAoEcmVhZBgCIAMoCzImLnRhaWxvci52MS5UYWlsb3JEQlR5cGUuUGVybWlzc2lvbkl0ZW0SNgoGdXBkYXRlGAMgAygLMiYudGFpbG9yLnYxLlRhaWxvckRCVHlwZS5QZXJtaXNzaW9uSXRlbRI2CgZkZWxldGUYBCADKAsyJi50YWlsb3IudjEuVGFpbG9yREJUeXBlLlBlcm1pc3Npb25JdGVtEjUKBWFkbWluGAUgAygLMiYudGFpbG9yLnYxLlRhaWxvckRCVHlwZS5QZXJtaXNzaW9uSXRlbRq4AQoQUmVjb3JkUGVybWlzc2lvbhI0CgRyZWFkGAEgAygLMiYudGFpbG9yLnYxLlRhaWxvckRCVHlwZS5QZXJtaXNzaW9uSXRlbRI2CgZ1cGRhdGUYAiADKAsyJi50YWlsb3IudjEuVGFpbG9yREJUeXBlLlBlcm1pc3Npb25JdGVtEjYKBmRlbGV0ZRgDIAMoCzImLnRhaWxvci52MS5UYWlsb3JEQlR5cGUuUGVybWlzc2lvbkl0ZW0aawoOUGVybWlzc2lvbkl0ZW0SQAoGcGVybWl0GAEgASgOMiQudGFpbG9yLnYxLlRhaWxvckRCVHlwZS5QZXJtaXRBY3Rpb25CCrpIB4IBBBABIAASCgoCaWQYAiABKAkSCwoDaWRzGAMgAygJGr4ICgpQZXJtaXNzaW9uEjkKBmNyZWF0ZRgBIAMoCzIpLnRhaWxvci52MS5UYWlsb3JEQlR5cGUuUGVybWlzc2lvbi5Qb2xpY3kSNwoEcmVhZBgCIAMoCzIpLnRhaWxvci52MS5UYWlsb3JEQlR5cGUuUGVybWlzc2lvbi5Qb2xpY3kSOQoGdXBkYXRlGAMgAygLMikudGFpbG9yLnYxLlRhaWxvckRCVHlwZS5QZXJtaXNzaW9uLlBvbGljeRI5CgZkZWxldGUYBCADKAsyKS50YWlsb3IudjEuVGFpbG9yREJUeXBlLlBlcm1pc3Npb24uUG9saWN5GrsBCgZQb2xpY3kSQAoKY29uZGl0aW9ucxgBIAMoCzIsLnRhaWxvci52MS5UYWlsb3JEQlR5cGUuUGVybWlzc2lvbi5Db25kaXRpb24SRQoGcGVybWl0GAIgASgOMikudGFpbG9yLnYxLlRhaWxvckRCVHlwZS5QZXJtaXNzaW9uLlBlcm1pdEIKukgHggEEEAEgABIYCgtkZXNjcmlwdGlvbhgDIAEoCUgAiAEBQg4KDF9kZXNjcmlwdGlvbhrbAQoJQ29uZGl0aW9uEkAKBGxlZnQYASABKAsyKi50YWlsb3IudjEuVGFpbG9yREJUeXBlLlBlcm1pc3Npb24uT3BlcmFuZEIGukgDyAEBEkkKCG9wZXJhdG9yGAIgASgOMisudGFpbG9yLnYxLlRhaWxvckRCVHlwZS5QZXJtaXNzaW9uLk9wZXJhdG9yQgq6SAeCAQQQASAAEkEKBXJpZ2h0GAMgASgLMioudGFpbG9yLnYxLlRhaWxvckRCVHlwZS5QZXJtaXNzaW9uLk9wZXJhbmRCBrpIA8gBARrLAQoHT3BlcmFuZBIdCgp1c2VyX2ZpZWxkGAEgASgJQge6SARyAhABSAASHwoMcmVjb3JkX2ZpZWxkGAIgASgJQge6SARyAhABSAASIwoQb2xkX3JlY29yZF9maWVsZBgDIAEoCUIHukgEcgIQAUgAEiMKEG5ld19yZWNvcmRfZmllbGQYBCABKAlCB7pIBHICEAFIABInCgV2YWx1ZRgFIAEoCzIWLmdvb2dsZS5wcm90b2J1Zi5WYWx1ZUgAQg0KBGtpbmQSBbpIAggBIpYBCghPcGVyYXRvchIYChRPUEVSQVRPUl9VTlNQRUNJRklFRBAAEg8KC09QRVJBVE9SX0VREAESDwoLT1BFUkFUT1JfTkUQAhIPCgtPUEVSQVRPUl9JThADEhAKDE9QRVJBVE9SX05JThAEEhQKEE9QRVJBVE9SX0hBU19BTlkQBRIVChFPUEVSQVRPUl9OSEFTX0FOWRAGIkMKBlBlcm1pdBIWChJQRVJNSVRfVU5TUEVDSUZJRUQQABIQCgxQRVJNSVRfQUxMT1cQARIPCgtQRVJNSVRfREVOWRACInoKDFBlcm1pdEFjdGlvbhIdChlQRVJNSVRfQUNUSU9OX1VOU1BFQ0lGSUVEEAASFwoTUEVSTUlUX0FDVElPTl9BTExPVxABEhoKElBFUk1JVF9BQ1RJT05fU0tJUBACGgIIARIWChJQRVJNSVRfQUNUSU9OX0RFTlkQAyKtCAoVVGFpbG9yREJHUUxQZXJtaXNzaW9uEg8KAmlkGAEgASgJQgPgQQMSOQoIcG9saWNpZXMYAiADKAsyJy50YWlsb3IudjEuVGFpbG9yREJHUUxQZXJtaXNzaW9uLlBvbGljeRqEAgoGUG9saWN5Ej4KCmNvbmRpdGlvbnMYASADKAsyKi50YWlsb3IudjEuVGFpbG9yREJHUUxQZXJtaXNzaW9uLkNvbmRpdGlvbhJLCgdhY3Rpb25zGAIgAygOMicudGFpbG9yLnYxLlRhaWxvckRCR1FMUGVybWlzc2lvbi5BY3Rpb25CEbpIDpIBCwgBIgeCAQQQASAAEkMKBnBlcm1pdBgDIAEoDjInLnRhaWxvci52MS5UYWlsb3JEQkdRTFBlcm1pc3Npb24uUGVybWl0Qgq6SAeCAQQQASAAEhgKC2Rlc2NyaXB0aW9uGAQgASgJSACIAQFCDgoMX2Rlc2NyaXB0aW9uGtUBCglDb25kaXRpb24SPgoEbGVmdBgBIAEoCzIoLnRhaWxvci52MS5UYWlsb3JEQkdRTFBlcm1pc3Npb24uT3BlcmFuZEIGukgDyAEBEkcKCG9wZXJhdG9yGAIgASgOMikudGFpbG9yLnYxLlRhaWxvckRCR1FMUGVybWlzc2lvbi5PcGVyYXRvckIKukgHggEEEAEgABI/CgVyaWdodBgDIAEoCzIoLnRhaWxvci52MS5UYWlsb3JEQkdRTFBlcm1pc3Npb24uT3BlcmFuZEIGukgDyAEBGmAKB09wZXJhbmQSHQoKdXNlcl9maWVsZBgBIAEoCUIHukgEcgIQAUgAEicKBXZhbHVlGAUgASgLMhYuZ29vZ2xlLnByb3RvYnVmLlZhbHVlSABCDQoEa2luZBIFukgCCAEilgEKCE9wZXJhdG9yEhgKFE9QRVJBVE9SX1VOU1BFQ0lGSUVEEAASDwoLT1BFUkFUT1JfRVEQARIPCgtPUEVSQVRPUl9ORRACEg8KC09QRVJBVE9SX0lOEAMSEAoMT1BFUkFUT1JfTklOEAQSFAoQT1BFUkFUT1JfSEFTX0FOWRAFEhUKEU9QRVJBVE9SX05IQVNfQU5ZEAYiqAEKBkFjdGlvbhIWChJBQ1RJT05fVU5TUEVDSUZJRUQQABIOCgpBQ1RJT05fQUxMEAESEQoNQUNUSU9OX0NSRUFURRACEg8KC0FDVElPTl9SRUFEEAMSEQoNQUNUSU9OX1VQREFURRAEEhEKDUFDVElPTl9ERUxFVEUQBRIUChBBQ1RJT05fQUdHUkVHQVRFEAYSFgoSQUNUSU9OX0JVTEtfVVBTRVJUEAciQwoGUGVybWl0EhYKElBFUk1JVF9VTlNQRUNJRklFRBAAEhAKDFBFUk1JVF9BTExPVxABEg8KC1BFUk1JVF9ERU5ZEAJiBnByb3RvMw", [file_buf_validate_validate, file_google_api_field_behavior, file_google_protobuf_struct, file_tailor_v1_resource]); /** * Describes the message tailor.v1.TailorDBService. @@ -105,74 +105,88 @@ export const TailorDBType_ValueSchema = /*@__PURE__*/ export const TailorDBType_FieldHookSchema = /*@__PURE__*/ messageDesc(file_tailor_v1_tailordb_resource, 1, 10); +/** + * Describes the message tailor.v1.TailorDBType.TypeHook. + * Use `create(TailorDBType_TypeHookSchema)` to create a new message. + */ +export const TailorDBType_TypeHookSchema = /*@__PURE__*/ + messageDesc(file_tailor_v1_tailordb_resource, 1, 11); + +/** + * Describes the message tailor.v1.TailorDBType.TypeValidate. + * Use `create(TailorDBType_TypeValidateSchema)` to create a new message. + */ +export const TailorDBType_TypeValidateSchema = /*@__PURE__*/ + messageDesc(file_tailor_v1_tailordb_resource, 1, 12); + /** * Describes the message tailor.v1.TailorDBType.Serial. * Use `create(TailorDBType_SerialSchema)` to create a new message. */ export const TailorDBType_SerialSchema = /*@__PURE__*/ - messageDesc(file_tailor_v1_tailordb_resource, 1, 11); + messageDesc(file_tailor_v1_tailordb_resource, 1, 13); /** * Describes the message tailor.v1.TailorDBType.ValidateConfig. * Use `create(TailorDBType_ValidateConfigSchema)` to create a new message. */ export const TailorDBType_ValidateConfigSchema = /*@__PURE__*/ - messageDesc(file_tailor_v1_tailordb_resource, 1, 12); + messageDesc(file_tailor_v1_tailordb_resource, 1, 14); /** * Describes the message tailor.v1.TailorDBType.TypePermission. * Use `create(TailorDBType_TypePermissionSchema)` to create a new message. */ export const TailorDBType_TypePermissionSchema = /*@__PURE__*/ - messageDesc(file_tailor_v1_tailordb_resource, 1, 13); + messageDesc(file_tailor_v1_tailordb_resource, 1, 15); /** * Describes the message tailor.v1.TailorDBType.RecordPermission. * Use `create(TailorDBType_RecordPermissionSchema)` to create a new message. */ export const TailorDBType_RecordPermissionSchema = /*@__PURE__*/ - messageDesc(file_tailor_v1_tailordb_resource, 1, 14); + messageDesc(file_tailor_v1_tailordb_resource, 1, 16); /** * Describes the message tailor.v1.TailorDBType.PermissionItem. * Use `create(TailorDBType_PermissionItemSchema)` to create a new message. */ export const TailorDBType_PermissionItemSchema = /*@__PURE__*/ - messageDesc(file_tailor_v1_tailordb_resource, 1, 15); + messageDesc(file_tailor_v1_tailordb_resource, 1, 17); /** * Describes the message tailor.v1.TailorDBType.Permission. * Use `create(TailorDBType_PermissionSchema)` to create a new message. */ export const TailorDBType_PermissionSchema = /*@__PURE__*/ - messageDesc(file_tailor_v1_tailordb_resource, 1, 16); + messageDesc(file_tailor_v1_tailordb_resource, 1, 18); /** * Describes the message tailor.v1.TailorDBType.Permission.Policy. * Use `create(TailorDBType_Permission_PolicySchema)` to create a new message. */ export const TailorDBType_Permission_PolicySchema = /*@__PURE__*/ - messageDesc(file_tailor_v1_tailordb_resource, 1, 16, 0); + messageDesc(file_tailor_v1_tailordb_resource, 1, 18, 0); /** * Describes the message tailor.v1.TailorDBType.Permission.Condition. * Use `create(TailorDBType_Permission_ConditionSchema)` to create a new message. */ export const TailorDBType_Permission_ConditionSchema = /*@__PURE__*/ - messageDesc(file_tailor_v1_tailordb_resource, 1, 16, 1); + messageDesc(file_tailor_v1_tailordb_resource, 1, 18, 1); /** * Describes the message tailor.v1.TailorDBType.Permission.Operand. * Use `create(TailorDBType_Permission_OperandSchema)` to create a new message. */ export const TailorDBType_Permission_OperandSchema = /*@__PURE__*/ - messageDesc(file_tailor_v1_tailordb_resource, 1, 16, 2); + messageDesc(file_tailor_v1_tailordb_resource, 1, 18, 2); /** * Describes the enum tailor.v1.TailorDBType.Permission.Operator. */ export const TailorDBType_Permission_OperatorSchema = /*@__PURE__*/ - enumDesc(file_tailor_v1_tailordb_resource, 1, 16, 0); + enumDesc(file_tailor_v1_tailordb_resource, 1, 18, 0); /** * @generated from enum tailor.v1.TailorDBType.Permission.Operator @@ -184,7 +198,7 @@ export const TailorDBType_Permission_Operator = /*@__PURE__*/ * Describes the enum tailor.v1.TailorDBType.Permission.Permit. */ export const TailorDBType_Permission_PermitSchema = /*@__PURE__*/ - enumDesc(file_tailor_v1_tailordb_resource, 1, 16, 1); + enumDesc(file_tailor_v1_tailordb_resource, 1, 18, 1); /** * @generated from enum tailor.v1.TailorDBType.Permission.Permit diff --git a/packages/tailor-proto/src/tailor/v1/workspace_pb.d.ts b/packages/tailor-proto/src/tailor/v1/workspace_pb.d.ts index 99e3358d6..6bd148f55 100644 --- a/packages/tailor-proto/src/tailor/v1/workspace_pb.d.ts +++ b/packages/tailor-proto/src/tailor/v1/workspace_pb.d.ts @@ -5,7 +5,7 @@ import type { GenEnum, GenFile, GenMessage } from "@bufbuild/protobuf/codegenv2"; import type { Message } from "@bufbuild/protobuf"; import type { Folder, FolderAccess, FolderRole, Organization, OrganizationAccess, OrganizationRole, PlatformAccountPlan, Team, TeamMember, TeamRole, Workspace, WorkspacePlatformUser, WorkspacePlatformUserRole } from "./workspace_resource_pb"; -import type { FieldMask } from "@bufbuild/protobuf/wkt"; +import type { FieldMask, Timestamp } from "@bufbuild/protobuf/wkt"; import type { PageDirection } from "./resource_pb"; /** @@ -1621,6 +1621,290 @@ export declare type GetOrganizationFolderAccessResponse = Message<"tailor.v1.Get */ export declare const GetOrganizationFolderAccessResponseSchema: GenMessage; +/** + * @generated from message tailor.v1.OrganizationIPRestriction + */ +export declare type OrganizationIPRestriction = Message<"tailor.v1.OrganizationIPRestriction"> & { + /** + * @generated from field: string organization_id = 1; + */ + organizationId: string; + + /** + * @generated from field: repeated string allowed_ip_addresses = 2; + */ + allowedIpAddresses: string[]; + + /** + * @generated from field: google.protobuf.Timestamp created_at = 3; + */ + createdAt?: Timestamp; + + /** + * @generated from field: google.protobuf.Timestamp updated_at = 4; + */ + updatedAt?: Timestamp; +}; + +/** + * Describes the message tailor.v1.OrganizationIPRestriction. + * Use `create(OrganizationIPRestrictionSchema)` to create a new message. + */ +export declare const OrganizationIPRestrictionSchema: GenMessage; + +/** + * @generated from message tailor.v1.OrganizationFolderIPRestriction + */ +export declare type OrganizationFolderIPRestriction = Message<"tailor.v1.OrganizationFolderIPRestriction"> & { + /** + * @generated from field: string organization_id = 1; + */ + organizationId: string; + + /** + * @generated from field: string folder_id = 2; + */ + folderId: string; + + /** + * @generated from field: repeated string allowed_ip_addresses = 3; + */ + allowedIpAddresses: string[]; + + /** + * @generated from field: google.protobuf.Timestamp created_at = 4; + */ + createdAt?: Timestamp; + + /** + * @generated from field: google.protobuf.Timestamp updated_at = 5; + */ + updatedAt?: Timestamp; +}; + +/** + * Describes the message tailor.v1.OrganizationFolderIPRestriction. + * Use `create(OrganizationFolderIPRestrictionSchema)` to create a new message. + */ +export declare const OrganizationFolderIPRestrictionSchema: GenMessage; + +/** + * @generated from message tailor.v1.UpsertOrganizationIPRestrictionRequest + */ +export declare type UpsertOrganizationIPRestrictionRequest = Message<"tailor.v1.UpsertOrganizationIPRestrictionRequest"> & { + /** + * @generated from field: string organization_id = 1; + */ + organizationId: string; + + /** + * Must contain at least one entry. To remove the restriction entirely, call + * DeleteOrganizationIPRestriction — an empty list here is rejected so that + * "rule exists" and "non-empty allowlist" stay equivalent and Get's NotFound + * contract is unambiguous. + * + * @generated from field: repeated string allowed_ip_addresses = 2; + */ + allowedIpAddresses: string[]; +}; + +/** + * Describes the message tailor.v1.UpsertOrganizationIPRestrictionRequest. + * Use `create(UpsertOrganizationIPRestrictionRequestSchema)` to create a new message. + */ +export declare const UpsertOrganizationIPRestrictionRequestSchema: GenMessage; + +/** + * @generated from message tailor.v1.UpsertOrganizationIPRestrictionResponse + */ +export declare type UpsertOrganizationIPRestrictionResponse = Message<"tailor.v1.UpsertOrganizationIPRestrictionResponse"> & { + /** + * @generated from field: tailor.v1.OrganizationIPRestriction organization_ip_restriction = 1; + */ + organizationIpRestriction?: OrganizationIPRestriction; +}; + +/** + * Describes the message tailor.v1.UpsertOrganizationIPRestrictionResponse. + * Use `create(UpsertOrganizationIPRestrictionResponseSchema)` to create a new message. + */ +export declare const UpsertOrganizationIPRestrictionResponseSchema: GenMessage; + +/** + * @generated from message tailor.v1.GetOrganizationIPRestrictionRequest + */ +export declare type GetOrganizationIPRestrictionRequest = Message<"tailor.v1.GetOrganizationIPRestrictionRequest"> & { + /** + * @generated from field: string organization_id = 1; + */ + organizationId: string; +}; + +/** + * Describes the message tailor.v1.GetOrganizationIPRestrictionRequest. + * Use `create(GetOrganizationIPRestrictionRequestSchema)` to create a new message. + */ +export declare const GetOrganizationIPRestrictionRequestSchema: GenMessage; + +/** + * @generated from message tailor.v1.GetOrganizationIPRestrictionResponse + */ +export declare type GetOrganizationIPRestrictionResponse = Message<"tailor.v1.GetOrganizationIPRestrictionResponse"> & { + /** + * @generated from field: tailor.v1.OrganizationIPRestriction organization_ip_restriction = 1; + */ + organizationIpRestriction?: OrganizationIPRestriction; +}; + +/** + * Describes the message tailor.v1.GetOrganizationIPRestrictionResponse. + * Use `create(GetOrganizationIPRestrictionResponseSchema)` to create a new message. + */ +export declare const GetOrganizationIPRestrictionResponseSchema: GenMessage; + +/** + * @generated from message tailor.v1.DeleteOrganizationIPRestrictionRequest + */ +export declare type DeleteOrganizationIPRestrictionRequest = Message<"tailor.v1.DeleteOrganizationIPRestrictionRequest"> & { + /** + * @generated from field: string organization_id = 1; + */ + organizationId: string; +}; + +/** + * Describes the message tailor.v1.DeleteOrganizationIPRestrictionRequest. + * Use `create(DeleteOrganizationIPRestrictionRequestSchema)` to create a new message. + */ +export declare const DeleteOrganizationIPRestrictionRequestSchema: GenMessage; + +/** + * @generated from message tailor.v1.DeleteOrganizationIPRestrictionResponse + */ +export declare type DeleteOrganizationIPRestrictionResponse = Message<"tailor.v1.DeleteOrganizationIPRestrictionResponse"> & { +}; + +/** + * Describes the message tailor.v1.DeleteOrganizationIPRestrictionResponse. + * Use `create(DeleteOrganizationIPRestrictionResponseSchema)` to create a new message. + */ +export declare const DeleteOrganizationIPRestrictionResponseSchema: GenMessage; + +/** + * @generated from message tailor.v1.UpsertOrganizationFolderIPRestrictionRequest + */ +export declare type UpsertOrganizationFolderIPRestrictionRequest = Message<"tailor.v1.UpsertOrganizationFolderIPRestrictionRequest"> & { + /** + * @generated from field: string organization_id = 1; + */ + organizationId: string; + + /** + * @generated from field: string folder_id = 2; + */ + folderId: string; + + /** + * Must contain at least one entry; see UpsertOrganizationIPRestrictionRequest + * for rationale. Use DeleteOrganizationFolderIPRestriction to clear the rule. + * + * @generated from field: repeated string allowed_ip_addresses = 3; + */ + allowedIpAddresses: string[]; +}; + +/** + * Describes the message tailor.v1.UpsertOrganizationFolderIPRestrictionRequest. + * Use `create(UpsertOrganizationFolderIPRestrictionRequestSchema)` to create a new message. + */ +export declare const UpsertOrganizationFolderIPRestrictionRequestSchema: GenMessage; + +/** + * @generated from message tailor.v1.UpsertOrganizationFolderIPRestrictionResponse + */ +export declare type UpsertOrganizationFolderIPRestrictionResponse = Message<"tailor.v1.UpsertOrganizationFolderIPRestrictionResponse"> & { + /** + * @generated from field: tailor.v1.OrganizationFolderIPRestriction organization_folder_ip_restriction = 1; + */ + organizationFolderIpRestriction?: OrganizationFolderIPRestriction; +}; + +/** + * Describes the message tailor.v1.UpsertOrganizationFolderIPRestrictionResponse. + * Use `create(UpsertOrganizationFolderIPRestrictionResponseSchema)` to create a new message. + */ +export declare const UpsertOrganizationFolderIPRestrictionResponseSchema: GenMessage; + +/** + * @generated from message tailor.v1.GetOrganizationFolderIPRestrictionRequest + */ +export declare type GetOrganizationFolderIPRestrictionRequest = Message<"tailor.v1.GetOrganizationFolderIPRestrictionRequest"> & { + /** + * @generated from field: string organization_id = 1; + */ + organizationId: string; + + /** + * @generated from field: string folder_id = 2; + */ + folderId: string; +}; + +/** + * Describes the message tailor.v1.GetOrganizationFolderIPRestrictionRequest. + * Use `create(GetOrganizationFolderIPRestrictionRequestSchema)` to create a new message. + */ +export declare const GetOrganizationFolderIPRestrictionRequestSchema: GenMessage; + +/** + * @generated from message tailor.v1.GetOrganizationFolderIPRestrictionResponse + */ +export declare type GetOrganizationFolderIPRestrictionResponse = Message<"tailor.v1.GetOrganizationFolderIPRestrictionResponse"> & { + /** + * @generated from field: tailor.v1.OrganizationFolderIPRestriction organization_folder_ip_restriction = 1; + */ + organizationFolderIpRestriction?: OrganizationFolderIPRestriction; +}; + +/** + * Describes the message tailor.v1.GetOrganizationFolderIPRestrictionResponse. + * Use `create(GetOrganizationFolderIPRestrictionResponseSchema)` to create a new message. + */ +export declare const GetOrganizationFolderIPRestrictionResponseSchema: GenMessage; + +/** + * @generated from message tailor.v1.DeleteOrganizationFolderIPRestrictionRequest + */ +export declare type DeleteOrganizationFolderIPRestrictionRequest = Message<"tailor.v1.DeleteOrganizationFolderIPRestrictionRequest"> & { + /** + * @generated from field: string organization_id = 1; + */ + organizationId: string; + + /** + * @generated from field: string folder_id = 2; + */ + folderId: string; +}; + +/** + * Describes the message tailor.v1.DeleteOrganizationFolderIPRestrictionRequest. + * Use `create(DeleteOrganizationFolderIPRestrictionRequestSchema)` to create a new message. + */ +export declare const DeleteOrganizationFolderIPRestrictionRequestSchema: GenMessage; + +/** + * @generated from message tailor.v1.DeleteOrganizationFolderIPRestrictionResponse + */ +export declare type DeleteOrganizationFolderIPRestrictionResponse = Message<"tailor.v1.DeleteOrganizationFolderIPRestrictionResponse"> & { +}; + +/** + * Describes the message tailor.v1.DeleteOrganizationFolderIPRestrictionResponse. + * Use `create(DeleteOrganizationFolderIPRestrictionResponseSchema)` to create a new message. + */ +export declare const DeleteOrganizationFolderIPRestrictionResponseSchema: GenMessage; + /** * @generated from message tailor.v1.CreateOrganizationTeamRequest */ diff --git a/packages/tailor-proto/src/tailor/v1/workspace_pb.js b/packages/tailor-proto/src/tailor/v1/workspace_pb.js index 38cf209f3..815c25ad3 100644 --- a/packages/tailor-proto/src/tailor/v1/workspace_pb.js +++ b/packages/tailor-proto/src/tailor/v1/workspace_pb.js @@ -4,7 +4,7 @@ import { enumDesc, fileDesc, messageDesc, tsEnum } from "@bufbuild/protobuf/codegenv2"; import { file_buf_validate_validate } from "../../buf/validate/validate_pb"; -import { file_google_protobuf_field_mask } from "@bufbuild/protobuf/wkt"; +import { file_google_protobuf_field_mask, file_google_protobuf_timestamp } from "@bufbuild/protobuf/wkt"; import { file_tailor_fieldmask_v1_option } from "../fieldmask/v1/option_pb"; import { file_tailor_v1_resource } from "./resource_pb"; import { file_tailor_v1_workspace_resource } from "./workspace_resource_pb"; @@ -13,7 +13,7 @@ import { file_tailor_v1_workspace_resource } from "./workspace_resource_pb"; * Describes the file tailor/v1/workspace.proto. */ export const file_tailor_v1_workspace = /*@__PURE__*/ - fileDesc("Chl0YWlsb3IvdjEvd29ya3NwYWNlLnByb3RvEgl0YWlsb3IudjEiJgokTGlzdEF2YWlsYWJsZVdvcmtzcGFjZVJlZ2lvbnNSZXF1ZXN0IjgKJUxpc3RBdmFpbGFibGVXb3Jrc3BhY2VSZWdpb25zUmVzcG9uc2USDwoHcmVnaW9ucxgBIAMoCSLVAQoWQ3JlYXRlV29ya3NwYWNlUmVxdWVzdBJACg53b3Jrc3BhY2VfbmFtZRgBIAEoCUIoukglciMyIV5bYS16MC05XVthLXowLTktXXsxLDYxfVthLXowLTldJBIYChB3b3Jrc3BhY2VfcmVnaW9uGAIgASgJEiQKD29yZ2FuaXphdGlvbl9pZBgDIAEoCUILukgI2AEBcgOwAQESHgoJZm9sZGVyX2lkGAQgASgJQgu6SAjYAQFyA7ABARIZChFkZWxldGVfcHJvdGVjdGlvbhgFIAEoCCJCChdDcmVhdGVXb3Jrc3BhY2VSZXNwb25zZRInCgl3b3Jrc3BhY2UYASABKAsyFC50YWlsb3IudjEuV29ya3NwYWNlItgCChZVcGRhdGVXb3Jrc3BhY2VSZXF1ZXN0Eh4KDHdvcmtzcGFjZV9pZBgBIAEoCUIIukgFcgOwAQESQwoOd29ya3NwYWNlX25hbWUYAiABKAlCK7pIKNgBAXIjMiFeW2EtejAtOV1bYS16MC05LV17MSw2MX1bYS16MC05XSQSJAoPb3JnYW5pemF0aW9uX2lkGAMgASgJQgu6SAjYAQFyA7ABARIeCglmb2xkZXJfaWQYBCABKAlCC7pICNgBAXIDsAEBEhkKEWRlbGV0ZV9wcm90ZWN0aW9uGAUgASgIEngKC3VwZGF0ZV9tYXNrGGQgASgLMhouZ29vZ2xlLnByb3RvYnVmLkZpZWxkTWFza0JHivkrDndvcmtzcGFjZV9uYW1livkrD29yZ2FuaXphdGlvbl9pZIr5Kwlmb2xkZXJfaWSK+SsRZGVsZXRlX3Byb3RlY3Rpb24iQgoXVXBkYXRlV29ya3NwYWNlUmVzcG9uc2USJwoJd29ya3NwYWNlGAEgASgLMhQudGFpbG9yLnYxLldvcmtzcGFjZSI4ChZEZWxldGVXb3Jrc3BhY2VSZXF1ZXN0Eh4KDHdvcmtzcGFjZV9pZBgBIAEoCUIIukgFcgOwAQEiGQoXRGVsZXRlV29ya3NwYWNlUmVzcG9uc2Ui8wEKFUxpc3RXb3Jrc3BhY2VzUmVxdWVzdBISCgpwYWdlX3Rva2VuGAEgASgJEhEKCXBhZ2Vfc2l6ZRgCIAEoDRIwCg5wYWdlX2RpcmVjdGlvbhgDIAEoDjIYLnRhaWxvci52MS5QYWdlRGlyZWN0aW9uEkIKBHZpZXcYBCABKA4yJS50YWlsb3IudjEuTGlzdFdvcmtzcGFjZXNSZXF1ZXN0LlZpZXdCDbpICtgBAYIBBBABIAAiPQoEVmlldxIUChBWSUVXX1VOU1BFQ0lGSUVEEAASDAoIVklFV19BTEwQARIRCg1WSUVXX1BFUlNPTkFMEAIicAoWTGlzdFdvcmtzcGFjZXNSZXNwb25zZRIoCgp3b3Jrc3BhY2VzGAEgAygLMhQudGFpbG9yLnYxLldvcmtzcGFjZRIXCg9uZXh0X3BhZ2VfdG9rZW4YAiABKAkSEwoLdG90YWxfY291bnQYAyABKAMivwEKIUxpc3RPcmdhbml6YXRpb25Xb3Jrc3BhY2VzUmVxdWVzdBISCgpwYWdlX3Rva2VuGAEgASgJEhEKCXBhZ2Vfc2l6ZRgCIAEoDRIwCg5wYWdlX2RpcmVjdGlvbhgDIAEoDjIYLnRhaWxvci52MS5QYWdlRGlyZWN0aW9uEiEKD29yZ2FuaXphdGlvbl9pZBgEIAEoCUIIukgFcgOwAQESHgoJZm9sZGVyX2lkGAUgASgJQgu6SAjYAQFyA7ABASJ8CiJMaXN0T3JnYW5pemF0aW9uV29ya3NwYWNlc1Jlc3BvbnNlEhcKD25leHRfcGFnZV90b2tlbhgBIAEoCRITCgt0b3RhbF9jb3VudBgCIAEoAxIoCgp3b3Jrc3BhY2VzGAMgAygLMhQudGFpbG9yLnYxLldvcmtzcGFjZSI5ChdSZXN0b3JlV29ya3NwYWNlUmVxdWVzdBIeCgx3b3Jrc3BhY2VfaWQYASABKAlCCLpIBXIDsAEBIhoKGFJlc3RvcmVXb3Jrc3BhY2VSZXNwb25zZSI1ChNHZXRXb3Jrc3BhY2VSZXF1ZXN0Eh4KDHdvcmtzcGFjZV9pZBgBIAEoCUIIukgFcgOwAQEiPwoUR2V0V29ya3NwYWNlUmVzcG9uc2USJwoJd29ya3NwYWNlGAEgASgLMhQudGFpbG9yLnYxLldvcmtzcGFjZSKcAQohTGlzdFdvcmtzcGFjZVBsYXRmb3JtVXNlcnNSZXF1ZXN0EhIKCnBhZ2VfdG9rZW4YASABKAkSEQoJcGFnZV9zaXplGAIgASgNEh4KDHdvcmtzcGFjZV9pZBgDIAEoCUIIukgFcgOwAQESMAoOcGFnZV9kaXJlY3Rpb24YBCABKA4yGC50YWlsb3IudjEuUGFnZURpcmVjdGlvbiKWAQoiTGlzdFdvcmtzcGFjZVBsYXRmb3JtVXNlcnNSZXNwb25zZRIXCg9uZXh0X3BhZ2VfdG9rZW4YASABKAkSQgoYd29ya3NwYWNlX3BsYXRmb3JtX3VzZXJzGAIgAygLMiAudGFpbG9yLnYxLldvcmtzcGFjZVBsYXRmb3JtVXNlchITCgt0b3RhbF9jb3VudBgDIAEoAyIwCi5MaXN0QXZhaWxhYmxlV29ya3NwYWNlUGxhdGZvcm1Vc2VyUm9sZXNSZXF1ZXN0ImYKL0xpc3RBdmFpbGFibGVXb3Jrc3BhY2VQbGF0Zm9ybVVzZXJSb2xlc1Jlc3BvbnNlEjMKBXJvbGVzGAEgAygOMiQudGFpbG9yLnYxLldvcmtzcGFjZVBsYXRmb3JtVXNlclJvbGUikAEKIkludml0ZVdvcmtzcGFjZVBsYXRmb3JtVXNlclJlcXVlc3QSHgoMd29ya3NwYWNlX2lkGAEgASgJQgi6SAVyA7ABARIWCgVlbWFpbBgCIAEoCUIHukgEcgJgARIyCgRyb2xlGAMgASgOMiQudGFpbG9yLnYxLldvcmtzcGFjZVBsYXRmb3JtVXNlclJvbGUiJQojSW52aXRlV29ya3NwYWNlUGxhdGZvcm1Vc2VyUmVzcG9uc2UiXAoiUmVtb3ZlV29ya3NwYWNlUGxhdGZvcm1Vc2VyUmVxdWVzdBIeCgx3b3Jrc3BhY2VfaWQYASABKAlCCLpIBXIDsAEBEhYKBWVtYWlsGAIgASgJQge6SARyAmABIiUKI1JlbW92ZVdvcmtzcGFjZVBsYXRmb3JtVXNlclJlc3BvbnNlIpABCiJVcGRhdGVXb3Jrc3BhY2VQbGF0Zm9ybVVzZXJSZXF1ZXN0Eh4KDHdvcmtzcGFjZV9pZBgBIAEoCUIIukgFcgOwAQESFgoFZW1haWwYAiABKAlCB7pIBHICYAESMgoEcm9sZRgDIAEoDjIkLnRhaWxvci52MS5Xb3Jrc3BhY2VQbGF0Zm9ybVVzZXJSb2xlIiUKI1VwZGF0ZVdvcmtzcGFjZVBsYXRmb3JtVXNlclJlc3BvbnNlIkEKH0dldFdvcmtzcGFjZVBsYXRmb3JtVXNlclJlcXVlc3QSHgoMd29ya3NwYWNlX2lkGAEgASgJQgi6SAVyA7ABASJlCiBHZXRXb3Jrc3BhY2VQbGF0Zm9ybVVzZXJSZXNwb25zZRJBChd3b3Jrc3BhY2VfcGxhdGZvcm1fdXNlchgBIAEoCzIgLnRhaWxvci52MS5Xb3Jrc3BhY2VQbGF0Zm9ybVVzZXIiOQoXR2V0V29ya3NwYWNlUm9sZVJlcXVlc3QSHgoMd29ya3NwYWNlX2lkGAEgASgJQgi6SAVyA7ABASJOChhHZXRXb3Jrc3BhY2VSb2xlUmVzcG9uc2USMgoEcm9sZRgBIAEoDjIkLnRhaWxvci52MS5Xb3Jrc3BhY2VQbGF0Zm9ybVVzZXJSb2xlImIKGVVwZGF0ZU9yZ2FuaXphdGlvblJlcXVlc3QSIQoPb3JnYW5pemF0aW9uX2lkGAEgASgJQgi6SAVyA7ABARIiChFvcmdhbml6YXRpb25fbmFtZRgCIAEoCUIHukgEcgIQASJLChpVcGRhdGVPcmdhbml6YXRpb25SZXNwb25zZRItCgxvcmdhbml6YXRpb24YASABKAsyFy50YWlsb3IudjEuT3JnYW5pemF0aW9uIjsKFkdldE9yZ2FuaXphdGlvblJlcXVlc3QSIQoPb3JnYW5pemF0aW9uX2lkGAEgASgJQgi6SAVyA7ABASJIChdHZXRPcmdhbml6YXRpb25SZXNwb25zZRItCgxvcmdhbml6YXRpb24YASABKAsyFy50YWlsb3IudjEuT3JnYW5pemF0aW9uInMKGExpc3RPcmdhbml6YXRpb25zUmVxdWVzdBISCgpwYWdlX3Rva2VuGAEgASgJEhEKCXBhZ2Vfc2l6ZRgCIAEoDRIwCg5wYWdlX2RpcmVjdGlvbhgDIAEoDjIYLnRhaWxvci52MS5QYWdlRGlyZWN0aW9uInkKGUxpc3RPcmdhbml6YXRpb25zUmVzcG9uc2USLgoNb3JnYW5pemF0aW9ucxgBIAMoCzIXLnRhaWxvci52MS5Pcmdhbml6YXRpb24SFwoPbmV4dF9wYWdlX3Rva2VuGAIgASgJEhMKC3RvdGFsX2NvdW50GAMgASgDIh4KHExpc3RVc2VyT3JnYW5pemF0aW9uc1JlcXVlc3QihwIKHUxpc3RVc2VyT3JnYW5pemF0aW9uc1Jlc3BvbnNlElUKEnVzZXJfb3JnYW5pemF0aW9ucxgBIAMoCzI5LnRhaWxvci52MS5MaXN0VXNlck9yZ2FuaXphdGlvbnNSZXNwb25zZS5Vc2VyT3JnYW5pemF0aW9uGo4BChBVc2VyT3JnYW5pemF0aW9uEhcKD29yZ2FuaXphdGlvbl9pZBgBIAEoCRIZChFvcmdhbml6YXRpb25fbmFtZRgCIAEoCRIWCg5yb290X2ZvbGRlcl9pZBgDIAEoCRIYChByb290X2ZvbGRlcl9uYW1lGAQgASgJEhQKDGRpc3BsYXlfbmFtZRgFIAEoCSLnAQoeR3JhbnRPcmdhbml6YXRpb25BY2Nlc3NSZXF1ZXN0EiEKD29yZ2FuaXphdGlvbl9pZBgBIAEoCUIIukgFcgOwAQESNQoEcm9sZRgCIAEoDjIbLnRhaWxvci52MS5Pcmdhbml6YXRpb25Sb2xlQgq6SAeCAQQQASAAEhsKB3RlYW1faWQYAyABKAlCCLpIBXIDsAEBSAASGAoFZW1haWwYBCABKAlCB7pIBHICYAFIABIjCg9tYWNoaW5lX3VzZXJfaWQYBSABKAlCCLpIBXIDsAEBSABCDwoGbWVtYmVyEgW6SAIIASIhCh9HcmFudE9yZ2FuaXphdGlvbkFjY2Vzc1Jlc3BvbnNlIugBCh9VcGRhdGVPcmdhbml6YXRpb25BY2Nlc3NSZXF1ZXN0EiEKD29yZ2FuaXphdGlvbl9pZBgBIAEoCUIIukgFcgOwAQESNQoEcm9sZRgCIAEoDjIbLnRhaWxvci52MS5Pcmdhbml6YXRpb25Sb2xlQgq6SAeCAQQQASAAEhsKB3RlYW1faWQYAyABKAlCCLpIBXIDsAEBSAASGAoFZW1haWwYBCABKAlCB7pIBHICYAFIABIjCg9tYWNoaW5lX3VzZXJfaWQYBSABKAlCCLpIBXIDsAEBSABCDwoGbWVtYmVyEgW6SAIIASIiCiBVcGRhdGVPcmdhbml6YXRpb25BY2Nlc3NSZXNwb25zZSKxAQofUmV2b2tlT3JnYW5pemF0aW9uQWNjZXNzUmVxdWVzdBIhCg9vcmdhbml6YXRpb25faWQYASABKAlCCLpIBXIDsAEBEhsKB3RlYW1faWQYAiABKAlCCLpIBXIDsAEBSAASGAoFZW1haWwYAyABKAlCB7pIBHICYAFIABIjCg9tYWNoaW5lX3VzZXJfaWQYBCABKAlCCLpIBXIDsAEBSABCDwoGbWVtYmVyEgW6SAIIASIiCiBSZXZva2VPcmdhbml6YXRpb25BY2Nlc3NSZXNwb25zZSKdAQofTGlzdE9yZ2FuaXphdGlvbkFjY2Vzc2VzUmVxdWVzdBISCgpwYWdlX3Rva2VuGAEgASgJEhEKCXBhZ2Vfc2l6ZRgCIAEoDRIwCg5wYWdlX2RpcmVjdGlvbhgDIAEoDjIYLnRhaWxvci52MS5QYWdlRGlyZWN0aW9uEiEKD29yZ2FuaXphdGlvbl9pZBgEIAEoCUIIukgFcgOwAQEijgEKIExpc3RPcmdhbml6YXRpb25BY2Nlc3Nlc1Jlc3BvbnNlEjwKFW9yZ2FuaXphdGlvbl9hY2Nlc3NlcxgBIAMoCzIdLnRhaWxvci52MS5Pcmdhbml6YXRpb25BY2Nlc3MSFwoPbmV4dF9wYWdlX3Rva2VuGAIgASgJEhMKC3RvdGFsX2NvdW50GAMgASgDIq4BChxHZXRPcmdhbml6YXRpb25BY2Nlc3NSZXF1ZXN0EiEKD29yZ2FuaXphdGlvbl9pZBgBIAEoCUIIukgFcgOwAQESGwoHdGVhbV9pZBgCIAEoCUIIukgFcgOwAQFIABIYCgVlbWFpbBgDIAEoCUIHukgEcgJgAUgAEiMKD21hY2hpbmVfdXNlcl9pZBgEIAEoCUIIukgFcgOwAQFIAEIPCgZtZW1iZXISBbpIAggBIlsKHUdldE9yZ2FuaXphdGlvbkFjY2Vzc1Jlc3BvbnNlEjoKE29yZ2FuaXphdGlvbl9hY2Nlc3MYASABKAsyHS50YWlsb3IudjEuT3JnYW5pemF0aW9uQWNjZXNzIokBCh9DcmVhdGVPcmdhbml6YXRpb25Gb2xkZXJSZXF1ZXN0EiEKD29yZ2FuaXphdGlvbl9pZBgBIAEoCUIIukgFcgOwAQESJQoQcGFyZW50X2ZvbGRlcl9pZBgCIAEoCUILukgI2AEBcgOwAQESHAoLZm9sZGVyX25hbWUYAyABKAlCB7pIBHICEAEiRQogQ3JlYXRlT3JnYW5pemF0aW9uRm9sZGVyUmVzcG9uc2USIQoGZm9sZGVyGAEgASgLMhEudGFpbG9yLnYxLkZvbGRlciKmAQofVXBkYXRlT3JnYW5pemF0aW9uRm9sZGVyUmVxdWVzdBIhCg9vcmdhbml6YXRpb25faWQYASABKAlCCLpIBXIDsAEBEhsKCWZvbGRlcl9pZBgCIAEoCUIIukgFcgOwAQESJQoQcGFyZW50X2ZvbGRlcl9pZBgDIAEoCUILukgI2AEBcgOwAQESHAoLZm9sZGVyX25hbWUYBCABKAlCB7pIBHICEAEiRQogVXBkYXRlT3JnYW5pemF0aW9uRm9sZGVyUmVzcG9uc2USIQoGZm9sZGVyGAEgASgLMhEudGFpbG9yLnYxLkZvbGRlciJhCh9EZWxldGVPcmdhbml6YXRpb25Gb2xkZXJSZXF1ZXN0EiEKD29yZ2FuaXphdGlvbl9pZBgBIAEoCUIIukgFcgOwAQESGwoJZm9sZGVyX2lkGAIgASgJQgi6SAVyA7ABASIiCiBEZWxldGVPcmdhbml6YXRpb25Gb2xkZXJSZXNwb25zZSJeChxHZXRPcmdhbml6YXRpb25Gb2xkZXJSZXF1ZXN0EiEKD29yZ2FuaXphdGlvbl9pZBgBIAEoCUIIukgFcgOwAQESGwoJZm9sZGVyX2lkGAIgASgJQgi6SAVyA7ABASJCCh1HZXRPcmdhbml6YXRpb25Gb2xkZXJSZXNwb25zZRIhCgZmb2xkZXIYASABKAsyES50YWlsb3IudjEuRm9sZGVyIsMBCh5MaXN0T3JnYW5pemF0aW9uRm9sZGVyc1JlcXVlc3QSEgoKcGFnZV90b2tlbhgBIAEoCRIRCglwYWdlX3NpemUYAiABKA0SMAoOcGFnZV9kaXJlY3Rpb24YAyABKA4yGC50YWlsb3IudjEuUGFnZURpcmVjdGlvbhIhCg9vcmdhbml6YXRpb25faWQYBCABKAlCCLpIBXIDsAEBEiUKEHBhcmVudF9mb2xkZXJfaWQYBSABKAlCC7pICNgBAXIDsAEBInMKH0xpc3RPcmdhbml6YXRpb25Gb2xkZXJzUmVzcG9uc2USFwoPbmV4dF9wYWdlX3Rva2VuGAEgASgJEhMKC3RvdGFsX2NvdW50GAIgASgDEiIKB2ZvbGRlcnMYAyADKAsyES50YWlsb3IudjEuRm9sZGVyIoQCCiRHcmFudE9yZ2FuaXphdGlvbkZvbGRlckFjY2Vzc1JlcXVlc3QSIQoPb3JnYW5pemF0aW9uX2lkGAEgASgJQgi6SAVyA7ABARIbCglmb2xkZXJfaWQYAiABKAlCCLpIBXIDsAEBEi8KBHJvbGUYAyABKA4yFS50YWlsb3IudjEuRm9sZGVyUm9sZUIKukgHggEEEAEgABIbCgd0ZWFtX2lkGAQgASgJQgi6SAVyA7ABAUgAEhgKBWVtYWlsGAUgASgJQge6SARyAmABSAASIwoPbWFjaGluZV91c2VyX2lkGAYgASgJQgi6SAVyA7ABAUgAQg8KBm1lbWJlchIFukgCCAEiJwolR3JhbnRPcmdhbml6YXRpb25Gb2xkZXJBY2Nlc3NSZXNwb25zZSKFAgolVXBkYXRlT3JnYW5pemF0aW9uRm9sZGVyQWNjZXNzUmVxdWVzdBIhCg9vcmdhbml6YXRpb25faWQYASABKAlCCLpIBXIDsAEBEhsKCWZvbGRlcl9pZBgCIAEoCUIIukgFcgOwAQESLwoEcm9sZRgDIAEoDjIVLnRhaWxvci52MS5Gb2xkZXJSb2xlQgq6SAeCAQQQASAAEhsKB3RlYW1faWQYBCABKAlCCLpIBXIDsAEBSAASGAoFZW1haWwYBSABKAlCB7pIBHICYAFIABIjCg9tYWNoaW5lX3VzZXJfaWQYBiABKAlCCLpIBXIDsAEBSABCDwoGbWVtYmVyEgW6SAIIASIoCiZVcGRhdGVPcmdhbml6YXRpb25Gb2xkZXJBY2Nlc3NSZXNwb25zZSLUAQolUmV2b2tlT3JnYW5pemF0aW9uRm9sZGVyQWNjZXNzUmVxdWVzdBIhCg9vcmdhbml6YXRpb25faWQYASABKAlCCLpIBXIDsAEBEhsKCWZvbGRlcl9pZBgCIAEoCUIIukgFcgOwAQESGwoHdGVhbV9pZBgDIAEoCUIIukgFcgOwAQFIABIYCgVlbWFpbBgEIAEoCUIHukgEcgJgAUgAEiMKD21hY2hpbmVfdXNlcl9pZBgFIAEoCUIIukgFcgOwAQFIAEIPCgZtZW1iZXISBbpIAggBIigKJlJldm9rZU9yZ2FuaXphdGlvbkZvbGRlckFjY2Vzc1Jlc3BvbnNlIsABCiVMaXN0T3JnYW5pemF0aW9uRm9sZGVyQWNjZXNzZXNSZXF1ZXN0EhIKCnBhZ2VfdG9rZW4YASABKAkSEQoJcGFnZV9zaXplGAIgASgNEjAKDnBhZ2VfZGlyZWN0aW9uGAMgASgOMhgudGFpbG9yLnYxLlBhZ2VEaXJlY3Rpb24SIQoPb3JnYW5pemF0aW9uX2lkGAQgASgJQgi6SAVyA7ABARIbCglmb2xkZXJfaWQYBSABKAlCCLpIBXIDsAEBIogBCiZMaXN0T3JnYW5pemF0aW9uRm9sZGVyQWNjZXNzZXNSZXNwb25zZRIwCg9mb2xkZXJfYWNjZXNzZXMYASADKAsyFy50YWlsb3IudjEuRm9sZGVyQWNjZXNzEhcKD25leHRfcGFnZV90b2tlbhgCIAEoCRITCgt0b3RhbF9jb3VudBgDIAEoAyLRAQoiR2V0T3JnYW5pemF0aW9uRm9sZGVyQWNjZXNzUmVxdWVzdBIhCg9vcmdhbml6YXRpb25faWQYASABKAlCCLpIBXIDsAEBEhsKCWZvbGRlcl9pZBgCIAEoCUIIukgFcgOwAQESGwoHdGVhbV9pZBgDIAEoCUIIukgFcgOwAQFIABIYCgVlbWFpbBgEIAEoCUIHukgEcgJgAUgAEiMKD21hY2hpbmVfdXNlcl9pZBgFIAEoCUIIukgFcgOwAQFIAEIPCgZtZW1iZXISBbpIAggBIlUKI0dldE9yZ2FuaXphdGlvbkZvbGRlckFjY2Vzc1Jlc3BvbnNlEi4KDWZvbGRlcl9hY2Nlc3MYASABKAsyFy50YWlsb3IudjEuRm9sZGVyQWNjZXNzIl4KHUNyZWF0ZU9yZ2FuaXphdGlvblRlYW1SZXF1ZXN0EiEKD29yZ2FuaXphdGlvbl9pZBgBIAEoCUIIukgFcgOwAQESGgoJdGVhbV9uYW1lGAIgASgJQge6SARyAhABIj8KHkNyZWF0ZU9yZ2FuaXphdGlvblRlYW1SZXNwb25zZRIdCgR0ZWFtGAEgASgLMg8udGFpbG9yLnYxLlRlYW0ieQodVXBkYXRlT3JnYW5pemF0aW9uVGVhbVJlcXVlc3QSIQoPb3JnYW5pemF0aW9uX2lkGAEgASgJQgi6SAVyA7ABARIZCgd0ZWFtX2lkGAIgASgJQgi6SAVyA7ABARIaCgl0ZWFtX25hbWUYAyABKAlCB7pIBHICEAEiPwoeVXBkYXRlT3JnYW5pemF0aW9uVGVhbVJlc3BvbnNlEh0KBHRlYW0YASABKAsyDy50YWlsb3IudjEuVGVhbSJdCh1EZWxldGVPcmdhbml6YXRpb25UZWFtUmVxdWVzdBIhCg9vcmdhbml6YXRpb25faWQYASABKAlCCLpIBXIDsAEBEhkKB3RlYW1faWQYAiABKAlCCLpIBXIDsAEBIiAKHkRlbGV0ZU9yZ2FuaXphdGlvblRlYW1SZXNwb25zZSKaAQocTGlzdE9yZ2FuaXphdGlvblRlYW1zUmVxdWVzdBISCgpwYWdlX3Rva2VuGAEgASgJEhEKCXBhZ2Vfc2l6ZRgCIAEoDRIwCg5wYWdlX2RpcmVjdGlvbhgDIAEoDjIYLnRhaWxvci52MS5QYWdlRGlyZWN0aW9uEiEKD29yZ2FuaXphdGlvbl9pZBgEIAEoCUIIukgFcgOwAQEibQodTGlzdE9yZ2FuaXphdGlvblRlYW1zUmVzcG9uc2USHgoFdGVhbXMYASADKAsyDy50YWlsb3IudjEuVGVhbRIXCg9uZXh0X3BhZ2VfdG9rZW4YAiABKAkSEwoLdG90YWxfY291bnQYAyABKAMiWgoaR2V0T3JnYW5pemF0aW9uVGVhbVJlcXVlc3QSIQoPb3JnYW5pemF0aW9uX2lkGAEgASgJQgi6SAVyA7ABARIZCgd0ZWFtX2lkGAIgASgJQgi6SAVyA7ABASI8ChtHZXRPcmdhbml6YXRpb25UZWFtUmVzcG9uc2USHQoEdGVhbRgBIAEoCzIPLnRhaWxvci52MS5UZWFtIqcBCiBBZGRPcmdhbml6YXRpb25UZWFtTWVtYmVyUmVxdWVzdBIhCg9vcmdhbml6YXRpb25faWQYASABKAlCCLpIBXIDsAEBEhkKB3RlYW1faWQYAiABKAlCCLpIBXIDsAEBEhYKBWVtYWlsGAMgASgJQge6SARyAmABEi0KBHJvbGUYBCABKA4yEy50YWlsb3IudjEuVGVhbVJvbGVCCrpIB4IBBBABIAAiIwohQWRkT3JnYW5pemF0aW9uVGVhbU1lbWJlclJlc3BvbnNlIqoBCiNVcGRhdGVPcmdhbml6YXRpb25UZWFtTWVtYmVyUmVxdWVzdBIhCg9vcmdhbml6YXRpb25faWQYASABKAlCCLpIBXIDsAEBEhkKB3RlYW1faWQYAiABKAlCCLpIBXIDsAEBEhYKBWVtYWlsGAMgASgJQge6SARyAmABEi0KBHJvbGUYBCABKA4yEy50YWlsb3IudjEuVGVhbVJvbGVCCrpIB4IBBBABIAAiJgokVXBkYXRlT3JnYW5pemF0aW9uVGVhbU1lbWJlclJlc3BvbnNlInsKI1JlbW92ZU9yZ2FuaXphdGlvblRlYW1NZW1iZXJSZXF1ZXN0EiEKD29yZ2FuaXphdGlvbl9pZBgBIAEoCUIIukgFcgOwAQESGQoHdGVhbV9pZBgCIAEoCUIIukgFcgOwAQESFgoFZW1haWwYAyABKAlCB7pIBHICYAEiJgokUmVtb3ZlT3JnYW5pemF0aW9uVGVhbU1lbWJlclJlc3BvbnNlIrsBCiJMaXN0T3JnYW5pemF0aW9uVGVhbU1lbWJlcnNSZXF1ZXN0EhIKCnBhZ2VfdG9rZW4YASABKAkSEQoJcGFnZV9zaXplGAIgASgNEjAKDnBhZ2VfZGlyZWN0aW9uGAMgASgOMhgudGFpbG9yLnYxLlBhZ2VEaXJlY3Rpb24SIQoPb3JnYW5pemF0aW9uX2lkGAQgASgJQgi6SAVyA7ABARIZCgd0ZWFtX2lkGAUgASgJQgi6SAVyA7ABASKAAQojTGlzdE9yZ2FuaXphdGlvblRlYW1NZW1iZXJzUmVzcG9uc2USKwoMdGVhbV9tZW1iZXJzGAEgAygLMhUudGFpbG9yLnYxLlRlYW1NZW1iZXISFwoPbmV4dF9wYWdlX3Rva2VuGAIgASgJEhMKC3RvdGFsX2NvdW50GAMgASgDIngKIEdldE9yZ2FuaXphdGlvblRlYW1NZW1iZXJSZXF1ZXN0EiEKD29yZ2FuaXphdGlvbl9pZBgBIAEoCUIIukgFcgOwAQESGQoHdGVhbV9pZBgCIAEoCUIIukgFcgOwAQESFgoFZW1haWwYAyABKAlCB7pIBHICYAEiTwohR2V0T3JnYW5pemF0aW9uVGVhbU1lbWJlclJlc3BvbnNlEioKC3RlYW1fbWVtYmVyGAEgASgLMhUudGFpbG9yLnYxLlRlYW1NZW1iZXIiHwodR2V0UGxhdGZvcm1BY2NvdW50UGxhblJlcXVlc3QiVgoeR2V0UGxhdGZvcm1BY2NvdW50UGxhblJlc3BvbnNlEjQKDGN1cnJlbnRfcGxhbhgBIAEoCzIeLnRhaWxvci52MS5QbGF0Zm9ybUFjY291bnRQbGFuYgZwcm90bzM", [file_buf_validate_validate, file_google_protobuf_field_mask, file_tailor_fieldmask_v1_option, file_tailor_v1_resource, file_tailor_v1_workspace_resource]); + fileDesc("Chl0YWlsb3IvdjEvd29ya3NwYWNlLnByb3RvEgl0YWlsb3IudjEiJgokTGlzdEF2YWlsYWJsZVdvcmtzcGFjZVJlZ2lvbnNSZXF1ZXN0IjgKJUxpc3RBdmFpbGFibGVXb3Jrc3BhY2VSZWdpb25zUmVzcG9uc2USDwoHcmVnaW9ucxgBIAMoCSLVAQoWQ3JlYXRlV29ya3NwYWNlUmVxdWVzdBJACg53b3Jrc3BhY2VfbmFtZRgBIAEoCUIoukglciMyIV5bYS16MC05XVthLXowLTktXXsxLDYxfVthLXowLTldJBIYChB3b3Jrc3BhY2VfcmVnaW9uGAIgASgJEiQKD29yZ2FuaXphdGlvbl9pZBgDIAEoCUILukgI2AEBcgOwAQESHgoJZm9sZGVyX2lkGAQgASgJQgu6SAjYAQFyA7ABARIZChFkZWxldGVfcHJvdGVjdGlvbhgFIAEoCCJCChdDcmVhdGVXb3Jrc3BhY2VSZXNwb25zZRInCgl3b3Jrc3BhY2UYASABKAsyFC50YWlsb3IudjEuV29ya3NwYWNlItgCChZVcGRhdGVXb3Jrc3BhY2VSZXF1ZXN0Eh4KDHdvcmtzcGFjZV9pZBgBIAEoCUIIukgFcgOwAQESQwoOd29ya3NwYWNlX25hbWUYAiABKAlCK7pIKNgBAXIjMiFeW2EtejAtOV1bYS16MC05LV17MSw2MX1bYS16MC05XSQSJAoPb3JnYW5pemF0aW9uX2lkGAMgASgJQgu6SAjYAQFyA7ABARIeCglmb2xkZXJfaWQYBCABKAlCC7pICNgBAXIDsAEBEhkKEWRlbGV0ZV9wcm90ZWN0aW9uGAUgASgIEngKC3VwZGF0ZV9tYXNrGGQgASgLMhouZ29vZ2xlLnByb3RvYnVmLkZpZWxkTWFza0JHivkrDndvcmtzcGFjZV9uYW1livkrD29yZ2FuaXphdGlvbl9pZIr5Kwlmb2xkZXJfaWSK+SsRZGVsZXRlX3Byb3RlY3Rpb24iQgoXVXBkYXRlV29ya3NwYWNlUmVzcG9uc2USJwoJd29ya3NwYWNlGAEgASgLMhQudGFpbG9yLnYxLldvcmtzcGFjZSI4ChZEZWxldGVXb3Jrc3BhY2VSZXF1ZXN0Eh4KDHdvcmtzcGFjZV9pZBgBIAEoCUIIukgFcgOwAQEiGQoXRGVsZXRlV29ya3NwYWNlUmVzcG9uc2Ui8wEKFUxpc3RXb3Jrc3BhY2VzUmVxdWVzdBISCgpwYWdlX3Rva2VuGAEgASgJEhEKCXBhZ2Vfc2l6ZRgCIAEoDRIwCg5wYWdlX2RpcmVjdGlvbhgDIAEoDjIYLnRhaWxvci52MS5QYWdlRGlyZWN0aW9uEkIKBHZpZXcYBCABKA4yJS50YWlsb3IudjEuTGlzdFdvcmtzcGFjZXNSZXF1ZXN0LlZpZXdCDbpICtgBAYIBBBABIAAiPQoEVmlldxIUChBWSUVXX1VOU1BFQ0lGSUVEEAASDAoIVklFV19BTEwQARIRCg1WSUVXX1BFUlNPTkFMEAIicAoWTGlzdFdvcmtzcGFjZXNSZXNwb25zZRIoCgp3b3Jrc3BhY2VzGAEgAygLMhQudGFpbG9yLnYxLldvcmtzcGFjZRIXCg9uZXh0X3BhZ2VfdG9rZW4YAiABKAkSEwoLdG90YWxfY291bnQYAyABKAMivwEKIUxpc3RPcmdhbml6YXRpb25Xb3Jrc3BhY2VzUmVxdWVzdBISCgpwYWdlX3Rva2VuGAEgASgJEhEKCXBhZ2Vfc2l6ZRgCIAEoDRIwCg5wYWdlX2RpcmVjdGlvbhgDIAEoDjIYLnRhaWxvci52MS5QYWdlRGlyZWN0aW9uEiEKD29yZ2FuaXphdGlvbl9pZBgEIAEoCUIIukgFcgOwAQESHgoJZm9sZGVyX2lkGAUgASgJQgu6SAjYAQFyA7ABASJ8CiJMaXN0T3JnYW5pemF0aW9uV29ya3NwYWNlc1Jlc3BvbnNlEhcKD25leHRfcGFnZV90b2tlbhgBIAEoCRITCgt0b3RhbF9jb3VudBgCIAEoAxIoCgp3b3Jrc3BhY2VzGAMgAygLMhQudGFpbG9yLnYxLldvcmtzcGFjZSI5ChdSZXN0b3JlV29ya3NwYWNlUmVxdWVzdBIeCgx3b3Jrc3BhY2VfaWQYASABKAlCCLpIBXIDsAEBIhoKGFJlc3RvcmVXb3Jrc3BhY2VSZXNwb25zZSI1ChNHZXRXb3Jrc3BhY2VSZXF1ZXN0Eh4KDHdvcmtzcGFjZV9pZBgBIAEoCUIIukgFcgOwAQEiPwoUR2V0V29ya3NwYWNlUmVzcG9uc2USJwoJd29ya3NwYWNlGAEgASgLMhQudGFpbG9yLnYxLldvcmtzcGFjZSKcAQohTGlzdFdvcmtzcGFjZVBsYXRmb3JtVXNlcnNSZXF1ZXN0EhIKCnBhZ2VfdG9rZW4YASABKAkSEQoJcGFnZV9zaXplGAIgASgNEh4KDHdvcmtzcGFjZV9pZBgDIAEoCUIIukgFcgOwAQESMAoOcGFnZV9kaXJlY3Rpb24YBCABKA4yGC50YWlsb3IudjEuUGFnZURpcmVjdGlvbiKWAQoiTGlzdFdvcmtzcGFjZVBsYXRmb3JtVXNlcnNSZXNwb25zZRIXCg9uZXh0X3BhZ2VfdG9rZW4YASABKAkSQgoYd29ya3NwYWNlX3BsYXRmb3JtX3VzZXJzGAIgAygLMiAudGFpbG9yLnYxLldvcmtzcGFjZVBsYXRmb3JtVXNlchITCgt0b3RhbF9jb3VudBgDIAEoAyIwCi5MaXN0QXZhaWxhYmxlV29ya3NwYWNlUGxhdGZvcm1Vc2VyUm9sZXNSZXF1ZXN0ImYKL0xpc3RBdmFpbGFibGVXb3Jrc3BhY2VQbGF0Zm9ybVVzZXJSb2xlc1Jlc3BvbnNlEjMKBXJvbGVzGAEgAygOMiQudGFpbG9yLnYxLldvcmtzcGFjZVBsYXRmb3JtVXNlclJvbGUikAEKIkludml0ZVdvcmtzcGFjZVBsYXRmb3JtVXNlclJlcXVlc3QSHgoMd29ya3NwYWNlX2lkGAEgASgJQgi6SAVyA7ABARIWCgVlbWFpbBgCIAEoCUIHukgEcgJgARIyCgRyb2xlGAMgASgOMiQudGFpbG9yLnYxLldvcmtzcGFjZVBsYXRmb3JtVXNlclJvbGUiJQojSW52aXRlV29ya3NwYWNlUGxhdGZvcm1Vc2VyUmVzcG9uc2UiXAoiUmVtb3ZlV29ya3NwYWNlUGxhdGZvcm1Vc2VyUmVxdWVzdBIeCgx3b3Jrc3BhY2VfaWQYASABKAlCCLpIBXIDsAEBEhYKBWVtYWlsGAIgASgJQge6SARyAmABIiUKI1JlbW92ZVdvcmtzcGFjZVBsYXRmb3JtVXNlclJlc3BvbnNlIpABCiJVcGRhdGVXb3Jrc3BhY2VQbGF0Zm9ybVVzZXJSZXF1ZXN0Eh4KDHdvcmtzcGFjZV9pZBgBIAEoCUIIukgFcgOwAQESFgoFZW1haWwYAiABKAlCB7pIBHICYAESMgoEcm9sZRgDIAEoDjIkLnRhaWxvci52MS5Xb3Jrc3BhY2VQbGF0Zm9ybVVzZXJSb2xlIiUKI1VwZGF0ZVdvcmtzcGFjZVBsYXRmb3JtVXNlclJlc3BvbnNlIkEKH0dldFdvcmtzcGFjZVBsYXRmb3JtVXNlclJlcXVlc3QSHgoMd29ya3NwYWNlX2lkGAEgASgJQgi6SAVyA7ABASJlCiBHZXRXb3Jrc3BhY2VQbGF0Zm9ybVVzZXJSZXNwb25zZRJBChd3b3Jrc3BhY2VfcGxhdGZvcm1fdXNlchgBIAEoCzIgLnRhaWxvci52MS5Xb3Jrc3BhY2VQbGF0Zm9ybVVzZXIiOQoXR2V0V29ya3NwYWNlUm9sZVJlcXVlc3QSHgoMd29ya3NwYWNlX2lkGAEgASgJQgi6SAVyA7ABASJOChhHZXRXb3Jrc3BhY2VSb2xlUmVzcG9uc2USMgoEcm9sZRgBIAEoDjIkLnRhaWxvci52MS5Xb3Jrc3BhY2VQbGF0Zm9ybVVzZXJSb2xlImIKGVVwZGF0ZU9yZ2FuaXphdGlvblJlcXVlc3QSIQoPb3JnYW5pemF0aW9uX2lkGAEgASgJQgi6SAVyA7ABARIiChFvcmdhbml6YXRpb25fbmFtZRgCIAEoCUIHukgEcgIQASJLChpVcGRhdGVPcmdhbml6YXRpb25SZXNwb25zZRItCgxvcmdhbml6YXRpb24YASABKAsyFy50YWlsb3IudjEuT3JnYW5pemF0aW9uIjsKFkdldE9yZ2FuaXphdGlvblJlcXVlc3QSIQoPb3JnYW5pemF0aW9uX2lkGAEgASgJQgi6SAVyA7ABASJIChdHZXRPcmdhbml6YXRpb25SZXNwb25zZRItCgxvcmdhbml6YXRpb24YASABKAsyFy50YWlsb3IudjEuT3JnYW5pemF0aW9uInMKGExpc3RPcmdhbml6YXRpb25zUmVxdWVzdBISCgpwYWdlX3Rva2VuGAEgASgJEhEKCXBhZ2Vfc2l6ZRgCIAEoDRIwCg5wYWdlX2RpcmVjdGlvbhgDIAEoDjIYLnRhaWxvci52MS5QYWdlRGlyZWN0aW9uInkKGUxpc3RPcmdhbml6YXRpb25zUmVzcG9uc2USLgoNb3JnYW5pemF0aW9ucxgBIAMoCzIXLnRhaWxvci52MS5Pcmdhbml6YXRpb24SFwoPbmV4dF9wYWdlX3Rva2VuGAIgASgJEhMKC3RvdGFsX2NvdW50GAMgASgDIh4KHExpc3RVc2VyT3JnYW5pemF0aW9uc1JlcXVlc3QihwIKHUxpc3RVc2VyT3JnYW5pemF0aW9uc1Jlc3BvbnNlElUKEnVzZXJfb3JnYW5pemF0aW9ucxgBIAMoCzI5LnRhaWxvci52MS5MaXN0VXNlck9yZ2FuaXphdGlvbnNSZXNwb25zZS5Vc2VyT3JnYW5pemF0aW9uGo4BChBVc2VyT3JnYW5pemF0aW9uEhcKD29yZ2FuaXphdGlvbl9pZBgBIAEoCRIZChFvcmdhbml6YXRpb25fbmFtZRgCIAEoCRIWCg5yb290X2ZvbGRlcl9pZBgDIAEoCRIYChByb290X2ZvbGRlcl9uYW1lGAQgASgJEhQKDGRpc3BsYXlfbmFtZRgFIAEoCSLnAQoeR3JhbnRPcmdhbml6YXRpb25BY2Nlc3NSZXF1ZXN0EiEKD29yZ2FuaXphdGlvbl9pZBgBIAEoCUIIukgFcgOwAQESNQoEcm9sZRgCIAEoDjIbLnRhaWxvci52MS5Pcmdhbml6YXRpb25Sb2xlQgq6SAeCAQQQASAAEhsKB3RlYW1faWQYAyABKAlCCLpIBXIDsAEBSAASGAoFZW1haWwYBCABKAlCB7pIBHICYAFIABIjCg9tYWNoaW5lX3VzZXJfaWQYBSABKAlCCLpIBXIDsAEBSABCDwoGbWVtYmVyEgW6SAIIASIhCh9HcmFudE9yZ2FuaXphdGlvbkFjY2Vzc1Jlc3BvbnNlIugBCh9VcGRhdGVPcmdhbml6YXRpb25BY2Nlc3NSZXF1ZXN0EiEKD29yZ2FuaXphdGlvbl9pZBgBIAEoCUIIukgFcgOwAQESNQoEcm9sZRgCIAEoDjIbLnRhaWxvci52MS5Pcmdhbml6YXRpb25Sb2xlQgq6SAeCAQQQASAAEhsKB3RlYW1faWQYAyABKAlCCLpIBXIDsAEBSAASGAoFZW1haWwYBCABKAlCB7pIBHICYAFIABIjCg9tYWNoaW5lX3VzZXJfaWQYBSABKAlCCLpIBXIDsAEBSABCDwoGbWVtYmVyEgW6SAIIASIiCiBVcGRhdGVPcmdhbml6YXRpb25BY2Nlc3NSZXNwb25zZSKxAQofUmV2b2tlT3JnYW5pemF0aW9uQWNjZXNzUmVxdWVzdBIhCg9vcmdhbml6YXRpb25faWQYASABKAlCCLpIBXIDsAEBEhsKB3RlYW1faWQYAiABKAlCCLpIBXIDsAEBSAASGAoFZW1haWwYAyABKAlCB7pIBHICYAFIABIjCg9tYWNoaW5lX3VzZXJfaWQYBCABKAlCCLpIBXIDsAEBSABCDwoGbWVtYmVyEgW6SAIIASIiCiBSZXZva2VPcmdhbml6YXRpb25BY2Nlc3NSZXNwb25zZSKdAQofTGlzdE9yZ2FuaXphdGlvbkFjY2Vzc2VzUmVxdWVzdBISCgpwYWdlX3Rva2VuGAEgASgJEhEKCXBhZ2Vfc2l6ZRgCIAEoDRIwCg5wYWdlX2RpcmVjdGlvbhgDIAEoDjIYLnRhaWxvci52MS5QYWdlRGlyZWN0aW9uEiEKD29yZ2FuaXphdGlvbl9pZBgEIAEoCUIIukgFcgOwAQEijgEKIExpc3RPcmdhbml6YXRpb25BY2Nlc3Nlc1Jlc3BvbnNlEjwKFW9yZ2FuaXphdGlvbl9hY2Nlc3NlcxgBIAMoCzIdLnRhaWxvci52MS5Pcmdhbml6YXRpb25BY2Nlc3MSFwoPbmV4dF9wYWdlX3Rva2VuGAIgASgJEhMKC3RvdGFsX2NvdW50GAMgASgDIq4BChxHZXRPcmdhbml6YXRpb25BY2Nlc3NSZXF1ZXN0EiEKD29yZ2FuaXphdGlvbl9pZBgBIAEoCUIIukgFcgOwAQESGwoHdGVhbV9pZBgCIAEoCUIIukgFcgOwAQFIABIYCgVlbWFpbBgDIAEoCUIHukgEcgJgAUgAEiMKD21hY2hpbmVfdXNlcl9pZBgEIAEoCUIIukgFcgOwAQFIAEIPCgZtZW1iZXISBbpIAggBIlsKHUdldE9yZ2FuaXphdGlvbkFjY2Vzc1Jlc3BvbnNlEjoKE29yZ2FuaXphdGlvbl9hY2Nlc3MYASABKAsyHS50YWlsb3IudjEuT3JnYW5pemF0aW9uQWNjZXNzIokBCh9DcmVhdGVPcmdhbml6YXRpb25Gb2xkZXJSZXF1ZXN0EiEKD29yZ2FuaXphdGlvbl9pZBgBIAEoCUIIukgFcgOwAQESJQoQcGFyZW50X2ZvbGRlcl9pZBgCIAEoCUILukgI2AEBcgOwAQESHAoLZm9sZGVyX25hbWUYAyABKAlCB7pIBHICEAEiRQogQ3JlYXRlT3JnYW5pemF0aW9uRm9sZGVyUmVzcG9uc2USIQoGZm9sZGVyGAEgASgLMhEudGFpbG9yLnYxLkZvbGRlciKmAQofVXBkYXRlT3JnYW5pemF0aW9uRm9sZGVyUmVxdWVzdBIhCg9vcmdhbml6YXRpb25faWQYASABKAlCCLpIBXIDsAEBEhsKCWZvbGRlcl9pZBgCIAEoCUIIukgFcgOwAQESJQoQcGFyZW50X2ZvbGRlcl9pZBgDIAEoCUILukgI2AEBcgOwAQESHAoLZm9sZGVyX25hbWUYBCABKAlCB7pIBHICEAEiRQogVXBkYXRlT3JnYW5pemF0aW9uRm9sZGVyUmVzcG9uc2USIQoGZm9sZGVyGAEgASgLMhEudGFpbG9yLnYxLkZvbGRlciJhCh9EZWxldGVPcmdhbml6YXRpb25Gb2xkZXJSZXF1ZXN0EiEKD29yZ2FuaXphdGlvbl9pZBgBIAEoCUIIukgFcgOwAQESGwoJZm9sZGVyX2lkGAIgASgJQgi6SAVyA7ABASIiCiBEZWxldGVPcmdhbml6YXRpb25Gb2xkZXJSZXNwb25zZSJeChxHZXRPcmdhbml6YXRpb25Gb2xkZXJSZXF1ZXN0EiEKD29yZ2FuaXphdGlvbl9pZBgBIAEoCUIIukgFcgOwAQESGwoJZm9sZGVyX2lkGAIgASgJQgi6SAVyA7ABASJCCh1HZXRPcmdhbml6YXRpb25Gb2xkZXJSZXNwb25zZRIhCgZmb2xkZXIYASABKAsyES50YWlsb3IudjEuRm9sZGVyIsMBCh5MaXN0T3JnYW5pemF0aW9uRm9sZGVyc1JlcXVlc3QSEgoKcGFnZV90b2tlbhgBIAEoCRIRCglwYWdlX3NpemUYAiABKA0SMAoOcGFnZV9kaXJlY3Rpb24YAyABKA4yGC50YWlsb3IudjEuUGFnZURpcmVjdGlvbhIhCg9vcmdhbml6YXRpb25faWQYBCABKAlCCLpIBXIDsAEBEiUKEHBhcmVudF9mb2xkZXJfaWQYBSABKAlCC7pICNgBAXIDsAEBInMKH0xpc3RPcmdhbml6YXRpb25Gb2xkZXJzUmVzcG9uc2USFwoPbmV4dF9wYWdlX3Rva2VuGAEgASgJEhMKC3RvdGFsX2NvdW50GAIgASgDEiIKB2ZvbGRlcnMYAyADKAsyES50YWlsb3IudjEuRm9sZGVyIoQCCiRHcmFudE9yZ2FuaXphdGlvbkZvbGRlckFjY2Vzc1JlcXVlc3QSIQoPb3JnYW5pemF0aW9uX2lkGAEgASgJQgi6SAVyA7ABARIbCglmb2xkZXJfaWQYAiABKAlCCLpIBXIDsAEBEi8KBHJvbGUYAyABKA4yFS50YWlsb3IudjEuRm9sZGVyUm9sZUIKukgHggEEEAEgABIbCgd0ZWFtX2lkGAQgASgJQgi6SAVyA7ABAUgAEhgKBWVtYWlsGAUgASgJQge6SARyAmABSAASIwoPbWFjaGluZV91c2VyX2lkGAYgASgJQgi6SAVyA7ABAUgAQg8KBm1lbWJlchIFukgCCAEiJwolR3JhbnRPcmdhbml6YXRpb25Gb2xkZXJBY2Nlc3NSZXNwb25zZSKFAgolVXBkYXRlT3JnYW5pemF0aW9uRm9sZGVyQWNjZXNzUmVxdWVzdBIhCg9vcmdhbml6YXRpb25faWQYASABKAlCCLpIBXIDsAEBEhsKCWZvbGRlcl9pZBgCIAEoCUIIukgFcgOwAQESLwoEcm9sZRgDIAEoDjIVLnRhaWxvci52MS5Gb2xkZXJSb2xlQgq6SAeCAQQQASAAEhsKB3RlYW1faWQYBCABKAlCCLpIBXIDsAEBSAASGAoFZW1haWwYBSABKAlCB7pIBHICYAFIABIjCg9tYWNoaW5lX3VzZXJfaWQYBiABKAlCCLpIBXIDsAEBSABCDwoGbWVtYmVyEgW6SAIIASIoCiZVcGRhdGVPcmdhbml6YXRpb25Gb2xkZXJBY2Nlc3NSZXNwb25zZSLUAQolUmV2b2tlT3JnYW5pemF0aW9uRm9sZGVyQWNjZXNzUmVxdWVzdBIhCg9vcmdhbml6YXRpb25faWQYASABKAlCCLpIBXIDsAEBEhsKCWZvbGRlcl9pZBgCIAEoCUIIukgFcgOwAQESGwoHdGVhbV9pZBgDIAEoCUIIukgFcgOwAQFIABIYCgVlbWFpbBgEIAEoCUIHukgEcgJgAUgAEiMKD21hY2hpbmVfdXNlcl9pZBgFIAEoCUIIukgFcgOwAQFIAEIPCgZtZW1iZXISBbpIAggBIigKJlJldm9rZU9yZ2FuaXphdGlvbkZvbGRlckFjY2Vzc1Jlc3BvbnNlIsABCiVMaXN0T3JnYW5pemF0aW9uRm9sZGVyQWNjZXNzZXNSZXF1ZXN0EhIKCnBhZ2VfdG9rZW4YASABKAkSEQoJcGFnZV9zaXplGAIgASgNEjAKDnBhZ2VfZGlyZWN0aW9uGAMgASgOMhgudGFpbG9yLnYxLlBhZ2VEaXJlY3Rpb24SIQoPb3JnYW5pemF0aW9uX2lkGAQgASgJQgi6SAVyA7ABARIbCglmb2xkZXJfaWQYBSABKAlCCLpIBXIDsAEBIogBCiZMaXN0T3JnYW5pemF0aW9uRm9sZGVyQWNjZXNzZXNSZXNwb25zZRIwCg9mb2xkZXJfYWNjZXNzZXMYASADKAsyFy50YWlsb3IudjEuRm9sZGVyQWNjZXNzEhcKD25leHRfcGFnZV90b2tlbhgCIAEoCRITCgt0b3RhbF9jb3VudBgDIAEoAyLRAQoiR2V0T3JnYW5pemF0aW9uRm9sZGVyQWNjZXNzUmVxdWVzdBIhCg9vcmdhbml6YXRpb25faWQYASABKAlCCLpIBXIDsAEBEhsKCWZvbGRlcl9pZBgCIAEoCUIIukgFcgOwAQESGwoHdGVhbV9pZBgDIAEoCUIIukgFcgOwAQFIABIYCgVlbWFpbBgEIAEoCUIHukgEcgJgAUgAEiMKD21hY2hpbmVfdXNlcl9pZBgFIAEoCUIIukgFcgOwAQFIAEIPCgZtZW1iZXISBbpIAggBIlUKI0dldE9yZ2FuaXphdGlvbkZvbGRlckFjY2Vzc1Jlc3BvbnNlEi4KDWZvbGRlcl9hY2Nlc3MYASABKAsyFy50YWlsb3IudjEuRm9sZGVyQWNjZXNzIrIBChlPcmdhbml6YXRpb25JUFJlc3RyaWN0aW9uEhcKD29yZ2FuaXphdGlvbl9pZBgBIAEoCRIcChRhbGxvd2VkX2lwX2FkZHJlc3NlcxgCIAMoCRIuCgpjcmVhdGVkX2F0GAMgASgLMhouZ29vZ2xlLnByb3RvYnVmLlRpbWVzdGFtcBIuCgp1cGRhdGVkX2F0GAQgASgLMhouZ29vZ2xlLnByb3RvYnVmLlRpbWVzdGFtcCLLAQofT3JnYW5pemF0aW9uRm9sZGVySVBSZXN0cmljdGlvbhIXCg9vcmdhbml6YXRpb25faWQYASABKAkSEQoJZm9sZGVyX2lkGAIgASgJEhwKFGFsbG93ZWRfaXBfYWRkcmVzc2VzGAMgAygJEi4KCmNyZWF0ZWRfYXQYBCABKAsyGi5nb29nbGUucHJvdG9idWYuVGltZXN0YW1wEi4KCnVwZGF0ZWRfYXQYBSABKAsyGi5nb29nbGUucHJvdG9idWYuVGltZXN0YW1wInsKJlVwc2VydE9yZ2FuaXphdGlvbklQUmVzdHJpY3Rpb25SZXF1ZXN0EiEKD29yZ2FuaXphdGlvbl9pZBgBIAEoCUIIukgFcgOwAQESLgoUYWxsb3dlZF9pcF9hZGRyZXNzZXMYAiADKAlCELpIDZIBCggBGAEiBHICEAEidAonVXBzZXJ0T3JnYW5pemF0aW9uSVBSZXN0cmljdGlvblJlc3BvbnNlEkkKG29yZ2FuaXphdGlvbl9pcF9yZXN0cmljdGlvbhgBIAEoCzIkLnRhaWxvci52MS5Pcmdhbml6YXRpb25JUFJlc3RyaWN0aW9uIkgKI0dldE9yZ2FuaXphdGlvbklQUmVzdHJpY3Rpb25SZXF1ZXN0EiEKD29yZ2FuaXphdGlvbl9pZBgBIAEoCUIIukgFcgOwAQEicQokR2V0T3JnYW5pemF0aW9uSVBSZXN0cmljdGlvblJlc3BvbnNlEkkKG29yZ2FuaXphdGlvbl9pcF9yZXN0cmljdGlvbhgBIAEoCzIkLnRhaWxvci52MS5Pcmdhbml6YXRpb25JUFJlc3RyaWN0aW9uIksKJkRlbGV0ZU9yZ2FuaXphdGlvbklQUmVzdHJpY3Rpb25SZXF1ZXN0EiEKD29yZ2FuaXphdGlvbl9pZBgBIAEoCUIIukgFcgOwAQEiKQonRGVsZXRlT3JnYW5pemF0aW9uSVBSZXN0cmljdGlvblJlc3BvbnNlIp4BCixVcHNlcnRPcmdhbml6YXRpb25Gb2xkZXJJUFJlc3RyaWN0aW9uUmVxdWVzdBIhCg9vcmdhbml6YXRpb25faWQYASABKAlCCLpIBXIDsAEBEhsKCWZvbGRlcl9pZBgCIAEoCUIIukgFcgOwAQESLgoUYWxsb3dlZF9pcF9hZGRyZXNzZXMYAyADKAlCELpIDZIBCggBGAEiBHICEAEihwEKLVVwc2VydE9yZ2FuaXphdGlvbkZvbGRlcklQUmVzdHJpY3Rpb25SZXNwb25zZRJWCiJvcmdhbml6YXRpb25fZm9sZGVyX2lwX3Jlc3RyaWN0aW9uGAEgASgLMioudGFpbG9yLnYxLk9yZ2FuaXphdGlvbkZvbGRlcklQUmVzdHJpY3Rpb24iawopR2V0T3JnYW5pemF0aW9uRm9sZGVySVBSZXN0cmljdGlvblJlcXVlc3QSIQoPb3JnYW5pemF0aW9uX2lkGAEgASgJQgi6SAVyA7ABARIbCglmb2xkZXJfaWQYAiABKAlCCLpIBXIDsAEBIoQBCipHZXRPcmdhbml6YXRpb25Gb2xkZXJJUFJlc3RyaWN0aW9uUmVzcG9uc2USVgoib3JnYW5pemF0aW9uX2ZvbGRlcl9pcF9yZXN0cmljdGlvbhgBIAEoCzIqLnRhaWxvci52MS5Pcmdhbml6YXRpb25Gb2xkZXJJUFJlc3RyaWN0aW9uIm4KLERlbGV0ZU9yZ2FuaXphdGlvbkZvbGRlcklQUmVzdHJpY3Rpb25SZXF1ZXN0EiEKD29yZ2FuaXphdGlvbl9pZBgBIAEoCUIIukgFcgOwAQESGwoJZm9sZGVyX2lkGAIgASgJQgi6SAVyA7ABASIvCi1EZWxldGVPcmdhbml6YXRpb25Gb2xkZXJJUFJlc3RyaWN0aW9uUmVzcG9uc2UiXgodQ3JlYXRlT3JnYW5pemF0aW9uVGVhbVJlcXVlc3QSIQoPb3JnYW5pemF0aW9uX2lkGAEgASgJQgi6SAVyA7ABARIaCgl0ZWFtX25hbWUYAiABKAlCB7pIBHICEAEiPwoeQ3JlYXRlT3JnYW5pemF0aW9uVGVhbVJlc3BvbnNlEh0KBHRlYW0YASABKAsyDy50YWlsb3IudjEuVGVhbSJ5Ch1VcGRhdGVPcmdhbml6YXRpb25UZWFtUmVxdWVzdBIhCg9vcmdhbml6YXRpb25faWQYASABKAlCCLpIBXIDsAEBEhkKB3RlYW1faWQYAiABKAlCCLpIBXIDsAEBEhoKCXRlYW1fbmFtZRgDIAEoCUIHukgEcgIQASI/Ch5VcGRhdGVPcmdhbml6YXRpb25UZWFtUmVzcG9uc2USHQoEdGVhbRgBIAEoCzIPLnRhaWxvci52MS5UZWFtIl0KHURlbGV0ZU9yZ2FuaXphdGlvblRlYW1SZXF1ZXN0EiEKD29yZ2FuaXphdGlvbl9pZBgBIAEoCUIIukgFcgOwAQESGQoHdGVhbV9pZBgCIAEoCUIIukgFcgOwAQEiIAoeRGVsZXRlT3JnYW5pemF0aW9uVGVhbVJlc3BvbnNlIpoBChxMaXN0T3JnYW5pemF0aW9uVGVhbXNSZXF1ZXN0EhIKCnBhZ2VfdG9rZW4YASABKAkSEQoJcGFnZV9zaXplGAIgASgNEjAKDnBhZ2VfZGlyZWN0aW9uGAMgASgOMhgudGFpbG9yLnYxLlBhZ2VEaXJlY3Rpb24SIQoPb3JnYW5pemF0aW9uX2lkGAQgASgJQgi6SAVyA7ABASJtCh1MaXN0T3JnYW5pemF0aW9uVGVhbXNSZXNwb25zZRIeCgV0ZWFtcxgBIAMoCzIPLnRhaWxvci52MS5UZWFtEhcKD25leHRfcGFnZV90b2tlbhgCIAEoCRITCgt0b3RhbF9jb3VudBgDIAEoAyJaChpHZXRPcmdhbml6YXRpb25UZWFtUmVxdWVzdBIhCg9vcmdhbml6YXRpb25faWQYASABKAlCCLpIBXIDsAEBEhkKB3RlYW1faWQYAiABKAlCCLpIBXIDsAEBIjwKG0dldE9yZ2FuaXphdGlvblRlYW1SZXNwb25zZRIdCgR0ZWFtGAEgASgLMg8udGFpbG9yLnYxLlRlYW0ipwEKIEFkZE9yZ2FuaXphdGlvblRlYW1NZW1iZXJSZXF1ZXN0EiEKD29yZ2FuaXphdGlvbl9pZBgBIAEoCUIIukgFcgOwAQESGQoHdGVhbV9pZBgCIAEoCUIIukgFcgOwAQESFgoFZW1haWwYAyABKAlCB7pIBHICYAESLQoEcm9sZRgEIAEoDjITLnRhaWxvci52MS5UZWFtUm9sZUIKukgHggEEEAEgACIjCiFBZGRPcmdhbml6YXRpb25UZWFtTWVtYmVyUmVzcG9uc2UiqgEKI1VwZGF0ZU9yZ2FuaXphdGlvblRlYW1NZW1iZXJSZXF1ZXN0EiEKD29yZ2FuaXphdGlvbl9pZBgBIAEoCUIIukgFcgOwAQESGQoHdGVhbV9pZBgCIAEoCUIIukgFcgOwAQESFgoFZW1haWwYAyABKAlCB7pIBHICYAESLQoEcm9sZRgEIAEoDjITLnRhaWxvci52MS5UZWFtUm9sZUIKukgHggEEEAEgACImCiRVcGRhdGVPcmdhbml6YXRpb25UZWFtTWVtYmVyUmVzcG9uc2UiewojUmVtb3ZlT3JnYW5pemF0aW9uVGVhbU1lbWJlclJlcXVlc3QSIQoPb3JnYW5pemF0aW9uX2lkGAEgASgJQgi6SAVyA7ABARIZCgd0ZWFtX2lkGAIgASgJQgi6SAVyA7ABARIWCgVlbWFpbBgDIAEoCUIHukgEcgJgASImCiRSZW1vdmVPcmdhbml6YXRpb25UZWFtTWVtYmVyUmVzcG9uc2UiuwEKIkxpc3RPcmdhbml6YXRpb25UZWFtTWVtYmVyc1JlcXVlc3QSEgoKcGFnZV90b2tlbhgBIAEoCRIRCglwYWdlX3NpemUYAiABKA0SMAoOcGFnZV9kaXJlY3Rpb24YAyABKA4yGC50YWlsb3IudjEuUGFnZURpcmVjdGlvbhIhCg9vcmdhbml6YXRpb25faWQYBCABKAlCCLpIBXIDsAEBEhkKB3RlYW1faWQYBSABKAlCCLpIBXIDsAEBIoABCiNMaXN0T3JnYW5pemF0aW9uVGVhbU1lbWJlcnNSZXNwb25zZRIrCgx0ZWFtX21lbWJlcnMYASADKAsyFS50YWlsb3IudjEuVGVhbU1lbWJlchIXCg9uZXh0X3BhZ2VfdG9rZW4YAiABKAkSEwoLdG90YWxfY291bnQYAyABKAMieAogR2V0T3JnYW5pemF0aW9uVGVhbU1lbWJlclJlcXVlc3QSIQoPb3JnYW5pemF0aW9uX2lkGAEgASgJQgi6SAVyA7ABARIZCgd0ZWFtX2lkGAIgASgJQgi6SAVyA7ABARIWCgVlbWFpbBgDIAEoCUIHukgEcgJgASJPCiFHZXRPcmdhbml6YXRpb25UZWFtTWVtYmVyUmVzcG9uc2USKgoLdGVhbV9tZW1iZXIYASABKAsyFS50YWlsb3IudjEuVGVhbU1lbWJlciIfCh1HZXRQbGF0Zm9ybUFjY291bnRQbGFuUmVxdWVzdCJWCh5HZXRQbGF0Zm9ybUFjY291bnRQbGFuUmVzcG9uc2USNAoMY3VycmVudF9wbGFuGAEgASgLMh4udGFpbG9yLnYxLlBsYXRmb3JtQWNjb3VudFBsYW5iBnByb3RvMw", [file_buf_validate_validate, file_google_protobuf_field_mask, file_google_protobuf_timestamp, file_tailor_fieldmask_v1_option, file_tailor_v1_resource, file_tailor_v1_workspace_resource]); /** * Describes the message tailor.v1.ListAvailableWorkspaceRegionsRequest. @@ -510,157 +510,255 @@ export const GetOrganizationFolderAccessRequestSchema = /*@__PURE__*/ export const GetOrganizationFolderAccessResponseSchema = /*@__PURE__*/ messageDesc(file_tailor_v1_workspace, 67); +/** + * Describes the message tailor.v1.OrganizationIPRestriction. + * Use `create(OrganizationIPRestrictionSchema)` to create a new message. + */ +export const OrganizationIPRestrictionSchema = /*@__PURE__*/ + messageDesc(file_tailor_v1_workspace, 68); + +/** + * Describes the message tailor.v1.OrganizationFolderIPRestriction. + * Use `create(OrganizationFolderIPRestrictionSchema)` to create a new message. + */ +export const OrganizationFolderIPRestrictionSchema = /*@__PURE__*/ + messageDesc(file_tailor_v1_workspace, 69); + +/** + * Describes the message tailor.v1.UpsertOrganizationIPRestrictionRequest. + * Use `create(UpsertOrganizationIPRestrictionRequestSchema)` to create a new message. + */ +export const UpsertOrganizationIPRestrictionRequestSchema = /*@__PURE__*/ + messageDesc(file_tailor_v1_workspace, 70); + +/** + * Describes the message tailor.v1.UpsertOrganizationIPRestrictionResponse. + * Use `create(UpsertOrganizationIPRestrictionResponseSchema)` to create a new message. + */ +export const UpsertOrganizationIPRestrictionResponseSchema = /*@__PURE__*/ + messageDesc(file_tailor_v1_workspace, 71); + +/** + * Describes the message tailor.v1.GetOrganizationIPRestrictionRequest. + * Use `create(GetOrganizationIPRestrictionRequestSchema)` to create a new message. + */ +export const GetOrganizationIPRestrictionRequestSchema = /*@__PURE__*/ + messageDesc(file_tailor_v1_workspace, 72); + +/** + * Describes the message tailor.v1.GetOrganizationIPRestrictionResponse. + * Use `create(GetOrganizationIPRestrictionResponseSchema)` to create a new message. + */ +export const GetOrganizationIPRestrictionResponseSchema = /*@__PURE__*/ + messageDesc(file_tailor_v1_workspace, 73); + +/** + * Describes the message tailor.v1.DeleteOrganizationIPRestrictionRequest. + * Use `create(DeleteOrganizationIPRestrictionRequestSchema)` to create a new message. + */ +export const DeleteOrganizationIPRestrictionRequestSchema = /*@__PURE__*/ + messageDesc(file_tailor_v1_workspace, 74); + +/** + * Describes the message tailor.v1.DeleteOrganizationIPRestrictionResponse. + * Use `create(DeleteOrganizationIPRestrictionResponseSchema)` to create a new message. + */ +export const DeleteOrganizationIPRestrictionResponseSchema = /*@__PURE__*/ + messageDesc(file_tailor_v1_workspace, 75); + +/** + * Describes the message tailor.v1.UpsertOrganizationFolderIPRestrictionRequest. + * Use `create(UpsertOrganizationFolderIPRestrictionRequestSchema)` to create a new message. + */ +export const UpsertOrganizationFolderIPRestrictionRequestSchema = /*@__PURE__*/ + messageDesc(file_tailor_v1_workspace, 76); + +/** + * Describes the message tailor.v1.UpsertOrganizationFolderIPRestrictionResponse. + * Use `create(UpsertOrganizationFolderIPRestrictionResponseSchema)` to create a new message. + */ +export const UpsertOrganizationFolderIPRestrictionResponseSchema = /*@__PURE__*/ + messageDesc(file_tailor_v1_workspace, 77); + +/** + * Describes the message tailor.v1.GetOrganizationFolderIPRestrictionRequest. + * Use `create(GetOrganizationFolderIPRestrictionRequestSchema)` to create a new message. + */ +export const GetOrganizationFolderIPRestrictionRequestSchema = /*@__PURE__*/ + messageDesc(file_tailor_v1_workspace, 78); + +/** + * Describes the message tailor.v1.GetOrganizationFolderIPRestrictionResponse. + * Use `create(GetOrganizationFolderIPRestrictionResponseSchema)` to create a new message. + */ +export const GetOrganizationFolderIPRestrictionResponseSchema = /*@__PURE__*/ + messageDesc(file_tailor_v1_workspace, 79); + +/** + * Describes the message tailor.v1.DeleteOrganizationFolderIPRestrictionRequest. + * Use `create(DeleteOrganizationFolderIPRestrictionRequestSchema)` to create a new message. + */ +export const DeleteOrganizationFolderIPRestrictionRequestSchema = /*@__PURE__*/ + messageDesc(file_tailor_v1_workspace, 80); + +/** + * Describes the message tailor.v1.DeleteOrganizationFolderIPRestrictionResponse. + * Use `create(DeleteOrganizationFolderIPRestrictionResponseSchema)` to create a new message. + */ +export const DeleteOrganizationFolderIPRestrictionResponseSchema = /*@__PURE__*/ + messageDesc(file_tailor_v1_workspace, 81); + /** * Describes the message tailor.v1.CreateOrganizationTeamRequest. * Use `create(CreateOrganizationTeamRequestSchema)` to create a new message. */ export const CreateOrganizationTeamRequestSchema = /*@__PURE__*/ - messageDesc(file_tailor_v1_workspace, 68); + messageDesc(file_tailor_v1_workspace, 82); /** * Describes the message tailor.v1.CreateOrganizationTeamResponse. * Use `create(CreateOrganizationTeamResponseSchema)` to create a new message. */ export const CreateOrganizationTeamResponseSchema = /*@__PURE__*/ - messageDesc(file_tailor_v1_workspace, 69); + messageDesc(file_tailor_v1_workspace, 83); /** * Describes the message tailor.v1.UpdateOrganizationTeamRequest. * Use `create(UpdateOrganizationTeamRequestSchema)` to create a new message. */ export const UpdateOrganizationTeamRequestSchema = /*@__PURE__*/ - messageDesc(file_tailor_v1_workspace, 70); + messageDesc(file_tailor_v1_workspace, 84); /** * Describes the message tailor.v1.UpdateOrganizationTeamResponse. * Use `create(UpdateOrganizationTeamResponseSchema)` to create a new message. */ export const UpdateOrganizationTeamResponseSchema = /*@__PURE__*/ - messageDesc(file_tailor_v1_workspace, 71); + messageDesc(file_tailor_v1_workspace, 85); /** * Describes the message tailor.v1.DeleteOrganizationTeamRequest. * Use `create(DeleteOrganizationTeamRequestSchema)` to create a new message. */ export const DeleteOrganizationTeamRequestSchema = /*@__PURE__*/ - messageDesc(file_tailor_v1_workspace, 72); + messageDesc(file_tailor_v1_workspace, 86); /** * Describes the message tailor.v1.DeleteOrganizationTeamResponse. * Use `create(DeleteOrganizationTeamResponseSchema)` to create a new message. */ export const DeleteOrganizationTeamResponseSchema = /*@__PURE__*/ - messageDesc(file_tailor_v1_workspace, 73); + messageDesc(file_tailor_v1_workspace, 87); /** * Describes the message tailor.v1.ListOrganizationTeamsRequest. * Use `create(ListOrganizationTeamsRequestSchema)` to create a new message. */ export const ListOrganizationTeamsRequestSchema = /*@__PURE__*/ - messageDesc(file_tailor_v1_workspace, 74); + messageDesc(file_tailor_v1_workspace, 88); /** * Describes the message tailor.v1.ListOrganizationTeamsResponse. * Use `create(ListOrganizationTeamsResponseSchema)` to create a new message. */ export const ListOrganizationTeamsResponseSchema = /*@__PURE__*/ - messageDesc(file_tailor_v1_workspace, 75); + messageDesc(file_tailor_v1_workspace, 89); /** * Describes the message tailor.v1.GetOrganizationTeamRequest. * Use `create(GetOrganizationTeamRequestSchema)` to create a new message. */ export const GetOrganizationTeamRequestSchema = /*@__PURE__*/ - messageDesc(file_tailor_v1_workspace, 76); + messageDesc(file_tailor_v1_workspace, 90); /** * Describes the message tailor.v1.GetOrganizationTeamResponse. * Use `create(GetOrganizationTeamResponseSchema)` to create a new message. */ export const GetOrganizationTeamResponseSchema = /*@__PURE__*/ - messageDesc(file_tailor_v1_workspace, 77); + messageDesc(file_tailor_v1_workspace, 91); /** * Describes the message tailor.v1.AddOrganizationTeamMemberRequest. * Use `create(AddOrganizationTeamMemberRequestSchema)` to create a new message. */ export const AddOrganizationTeamMemberRequestSchema = /*@__PURE__*/ - messageDesc(file_tailor_v1_workspace, 78); + messageDesc(file_tailor_v1_workspace, 92); /** * Describes the message tailor.v1.AddOrganizationTeamMemberResponse. * Use `create(AddOrganizationTeamMemberResponseSchema)` to create a new message. */ export const AddOrganizationTeamMemberResponseSchema = /*@__PURE__*/ - messageDesc(file_tailor_v1_workspace, 79); + messageDesc(file_tailor_v1_workspace, 93); /** * Describes the message tailor.v1.UpdateOrganizationTeamMemberRequest. * Use `create(UpdateOrganizationTeamMemberRequestSchema)` to create a new message. */ export const UpdateOrganizationTeamMemberRequestSchema = /*@__PURE__*/ - messageDesc(file_tailor_v1_workspace, 80); + messageDesc(file_tailor_v1_workspace, 94); /** * Describes the message tailor.v1.UpdateOrganizationTeamMemberResponse. * Use `create(UpdateOrganizationTeamMemberResponseSchema)` to create a new message. */ export const UpdateOrganizationTeamMemberResponseSchema = /*@__PURE__*/ - messageDesc(file_tailor_v1_workspace, 81); + messageDesc(file_tailor_v1_workspace, 95); /** * Describes the message tailor.v1.RemoveOrganizationTeamMemberRequest. * Use `create(RemoveOrganizationTeamMemberRequestSchema)` to create a new message. */ export const RemoveOrganizationTeamMemberRequestSchema = /*@__PURE__*/ - messageDesc(file_tailor_v1_workspace, 82); + messageDesc(file_tailor_v1_workspace, 96); /** * Describes the message tailor.v1.RemoveOrganizationTeamMemberResponse. * Use `create(RemoveOrganizationTeamMemberResponseSchema)` to create a new message. */ export const RemoveOrganizationTeamMemberResponseSchema = /*@__PURE__*/ - messageDesc(file_tailor_v1_workspace, 83); + messageDesc(file_tailor_v1_workspace, 97); /** * Describes the message tailor.v1.ListOrganizationTeamMembersRequest. * Use `create(ListOrganizationTeamMembersRequestSchema)` to create a new message. */ export const ListOrganizationTeamMembersRequestSchema = /*@__PURE__*/ - messageDesc(file_tailor_v1_workspace, 84); + messageDesc(file_tailor_v1_workspace, 98); /** * Describes the message tailor.v1.ListOrganizationTeamMembersResponse. * Use `create(ListOrganizationTeamMembersResponseSchema)` to create a new message. */ export const ListOrganizationTeamMembersResponseSchema = /*@__PURE__*/ - messageDesc(file_tailor_v1_workspace, 85); + messageDesc(file_tailor_v1_workspace, 99); /** * Describes the message tailor.v1.GetOrganizationTeamMemberRequest. * Use `create(GetOrganizationTeamMemberRequestSchema)` to create a new message. */ export const GetOrganizationTeamMemberRequestSchema = /*@__PURE__*/ - messageDesc(file_tailor_v1_workspace, 86); + messageDesc(file_tailor_v1_workspace, 100); /** * Describes the message tailor.v1.GetOrganizationTeamMemberResponse. * Use `create(GetOrganizationTeamMemberResponseSchema)` to create a new message. */ export const GetOrganizationTeamMemberResponseSchema = /*@__PURE__*/ - messageDesc(file_tailor_v1_workspace, 87); + messageDesc(file_tailor_v1_workspace, 101); /** * Describes the message tailor.v1.GetPlatformAccountPlanRequest. * Use `create(GetPlatformAccountPlanRequestSchema)` to create a new message. */ export const GetPlatformAccountPlanRequestSchema = /*@__PURE__*/ - messageDesc(file_tailor_v1_workspace, 88); + messageDesc(file_tailor_v1_workspace, 102); /** * Describes the message tailor.v1.GetPlatformAccountPlanResponse. * Use `create(GetPlatformAccountPlanResponseSchema)` to create a new message. */ export const GetPlatformAccountPlanResponseSchema = /*@__PURE__*/ - messageDesc(file_tailor_v1_workspace, 89); + messageDesc(file_tailor_v1_workspace, 103);