@databricks/appkit 0.67.0 → 0.68.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (68) hide show
  1. package/CLAUDE.md +2 -2
  2. package/dist/appkit/package.js +1 -1
  3. package/dist/database/contract/index.js +1 -1
  4. package/dist/database/contract/wire.d.ts.map +1 -1
  5. package/dist/database/contract/wire.js +5 -1
  6. package/dist/database/contract/wire.js.map +1 -1
  7. package/dist/database/errors.js +39 -3
  8. package/dist/database/errors.js.map +1 -1
  9. package/dist/database/runtime/data-path.js.map +1 -1
  10. package/dist/database/runtime/engine/drizzle-data-path.js +1 -1
  11. package/dist/database/runtime/engine/drizzle-data-path.js.map +1 -1
  12. package/dist/database/runtime/engine/translate.js +9 -2
  13. package/dist/database/runtime/engine/translate.js.map +1 -1
  14. package/dist/database/schema-builder/define-schema.d.ts +5 -1
  15. package/dist/database/schema-builder/define-schema.d.ts.map +1 -1
  16. package/dist/database/schema-builder/define-schema.js +4 -0
  17. package/dist/database/schema-builder/define-schema.js.map +1 -1
  18. package/dist/database/schema-builder/types.d.ts +7 -2
  19. package/dist/database/schema-builder/types.d.ts.map +1 -1
  20. package/dist/database/schema-builder/types.js.map +1 -1
  21. package/dist/plugins/database/crud/codecs.js +78 -0
  22. package/dist/plugins/database/crud/codecs.js.map +1 -0
  23. package/dist/plugins/database/crud/contract.js +151 -0
  24. package/dist/plugins/database/crud/contract.js.map +1 -0
  25. package/dist/plugins/database/crud/exposure.js +40 -0
  26. package/dist/plugins/database/crud/exposure.js.map +1 -0
  27. package/dist/plugins/database/crud/query.js +244 -0
  28. package/dist/plugins/database/crud/query.js.map +1 -0
  29. package/dist/plugins/database/crud/routes.js +124 -0
  30. package/dist/plugins/database/crud/routes.js.map +1 -0
  31. package/dist/plugins/database/database.d.ts +6 -0
  32. package/dist/plugins/database/database.d.ts.map +1 -1
  33. package/dist/plugins/database/database.js +58 -4
  34. package/dist/plugins/database/database.js.map +1 -1
  35. package/dist/plugins/database/defaults.js +28 -1
  36. package/dist/plugins/database/defaults.js.map +1 -1
  37. package/dist/plugins/database/entity-client.js +1 -1
  38. package/dist/plugins/database/entity-types.d.ts +13 -4
  39. package/dist/plugins/database/entity-types.d.ts.map +1 -1
  40. package/dist/plugins/database/lifecycle.js +2 -1
  41. package/dist/plugins/database/lifecycle.js.map +1 -1
  42. package/dist/plugins/database/types.d.ts +32 -0
  43. package/dist/plugins/database/types.d.ts.map +1 -1
  44. package/dist/plugins/server/index.js +2 -2
  45. package/dist/plugins/server/index.js.map +1 -1
  46. package/dist/plugins/server/remote-tunnel/remote-tunnel-manager.js +3 -3
  47. package/dist/plugins/server/remote-tunnel/remote-tunnel-manager.js.map +1 -1
  48. package/dist/plugins/server/static-server.js +3 -3
  49. package/dist/plugins/server/static-server.js.map +1 -1
  50. package/dist/plugins/server/utils.js +3 -3
  51. package/dist/plugins/server/utils.js.map +1 -1
  52. package/dist/plugins/server/vite-dev-server.js +4 -4
  53. package/dist/plugins/server/vite-dev-server.js.map +1 -1
  54. package/dist/shared/src/schemas/manifest.d.ts +33 -33
  55. package/dist/type-generator/database/generate.js +3 -3
  56. package/dist/type-generator/database/generate.js.map +1 -1
  57. package/dist/type-generator/migration.js +2 -2
  58. package/dist/type-generator/migration.js.map +1 -1
  59. package/dist/type-generator/serving/server-file-extractor.js +3 -3
  60. package/dist/type-generator/serving/server-file-extractor.js.map +1 -1
  61. package/docs/api/appkit/Function.database.md +3 -3
  62. package/docs/api/appkit/Function.defineSchema.md +14 -6
  63. package/docs/api/appkit/Interface.Schema.md +10 -2
  64. package/docs/api/appkit/TypeAlias.IDatabaseConfig.md +20 -0
  65. package/docs/api/appkit.md +2 -2
  66. package/llms.txt +2 -2
  67. package/package.json +1 -1
  68. package/sbom.cdx.json +1 -1
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.ts","names":[],"sources":["../../../src/database/schema-builder/types.ts"],"mappings":";cAEc,YAAA;AAAA,cACA,aAAA;;KAEF,WAAA;EAAA,UAA0B,YAAA;AAAA;AAAA,KAC1B,YAAA;EAAA,UAA2B,aAAA;AAAA;;KAG3B,eAAA;;KAiCA,iBAAA;AApCZ;AAAA,KA4CY,cAAA;EAAA,SACG,IAAA;AAAA;EAAA,SACA,IAAA;AAAA;EAAA,SACA,IAAA;AAAA;EAAA,SACA,IAAA;EAAA,SAA0B,MAAA;AAAA;EAAA,SAC1B,IAAA;AAAA;EAAA,SACA,IAAA;AAAA;EAAA,SACA,IAAA;AAAA;EAAA,SACA,IAAA;AAAA;EAAA,SACA,IAAA;EAAA,SAA4B,YAAA;AAAA;EAAA,SAC5B,IAAA;AAAA;EAAA,SAEA,IAAA;EAAA,SACA,QAAA;EAAA,SACA,MAAA;AAAA;EAAA,SAEA,IAAA;AAAA;;KAGH,WAAA;AAAA,UAaK,SAAA;EAAA,SACN,aAAA;EAAA,SACA,SAAA;EAAA,SACA,UAAA;AAAA;AAAA,KAGC,KAAA,GAAQ,SAAA,UAAmB,SAAA;AAAA,UAEtB,kBAAA;EAAA,SACN,WAAA;EAAA,SACA,YAAA;EAAA,SACA,QAAA,GAAW,iBAAA;EAAA,SACX,QAAA,GAAW,iBAAA;AAAA;;UAIL,iBAAA;EACf,IAAA;EACA,UAAA;EACA,IAAA,EAAM,eAAA;EACN,WAAA,EAAa,WAAA;EACb,OAAA;EACA,UAAA;EACA,MAAA;EACA,SAAA;EACA,eAAA;EACA,UAAA;EACA,YAAA;EACA,UAAA;EACA,aAAA;EACA,YAAA;EACA,aAAA;EACA,QAAA;EACA,UAAA;EACA,KAAA,GAAQ,KAAA;EACR,QAAA,GAAW,iBAAA;EACX,QAAA,GAAW,iBAAA;EACX,EAAA,GAAK,kBAAA;EACL,YAAA,GAAe,YAAA;AAAA;;KAIL,UAAA,GAAa,QAAA,CACvB,IAAA,CAAK,iBAAA;EAAA,SACM,UAAA;EAAA,SACA,EAAA,GAAK,QAAA,CAAS,kBAAA;EAAA,SACd,YAAA,EAAc,YAAA;AAAA;AAAA,UAIV,gBAAA;EAAA,SACN,IAAA;EAAA,SACA,WAAA;EAAA,SACA,WAAA;EAAA,SACA,WAAA;EAAA,SACA,YAAA;EAAA,SACA,QAAA;AAAA;AAAA,UAGM,WAAA;EAAA,SACN,KAAA;EAAA,SACA,WAAA;EAAA,SACA,QAAA,EAAU,QAAA,CAAS,MAAA,SAAe,UAAA;EAAA,SAClC,OAAA,EAAS,WAAA;EAAA,SACT,UAAA,WAAqB,gBAAA;EAxC9B;EAAA,SA0CS,aAAA;EAxCT;EAAA,SA0CS,aAAA;AAAA;;KAIC,WAAA,WAAsB,MAAA,qBAA2B,WAAA,0BACtC,CAAA,GAAI,SAAA;AAAA,UAGV,mBAAA;EAAA,SACN,UAAA;AAAA;AAAA,UAGM,MAAA;EAAA,SACN,WAAA;EAAA,SACA,OAAA,EAAS,QAAA,CAAS,MAAA,SAAe,WAAA;EAAA,SACjC,OAAA,EAAS,QAAA,CAAS,MAAA,SAAe,WAAA;AAAA"}
1
+ {"version":3,"file":"types.d.ts","names":[],"sources":["../../../src/database/schema-builder/types.ts"],"mappings":";cAEc,YAAA;AAAA,cACA,aAAA;;KAEF,WAAA;EAAA,UAA0B,YAAA;AAAA;AAAA,KAC1B,YAAA;EAAA,UAA2B,aAAA;AAAA;;KAG3B,eAAA;;KAiCA,iBAAA;AApCZ;AAAA,KA4CY,cAAA;EAAA,SACG,IAAA;AAAA;EAAA,SACA,IAAA;AAAA;EAAA,SACA,IAAA;AAAA;EAAA,SACA,IAAA;EAAA,SAA0B,MAAA;AAAA;EAAA,SAC1B,IAAA;AAAA;EAAA,SACA,IAAA;AAAA;EAAA,SACA,IAAA;AAAA;EAAA,SACA,IAAA;AAAA;EAAA,SACA,IAAA;EAAA,SAA4B,YAAA;AAAA;EAAA,SAC5B,IAAA;AAAA;EAAA,SAEA,IAAA;EAAA,SACA,QAAA;EAAA,SACA,MAAA;AAAA;EAAA,SAEA,IAAA;AAAA;;KAGH,WAAA;AAAA,UAaK,SAAA;EAAA,SACN,aAAA;EAAA,SACA,SAAA;EAAA,SACA,UAAA;AAAA;AAAA,KAGC,KAAA,GAAQ,SAAA,UAAmB,SAAA;AAAA,UAEtB,kBAAA;EAAA,SACN,WAAA;EAAA,SACA,YAAA;EAAA,SACA,QAAA,GAAW,iBAAA;EAAA,SACX,QAAA,GAAW,iBAAA;AAAA;;UAIL,iBAAA;EACf,IAAA;EACA,UAAA;EACA,IAAA,EAAM,eAAA;EACN,WAAA,EAAa,WAAA;EACb,OAAA;EACA,UAAA;EACA,MAAA;EACA,SAAA;EACA,eAAA;EACA,UAAA;EACA,YAAA;EACA,UAAA;EACA,aAAA;EACA,YAAA;EACA,aAAA;EACA,QAAA;EACA,UAAA;EACA,KAAA,GAAQ,KAAA;EACR,QAAA,GAAW,iBAAA;EACX,QAAA,GAAW,iBAAA;EACX,EAAA,GAAK,kBAAA;EACL,YAAA,GAAe,YAAA;AAAA;;KAIL,UAAA,GAAa,QAAA,CACvB,IAAA,CAAK,iBAAA;EAAA,SACM,UAAA;EAAA,SACA,EAAA,GAAK,QAAA,CAAS,kBAAA;EAAA,SACd,YAAA,EAAc,YAAA;AAAA;AAAA,UAIV,gBAAA;EAAA,SACN,IAAA;EAAA,SACA,WAAA;EAAA,SACA,WAAA;EAAA,SACA,WAAA;EAAA,SACA,YAAA;EAAA,SACA,QAAA;AAAA;AAAA,UAGM,WAAA;EAAA,SACN,KAAA;EAAA,SACA,WAAA;EAAA,SACA,QAAA,EAAU,QAAA,CAAS,MAAA,SAAe,UAAA;EAAA,SAClC,OAAA,EAAS,WAAA;EAAA,SACT,UAAA,WAAqB,gBAAA;EAxC9B;EAAA,SA0CS,aAAA;EAxCT;EAAA,SA0CS,aAAA;AAAA;;KAIC,WAAA,WAAsB,MAAA,qBAA2B,WAAA,0BACtC,CAAA,GAAI,SAAA;AAAA,UAGV,mBAAA;EAAA,SACN,UAAA;AAAA;;;;;;UAQM,MAAA;EAAA,SACN,WAAA;EAAA,SACA,OAAA,EAAS,QAAA,CAAS,MAAA,CAAO,UAAA,EAAY,WAAA;EAAA,SACrC,OAAA,EAAS,QAAA,CAAS,MAAA,SAAe,WAAA;AAAA"}
@@ -1 +1 @@
1
- {"version":3,"file":"types.js","names":[],"sources":["../../../src/database/schema-builder/types.ts"],"sourcesContent":["import type { FilterOperator } from \"../contract\";\n\ndeclare const ENGINE_TABLE: unique symbol;\ndeclare const ENGINE_COLUMN: unique symbol;\n/** Opaque handles keep Drizzle types behind the schema/runtime boundary. */\nexport type EngineTable = { readonly [ENGINE_TABLE]: true };\nexport type EngineColumn = { readonly [ENGINE_COLUMN]: true };\n\n/** JavaScript value category exposed by a column, independent of its storage. */\nexport type ColumnValueKind =\n | \"string\"\n | \"number\"\n | \"bigint\"\n | \"boolean\"\n | \"date\"\n | \"json\"\n | \"uuid\"\n | \"enum\"\n | \"unknown\";\n\n/** The filter subset shared by runtime translation and later typed surfaces. */\nexport function filterOperatorsForKind(\n kind: ColumnValueKind,\n): readonly FilterOperator[] {\n switch (kind) {\n case \"string\":\n return [\"eq\", \"neq\", \"in\", \"like\", \"ilike\"];\n case \"number\":\n case \"bigint\":\n case \"date\":\n return [\"eq\", \"neq\", \"in\", \"gt\", \"gte\", \"lt\", \"lte\"];\n case \"enum\":\n case \"boolean\":\n case \"uuid\":\n return [\"eq\", \"neq\", \"in\"];\n case \"json\":\n case \"unknown\":\n return [];\n }\n}\n\n/** PostgreSQL action applied when a referenced row changes or is deleted. */\nexport type ReferentialAction =\n | \"cascade\"\n | \"set null\"\n | \"set default\"\n | \"restrict\"\n | \"no action\";\n\n/** DSL declaration kind, including identity shorthand and unresolved FKs. */\nexport type ColumnTypeSpec =\n | { readonly kind: \"id\" }\n | { readonly kind: \"bigid\" }\n | { readonly kind: \"text\" }\n | { readonly kind: \"varchar\"; readonly length: number }\n | { readonly kind: \"integer\" }\n | { readonly kind: \"bigint\" }\n | { readonly kind: \"boolean\" }\n | { readonly kind: \"uuid\" }\n | { readonly kind: \"timestamp\"; readonly withTimezone: boolean }\n | { readonly kind: \"jsonb\" }\n | {\n readonly kind: \"enum\";\n readonly enumName: string;\n readonly values: readonly string[];\n }\n | { readonly kind: \"fk\" };\n\n/** Resolved PostgreSQL storage used to construct the engine column. */\nexport type StorageKind =\n | \"id\"\n | \"bigid\"\n | \"text\"\n | \"varchar\"\n | \"integer\"\n | \"bigint\"\n | \"boolean\"\n | \"uuid\"\n | \"timestamp\"\n | \"jsonb\"\n | \"enum\";\n\nexport interface ColumnRef {\n readonly __isColumnRef: true;\n readonly tableName: string;\n readonly columnName: string;\n}\n\nexport type FkRef = ColumnRef | (() => ColumnRef);\n\nexport interface ResolvedForeignKey {\n readonly targetTable: string;\n readonly targetColumn: string;\n readonly onDelete?: ReferentialAction;\n readonly onUpdate?: ReferentialAction;\n}\n\n/** Mutable declaration state. It is cloned per table and never published. */\nexport interface MutableColumnMeta {\n name: string;\n columnName: string;\n kind: ColumnValueKind;\n storageKind: StorageKind;\n notNull: boolean;\n primaryKey: boolean;\n unique: boolean;\n isPrivate: boolean;\n serverGenerated: boolean;\n hasDefault: boolean;\n defaultValue?: string | number | boolean;\n defaultNow?: boolean;\n defaultRandom?: boolean;\n withTimezone?: boolean;\n varcharLength?: number;\n enumName?: string;\n enumValues?: readonly string[];\n fkRef?: FkRef;\n onDelete?: ReferentialAction;\n onUpdate?: ReferentialAction;\n fk?: ResolvedForeignKey;\n engineColumn?: EngineColumn;\n}\n\n/** Immutable column metadata published by a finalized schema. */\nexport type ColumnMeta = Readonly<\n Omit<MutableColumnMeta, \"enumValues\" | \"fk\" | \"fkRef\" | \"engineColumn\"> & {\n readonly enumValues?: readonly string[];\n readonly fk?: Readonly<ResolvedForeignKey>;\n readonly engineColumn: EngineColumn;\n }\n>;\n\nexport interface ResolvedRelation {\n readonly name: string;\n readonly cardinality: \"toOne\" | \"toMany\";\n readonly localColumn: string;\n readonly targetTable: string;\n readonly targetColumn: string;\n readonly inferred: boolean;\n}\n\nexport interface AppKitTable {\n readonly $name: string;\n readonly $schemaName: string;\n readonly $columns: Readonly<Record<string, ColumnMeta>>;\n readonly $engine: EngineTable;\n readonly $relations: readonly ResolvedRelation[];\n /** @internal insert schema retained from the current-main foundation. */\n readonly $insertSchema: unknown;\n /** @internal update schema retained from the current-main foundation. */\n readonly $updateSchema: unknown;\n}\n\n/** A declaration handle gains finalized table metadata only at publication. */\nexport type TableHandle<C extends Record<string, unknown>> = AppKitTable & {\n readonly [K in keyof C]: ColumnRef;\n};\n\nexport interface DefineSchemaOptions {\n readonly schemaName?: string;\n}\n\nexport interface Schema {\n readonly $schemaName: string;\n readonly $tables: Readonly<Record<string, AppKitTable>>;\n readonly $engine: Readonly<Record<string, EngineTable>>;\n}\n\nexport class SchemaBuildError extends Error {\n constructor(message: string) {\n super(message);\n this.name = \"SchemaBuildError\";\n }\n}\n"],"mappings":";;AAqBA,SAAgB,uBACd,MAC2B;AAC3B,SAAQ,MAAR;EACE,KAAK,SACH,QAAO;GAAC;GAAM;GAAO;GAAM;GAAQ;GAAQ;EAC7C,KAAK;EACL,KAAK;EACL,KAAK,OACH,QAAO;GAAC;GAAM;GAAO;GAAM;GAAM;GAAO;GAAM;GAAM;EACtD,KAAK;EACL,KAAK;EACL,KAAK,OACH,QAAO;GAAC;GAAM;GAAO;GAAK;EAC5B,KAAK;EACL,KAAK,UACH,QAAO,EAAE;;;AAmIf,IAAa,mBAAb,cAAsC,MAAM;CAC1C,YAAY,SAAiB;AAC3B,QAAM,QAAQ;AACd,OAAK,OAAO"}
1
+ {"version":3,"file":"types.js","names":[],"sources":["../../../src/database/schema-builder/types.ts"],"sourcesContent":["import type { FilterOperator } from \"../contract\";\n\ndeclare const ENGINE_TABLE: unique symbol;\ndeclare const ENGINE_COLUMN: unique symbol;\n/** Opaque handles keep Drizzle types behind the schema/runtime boundary. */\nexport type EngineTable = { readonly [ENGINE_TABLE]: true };\nexport type EngineColumn = { readonly [ENGINE_COLUMN]: true };\n\n/** JavaScript value category exposed by a column, independent of its storage. */\nexport type ColumnValueKind =\n | \"string\"\n | \"number\"\n | \"bigint\"\n | \"boolean\"\n | \"date\"\n | \"json\"\n | \"uuid\"\n | \"enum\"\n | \"unknown\";\n\n/** The filter subset shared by runtime translation and later typed surfaces. */\nexport function filterOperatorsForKind(\n kind: ColumnValueKind,\n): readonly FilterOperator[] {\n switch (kind) {\n case \"string\":\n return [\"eq\", \"neq\", \"in\", \"like\", \"ilike\"];\n case \"number\":\n case \"bigint\":\n case \"date\":\n return [\"eq\", \"neq\", \"in\", \"gt\", \"gte\", \"lt\", \"lte\"];\n case \"enum\":\n case \"boolean\":\n case \"uuid\":\n return [\"eq\", \"neq\", \"in\"];\n case \"json\":\n case \"unknown\":\n return [];\n }\n}\n\n/** PostgreSQL action applied when a referenced row changes or is deleted. */\nexport type ReferentialAction =\n | \"cascade\"\n | \"set null\"\n | \"set default\"\n | \"restrict\"\n | \"no action\";\n\n/** DSL declaration kind, including identity shorthand and unresolved FKs. */\nexport type ColumnTypeSpec =\n | { readonly kind: \"id\" }\n | { readonly kind: \"bigid\" }\n | { readonly kind: \"text\" }\n | { readonly kind: \"varchar\"; readonly length: number }\n | { readonly kind: \"integer\" }\n | { readonly kind: \"bigint\" }\n | { readonly kind: \"boolean\" }\n | { readonly kind: \"uuid\" }\n | { readonly kind: \"timestamp\"; readonly withTimezone: boolean }\n | { readonly kind: \"jsonb\" }\n | {\n readonly kind: \"enum\";\n readonly enumName: string;\n readonly values: readonly string[];\n }\n | { readonly kind: \"fk\" };\n\n/** Resolved PostgreSQL storage used to construct the engine column. */\nexport type StorageKind =\n | \"id\"\n | \"bigid\"\n | \"text\"\n | \"varchar\"\n | \"integer\"\n | \"bigint\"\n | \"boolean\"\n | \"uuid\"\n | \"timestamp\"\n | \"jsonb\"\n | \"enum\";\n\nexport interface ColumnRef {\n readonly __isColumnRef: true;\n readonly tableName: string;\n readonly columnName: string;\n}\n\nexport type FkRef = ColumnRef | (() => ColumnRef);\n\nexport interface ResolvedForeignKey {\n readonly targetTable: string;\n readonly targetColumn: string;\n readonly onDelete?: ReferentialAction;\n readonly onUpdate?: ReferentialAction;\n}\n\n/** Mutable declaration state. It is cloned per table and never published. */\nexport interface MutableColumnMeta {\n name: string;\n columnName: string;\n kind: ColumnValueKind;\n storageKind: StorageKind;\n notNull: boolean;\n primaryKey: boolean;\n unique: boolean;\n isPrivate: boolean;\n serverGenerated: boolean;\n hasDefault: boolean;\n defaultValue?: string | number | boolean;\n defaultNow?: boolean;\n defaultRandom?: boolean;\n withTimezone?: boolean;\n varcharLength?: number;\n enumName?: string;\n enumValues?: readonly string[];\n fkRef?: FkRef;\n onDelete?: ReferentialAction;\n onUpdate?: ReferentialAction;\n fk?: ResolvedForeignKey;\n engineColumn?: EngineColumn;\n}\n\n/** Immutable column metadata published by a finalized schema. */\nexport type ColumnMeta = Readonly<\n Omit<MutableColumnMeta, \"enumValues\" | \"fk\" | \"fkRef\" | \"engineColumn\"> & {\n readonly enumValues?: readonly string[];\n readonly fk?: Readonly<ResolvedForeignKey>;\n readonly engineColumn: EngineColumn;\n }\n>;\n\nexport interface ResolvedRelation {\n readonly name: string;\n readonly cardinality: \"toOne\" | \"toMany\";\n readonly localColumn: string;\n readonly targetTable: string;\n readonly targetColumn: string;\n readonly inferred: boolean;\n}\n\nexport interface AppKitTable {\n readonly $name: string;\n readonly $schemaName: string;\n readonly $columns: Readonly<Record<string, ColumnMeta>>;\n readonly $engine: EngineTable;\n readonly $relations: readonly ResolvedRelation[];\n /** @internal insert schema retained from the current-main foundation. */\n readonly $insertSchema: unknown;\n /** @internal update schema retained from the current-main foundation. */\n readonly $updateSchema: unknown;\n}\n\n/** A declaration handle gains finalized table metadata only at publication. */\nexport type TableHandle<C extends Record<string, unknown>> = AppKitTable & {\n readonly [K in keyof C]: ColumnRef;\n};\n\nexport interface DefineSchemaOptions {\n readonly schemaName?: string;\n}\n\n/**\n * One finalized schema. `TTableName` keeps the declared names in the type, so\n * configuration that addresses a table by name is checked against the schema\n * it was written for. Code that accepts any schema uses the default.\n */\nexport interface Schema<TTableName extends string = string> {\n readonly $schemaName: string;\n readonly $tables: Readonly<Record<TTableName, AppKitTable>>;\n readonly $engine: Readonly<Record<string, EngineTable>>;\n}\n\nexport class SchemaBuildError extends Error {\n constructor(message: string) {\n super(message);\n this.name = \"SchemaBuildError\";\n }\n}\n"],"mappings":";;AAqBA,SAAgB,uBACd,MAC2B;AAC3B,SAAQ,MAAR;EACE,KAAK,SACH,QAAO;GAAC;GAAM;GAAO;GAAM;GAAQ;GAAQ;EAC7C,KAAK;EACL,KAAK;EACL,KAAK,OACH,QAAO;GAAC;GAAM;GAAO;GAAM;GAAM;GAAO;GAAM;GAAM;EACtD,KAAK;EACL,KAAK;EACL,KAAK,OACH,QAAO;GAAC;GAAM;GAAO;GAAK;EAC5B,KAAK;EACL,KAAK,UACH,QAAO,EAAE;;;AAwIf,IAAa,mBAAb,cAAsC,MAAM;CAC1C,YAAY,SAAiB;AAC3B,QAAM,QAAQ;AACd,OAAK,OAAO"}
@@ -0,0 +1,78 @@
1
+ import { columnValueSchema } from "../../../database/schema-builder/validators.js";
2
+ import { DatabasePluginError } from "../../../database/errors.js";
3
+
4
+ //#region src/plugins/database/crud/codecs.ts
5
+ /** Decimal integer with no sign padding, leading zeros, or exponent. */
6
+ const DECIMAL_INT = /^-?(0|[1-9]\d*)$/;
7
+ /**
8
+ * Map a wire value onto the runtime representation the schema validators and
9
+ * Drizzle columns expect. Only path segments and JSON scalars reach this, so
10
+ * the accepted shapes stay exact rather than coercing whatever parses.
11
+ */
12
+ function toRuntimeValue(kind, raw) {
13
+ switch (kind) {
14
+ case "number":
15
+ if (typeof raw === "number") return raw;
16
+ return typeof raw === "string" && DECIMAL_INT.test(raw) ? Number(raw) : void 0;
17
+ case "bigint":
18
+ if (typeof raw === "string" && DECIMAL_INT.test(raw)) return BigInt(raw);
19
+ return typeof raw === "number" && Number.isSafeInteger(raw) ? BigInt(raw) : void 0;
20
+ case "boolean": return typeof raw === "boolean" ? raw : void 0;
21
+ case "string":
22
+ case "uuid":
23
+ case "enum":
24
+ case "date": return typeof raw === "string" ? raw : void 0;
25
+ default: return;
26
+ }
27
+ }
28
+ function encodeValue(meta, value) {
29
+ if (value === null || value === void 0) return null;
30
+ switch (meta.kind) {
31
+ case "bigint":
32
+ if (typeof value === "bigint") return value.toString();
33
+ if (typeof value === "number" && Number.isSafeInteger(value)) return value.toString();
34
+ break;
35
+ case "number":
36
+ if (typeof value === "number" && Number.isFinite(value)) return value;
37
+ break;
38
+ case "boolean":
39
+ if (typeof value === "boolean") return value;
40
+ break;
41
+ case "string":
42
+ case "uuid":
43
+ case "enum":
44
+ case "date":
45
+ if (typeof value === "string") return value;
46
+ break;
47
+ case "json":
48
+ if (isJsonValue(value)) return value;
49
+ break;
50
+ case "unknown": break;
51
+ }
52
+ throw new DatabasePluginError("INTERNAL", "read");
53
+ }
54
+ function isJsonValue(value) {
55
+ if (value === null || typeof value === "string" || typeof value === "boolean") return true;
56
+ if (typeof value === "number") return Number.isFinite(value);
57
+ if (Array.isArray(value)) return true;
58
+ if (typeof value !== "object") return false;
59
+ const prototype = Object.getPrototypeOf(value);
60
+ return prototype === Object.prototype || prototype === null;
61
+ }
62
+ /** Compile the wire codecs for one column from its finalized metadata. */
63
+ function compileColumn(meta) {
64
+ const schema = columnValueSchema(meta);
65
+ return {
66
+ meta,
67
+ decode: (raw) => {
68
+ const candidate = toRuntimeValue(meta.kind, raw);
69
+ if (candidate === void 0) return void 0;
70
+ return schema.safeParse(candidate).success ? candidate : void 0;
71
+ },
72
+ encode: (value) => encodeValue(meta, value)
73
+ };
74
+ }
75
+
76
+ //#endregion
77
+ export { compileColumn };
78
+ //# sourceMappingURL=codecs.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"codecs.js","names":[],"sources":["../../../../src/plugins/database/crud/codecs.ts"],"sourcesContent":["import { DatabasePluginError } from \"../../../database/errors\";\nimport type { ScalarValue } from \"../../../database/runtime\";\nimport type { ColumnMeta } from \"../../../database/schema-builder\";\nimport { columnValueSchema } from \"../../../database/schema-builder/validators\";\n\n/** JSON-representable value; every generated response is built from these. */\nexport type JsonValue =\n | string\n | number\n | boolean\n | null\n | JsonValue[]\n | { [key: string]: JsonValue };\n\n/** Both wire directions for one column, compiled once per exposed table. */\nexport interface CompiledColumn {\n readonly meta: ColumnMeta;\n /** Untrusted wire value to canonical runtime value; `undefined` when invalid. */\n decode(raw: unknown): ScalarValue | undefined;\n /** Trusted runtime value to its deterministic JSON form. */\n encode(value: unknown): JsonValue;\n}\n\n/** Decimal integer with no sign padding, leading zeros, or exponent. */\nconst DECIMAL_INT = /^-?(0|[1-9]\\d*)$/;\n\n/**\n * Map a wire value onto the runtime representation the schema validators and\n * Drizzle columns expect. Only path segments and JSON scalars reach this, so\n * the accepted shapes stay exact rather than coercing whatever parses.\n */\nfunction toRuntimeValue(\n kind: ColumnMeta[\"kind\"],\n raw: unknown,\n): ScalarValue | undefined {\n switch (kind) {\n case \"number\":\n if (typeof raw === \"number\") return raw;\n return typeof raw === \"string\" && DECIMAL_INT.test(raw)\n ? Number(raw)\n : undefined;\n case \"bigint\":\n if (typeof raw === \"string\" && DECIMAL_INT.test(raw)) return BigInt(raw);\n return typeof raw === \"number\" && Number.isSafeInteger(raw)\n ? BigInt(raw)\n : undefined;\n case \"boolean\":\n return typeof raw === \"boolean\" ? raw : undefined;\n case \"string\":\n case \"uuid\":\n case \"enum\":\n case \"date\":\n return typeof raw === \"string\" ? raw : undefined;\n default:\n return undefined;\n }\n}\n\nfunction encodeValue(meta: ColumnMeta, value: unknown): JsonValue {\n if (value === null || value === undefined) return null;\n switch (meta.kind) {\n case \"bigint\":\n // JSON cannot carry a bigint, and a large id must not lose precision.\n if (typeof value === \"bigint\") return value.toString();\n if (typeof value === \"number\" && Number.isSafeInteger(value)) {\n return value.toString();\n }\n break;\n case \"number\":\n if (typeof value === \"number\" && Number.isFinite(value)) return value;\n break;\n case \"boolean\":\n if (typeof value === \"boolean\") return value;\n break;\n case \"string\":\n case \"uuid\":\n case \"enum\":\n case \"date\":\n if (typeof value === \"string\") return value;\n break;\n case \"json\":\n if (isJsonValue(value)) return value;\n break;\n case \"unknown\":\n break;\n }\n // The driver produced a value this column contract cannot describe.\n throw new DatabasePluginError(\"INTERNAL\", \"read\");\n}\n\nfunction isJsonValue(value: unknown): value is JsonValue {\n if (\n value === null ||\n typeof value === \"string\" ||\n typeof value === \"boolean\"\n ) {\n return true;\n }\n if (typeof value === \"number\") return Number.isFinite(value);\n if (Array.isArray(value)) return true;\n if (typeof value !== \"object\") return false;\n const prototype = Object.getPrototypeOf(value);\n return prototype === Object.prototype || prototype === null;\n}\n\n/** Compile the wire codecs for one column from its finalized metadata. */\nexport function compileColumn(meta: ColumnMeta): CompiledColumn {\n const schema = columnValueSchema(meta);\n return {\n meta,\n decode: (raw) => {\n const candidate = toRuntimeValue(meta.kind, raw);\n if (candidate === undefined) return undefined;\n return schema.safeParse(candidate).success ? candidate : undefined;\n },\n encode: (value) => encodeValue(meta, value),\n };\n}\n"],"mappings":";;;;;AAwBA,MAAM,cAAc;;;;;;AAOpB,SAAS,eACP,MACA,KACyB;AACzB,SAAQ,MAAR;EACE,KAAK;AACH,OAAI,OAAO,QAAQ,SAAU,QAAO;AACpC,UAAO,OAAO,QAAQ,YAAY,YAAY,KAAK,IAAI,GACnD,OAAO,IAAI,GACX;EACN,KAAK;AACH,OAAI,OAAO,QAAQ,YAAY,YAAY,KAAK,IAAI,CAAE,QAAO,OAAO,IAAI;AACxE,UAAO,OAAO,QAAQ,YAAY,OAAO,cAAc,IAAI,GACvD,OAAO,IAAI,GACX;EACN,KAAK,UACH,QAAO,OAAO,QAAQ,YAAY,MAAM;EAC1C,KAAK;EACL,KAAK;EACL,KAAK;EACL,KAAK,OACH,QAAO,OAAO,QAAQ,WAAW,MAAM;EACzC,QACE;;;AAIN,SAAS,YAAY,MAAkB,OAA2B;AAChE,KAAI,UAAU,QAAQ,UAAU,OAAW,QAAO;AAClD,SAAQ,KAAK,MAAb;EACE,KAAK;AAEH,OAAI,OAAO,UAAU,SAAU,QAAO,MAAM,UAAU;AACtD,OAAI,OAAO,UAAU,YAAY,OAAO,cAAc,MAAM,CAC1D,QAAO,MAAM,UAAU;AAEzB;EACF,KAAK;AACH,OAAI,OAAO,UAAU,YAAY,OAAO,SAAS,MAAM,CAAE,QAAO;AAChE;EACF,KAAK;AACH,OAAI,OAAO,UAAU,UAAW,QAAO;AACvC;EACF,KAAK;EACL,KAAK;EACL,KAAK;EACL,KAAK;AACH,OAAI,OAAO,UAAU,SAAU,QAAO;AACtC;EACF,KAAK;AACH,OAAI,YAAY,MAAM,CAAE,QAAO;AAC/B;EACF,KAAK,UACH;;AAGJ,OAAM,IAAI,oBAAoB,YAAY,OAAO;;AAGnD,SAAS,YAAY,OAAoC;AACvD,KACE,UAAU,QACV,OAAO,UAAU,YACjB,OAAO,UAAU,UAEjB,QAAO;AAET,KAAI,OAAO,UAAU,SAAU,QAAO,OAAO,SAAS,MAAM;AAC5D,KAAI,MAAM,QAAQ,MAAM,CAAE,QAAO;AACjC,KAAI,OAAO,UAAU,SAAU,QAAO;CACtC,MAAM,YAAY,OAAO,eAAe,MAAM;AAC9C,QAAO,cAAc,OAAO,aAAa,cAAc;;;AAIzD,SAAgB,cAAc,MAAkC;CAC9D,MAAM,SAAS,kBAAkB,KAAK;AACtC,QAAO;EACL;EACA,SAAS,QAAQ;GACf,MAAM,YAAY,eAAe,KAAK,MAAM,IAAI;AAChD,OAAI,cAAc,OAAW,QAAO;AACpC,UAAO,OAAO,UAAU,UAAU,CAAC,UAAU,YAAY;;EAE3D,SAAS,UAAU,YAAY,MAAM,MAAM;EAC5C"}
@@ -0,0 +1,151 @@
1
+ import { filterOperatorsForKind } from "../../../database/schema-builder/types.js";
2
+ import { DatabasePluginError, invalidDatabaseInput } from "../../../database/errors.js";
3
+ import { MAX_SERIALIZED_DEPTH, MAX_SERIALIZED_NODES } from "../defaults.js";
4
+ import { compileColumn } from "./codecs.js";
5
+
6
+ //#region src/plugins/database/crud/contract.ts
7
+ /** A bare object literal; a `Date`, class instance, or `Map` is not JSON. */
8
+ function isPlainObject(value) {
9
+ if (value === null || typeof value !== "object" || Array.isArray(value)) return false;
10
+ const prototype = Object.getPrototypeOf(value);
11
+ return prototype === Object.prototype || prototype === null;
12
+ }
13
+ /** A serializer that breaks its contract is trusted code failing, not input. */
14
+ function serializerFault() {
15
+ throw new DatabasePluginError("INTERNAL", "read");
16
+ }
17
+ /** Charge one value against the output budget before descending into it. */
18
+ function countNode(depth, state) {
19
+ state.nodes += 1;
20
+ if (state.nodes > MAX_SERIALIZED_NODES || depth > MAX_SERIALIZED_DEPTH) serializerFault();
21
+ }
22
+ /** Walk a container while its ancestors are tracked, so a cycle cannot pass. */
23
+ function enterObject(value, state, visit) {
24
+ if (state.ancestors.has(value)) serializerFault();
25
+ state.ancestors.add(value);
26
+ try {
27
+ return visit();
28
+ } finally {
29
+ state.ancestors.delete(value);
30
+ }
31
+ }
32
+ /** Accept a serializer's own added value only where it is already JSON. */
33
+ function sanitizeJson(value, depth, state) {
34
+ countNode(depth, state);
35
+ if (value === null || typeof value === "string" || typeof value === "boolean") return value;
36
+ if (typeof value === "number") {
37
+ if (!Number.isFinite(value)) serializerFault();
38
+ return value;
39
+ }
40
+ if (Array.isArray(value)) return enterObject(value, state, () => value.map((item) => sanitizeJson(item, depth + 1, state)));
41
+ if (!isPlainObject(value)) serializerFault();
42
+ return enterObject(value, state, () => {
43
+ const out = Object.create(null);
44
+ for (const [key, child] of Object.entries(value)) {
45
+ if (child === void 0) continue;
46
+ out[key] = sanitizeJson(child, depth + 1, state);
47
+ }
48
+ return out;
49
+ });
50
+ }
51
+ /** Keep an included row under its own table's policy, one row or many. */
52
+ function sanitizeRelation(target, value, depth, state) {
53
+ if (value === null) return null;
54
+ if (!Array.isArray(value)) return sanitizeRow(target, value, depth, state);
55
+ countNode(depth, state);
56
+ return enterObject(value, state, () => value.map((row) => sanitizeRow(target, row, depth + 1, state)));
57
+ }
58
+ /** Re-apply the private-column policy wherever the output stays contracted. */
59
+ function sanitizeRow(table, row, depth, state) {
60
+ countNode(depth, state);
61
+ if (!isPlainObject(row)) serializerFault();
62
+ return enterObject(row, state, () => {
63
+ const out = {};
64
+ for (const [key, child] of Object.entries(row)) {
65
+ if (child === void 0) continue;
66
+ if (table.columns.get(key)?.meta.isPrivate) continue;
67
+ const relation = table.relations.get(key);
68
+ out[key] = relation ? sanitizeRelation(relation.target, child, depth + 1, state) : sanitizeJson(child, depth + 1, state);
69
+ }
70
+ return out;
71
+ });
72
+ }
73
+ /** Project an included row through its own table; absent to-one reads null. */
74
+ function projectRelation(target, value) {
75
+ if (value === null || value === void 0) return null;
76
+ return Array.isArray(value) ? value.map((row) => target.projectPublicRow(row)) : target.projectPublicRow(value);
77
+ }
78
+ /** Build the public JSON for one driver row, dropping anything uncontracted. */
79
+ function projectRow(table, row) {
80
+ const out = {};
81
+ for (const [key, value] of Object.entries(row)) {
82
+ const column = table.columns.get(key);
83
+ if (column) {
84
+ if (!column.meta.isPrivate) out[key] = column.encode(value);
85
+ continue;
86
+ }
87
+ const relation = table.relations.get(key);
88
+ if (relation) out[key] = projectRelation(relation.target, value);
89
+ }
90
+ return out;
91
+ }
92
+ /** Compile one table's allowlists and codecs from its finalized metadata. */
93
+ function compileTable(table) {
94
+ const columns = /* @__PURE__ */ new Map();
95
+ const selectable = /* @__PURE__ */ new Set();
96
+ const queryable = /* @__PURE__ */ new Set();
97
+ let primaryKey;
98
+ for (const meta of Object.values(table.$columns)) {
99
+ const column = compileColumn(meta);
100
+ columns.set(meta.columnName, column);
101
+ if (meta.primaryKey && !meta.isPrivate) primaryKey = column;
102
+ if (meta.isPrivate) continue;
103
+ selectable.add(meta.columnName);
104
+ if (filterOperatorsForKind(meta.kind).length > 0) queryable.add(meta.columnName);
105
+ }
106
+ const compiled = {
107
+ name: table.$name,
108
+ primaryKey,
109
+ columns,
110
+ selectable,
111
+ queryable,
112
+ relations: /* @__PURE__ */ new Map(),
113
+ decodeId: (raw) => {
114
+ if (!primaryKey) throw new DatabasePluginError("INTERNAL", "read");
115
+ const value = primaryKey.decode(raw);
116
+ if (typeof value !== "string" && typeof value !== "number" && typeof value !== "bigint") throw invalidDatabaseInput(["id"], "Not a valid identifier");
117
+ return value;
118
+ },
119
+ projectPublicRow: (row) => projectRow(compiled, row),
120
+ sanitizeSerializedRow: (row) => sanitizeRow(compiled, row, 0, {
121
+ nodes: 0,
122
+ ancestors: /* @__PURE__ */ new Set()
123
+ })
124
+ };
125
+ return compiled;
126
+ }
127
+ /**
128
+ * Compile the HTTP contract for every exposed table and wire the relations
129
+ * they share. A relation whose target is not exposed stays unreachable, so
130
+ * enabling one table never widens another table's public surface.
131
+ */
132
+ function compileCrudTables(tables) {
133
+ const compiled = /* @__PURE__ */ new Map();
134
+ for (const table of Object.values(tables)) compiled.set(table.$name, compileTable(table));
135
+ for (const table of Object.values(tables)) {
136
+ const entry = compiled.get(table.$name);
137
+ for (const relation of table.$relations) {
138
+ const target = compiled.get(relation.targetTable);
139
+ if (!entry || !target) continue;
140
+ entry.relations.set(relation.name, {
141
+ cardinality: relation.cardinality,
142
+ target
143
+ });
144
+ }
145
+ }
146
+ return compiled;
147
+ }
148
+
149
+ //#endregion
150
+ export { compileCrudTables };
151
+ //# sourceMappingURL=contract.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"contract.js","names":[],"sources":["../../../../src/plugins/database/crud/contract.ts"],"sourcesContent":["import {\n DatabasePluginError,\n invalidDatabaseInput,\n} from \"../../../database/errors\";\nimport type { IdValue, Row } from \"../../../database/runtime\";\nimport type { AppKitTable } from \"../../../database/schema-builder\";\nimport { filterOperatorsForKind } from \"../../../database/schema-builder/types\";\nimport { MAX_SERIALIZED_DEPTH, MAX_SERIALIZED_NODES } from \"../defaults\";\nimport { type CompiledColumn, compileColumn, type JsonValue } from \"./codecs\";\n\n/** One relation edge wired to the contract of its target table. */\nexport interface CrudRelation {\n readonly cardinality: \"toOne\" | \"toMany\";\n readonly target: CrudTable;\n}\n\n/** Private HTTP contract compiled once for one explicitly exposed table. */\nexport interface CrudTable {\n readonly name: string;\n readonly primaryKey?: CompiledColumn;\n readonly columns: ReadonlyMap<string, CompiledColumn>;\n /** Public columns a request may project. */\n readonly selectable: ReadonlySet<string>;\n /** Public columns a request may filter or order by. */\n readonly queryable: ReadonlySet<string>;\n readonly relations: ReadonlyMap<string, CrudRelation>;\n decodeId(raw: string): IdValue;\n projectPublicRow(row: Row): JsonValue;\n sanitizeSerializedRow(row: unknown): JsonValue;\n}\n\ntype MutableCrudTable = Omit<CrudTable, \"relations\"> & {\n readonly relations: Map<string, CrudRelation>;\n};\n\ninterface SanitizeState {\n nodes: number;\n readonly ancestors: Set<object>;\n}\n\n/** A bare object literal; a `Date`, class instance, or `Map` is not JSON. */\nfunction isPlainObject(value: unknown): value is Record<string, unknown> {\n if (value === null || typeof value !== \"object\" || Array.isArray(value)) {\n return false;\n }\n const prototype = Object.getPrototypeOf(value);\n return prototype === Object.prototype || prototype === null;\n}\n\n/** A serializer that breaks its contract is trusted code failing, not input. */\nfunction serializerFault(): never {\n throw new DatabasePluginError(\"INTERNAL\", \"read\");\n}\n\n/** Charge one value against the output budget before descending into it. */\nfunction countNode(depth: number, state: SanitizeState): void {\n state.nodes += 1;\n if (state.nodes > MAX_SERIALIZED_NODES || depth > MAX_SERIALIZED_DEPTH) {\n serializerFault();\n }\n}\n\n/** Walk a container while its ancestors are tracked, so a cycle cannot pass. */\nfunction enterObject<T>(\n value: object,\n state: SanitizeState,\n visit: () => T,\n): T {\n if (state.ancestors.has(value)) serializerFault();\n state.ancestors.add(value);\n try {\n return visit();\n } finally {\n state.ancestors.delete(value);\n }\n}\n\n/** Accept a serializer's own added value only where it is already JSON. */\nfunction sanitizeJson(\n value: unknown,\n depth: number,\n state: SanitizeState,\n): JsonValue {\n countNode(depth, state);\n if (\n value === null ||\n typeof value === \"string\" ||\n typeof value === \"boolean\"\n ) {\n return value;\n }\n if (typeof value === \"number\") {\n if (!Number.isFinite(value)) serializerFault();\n return value;\n }\n if (Array.isArray(value)) {\n return enterObject(value, state, () =>\n value.map((item) => sanitizeJson(item, depth + 1, state)),\n );\n }\n if (!isPlainObject(value)) serializerFault();\n return enterObject(value, state, () => {\n // A null prototype keeps `__proto__` an ordinary key instead of a setter.\n const out: Record<string, JsonValue> = Object.create(null);\n for (const [key, child] of Object.entries(value)) {\n if (child === undefined) continue;\n out[key] = sanitizeJson(child, depth + 1, state);\n }\n return out;\n });\n}\n\n/** Keep an included row under its own table's policy, one row or many. */\nfunction sanitizeRelation(\n target: CrudTable,\n value: unknown,\n depth: number,\n state: SanitizeState,\n): JsonValue {\n if (value === null) return null;\n if (!Array.isArray(value)) return sanitizeRow(target, value, depth, state);\n countNode(depth, state);\n return enterObject(value, state, () =>\n value.map((row) => sanitizeRow(target, row, depth + 1, state)),\n );\n}\n\n/** Re-apply the private-column policy wherever the output stays contracted. */\nfunction sanitizeRow(\n table: CrudTable,\n row: unknown,\n depth: number,\n state: SanitizeState,\n): JsonValue {\n countNode(depth, state);\n if (!isPlainObject(row)) serializerFault();\n return enterObject(row, state, () => {\n const out: Record<string, JsonValue> = {};\n for (const [key, child] of Object.entries(row)) {\n if (child === undefined) continue;\n if (table.columns.get(key)?.meta.isPrivate) continue;\n const relation = table.relations.get(key);\n out[key] = relation\n ? sanitizeRelation(relation.target, child, depth + 1, state)\n : sanitizeJson(child, depth + 1, state);\n }\n return out;\n });\n}\n\n/** Project an included row through its own table; absent to-one reads null. */\nfunction projectRelation(target: CrudTable, value: unknown): JsonValue {\n if (value === null || value === undefined) return null;\n return Array.isArray(value)\n ? value.map((row) => target.projectPublicRow(row as Row))\n : target.projectPublicRow(value as Row);\n}\n\n/** Build the public JSON for one driver row, dropping anything uncontracted. */\nfunction projectRow(table: CrudTable, row: Row): JsonValue {\n const out: Record<string, JsonValue> = {};\n for (const [key, value] of Object.entries(row)) {\n const column = table.columns.get(key);\n if (column) {\n // Whatever the driver returned, only public contracted columns ship.\n if (!column.meta.isPrivate) out[key] = column.encode(value);\n continue;\n }\n const relation = table.relations.get(key);\n if (relation) out[key] = projectRelation(relation.target, value);\n }\n return out;\n}\n\n/** Compile one table's allowlists and codecs from its finalized metadata. */\nfunction compileTable(table: AppKitTable): MutableCrudTable {\n const columns = new Map<string, CompiledColumn>();\n const selectable = new Set<string>();\n const queryable = new Set<string>();\n let primaryKey: CompiledColumn | undefined;\n\n for (const meta of Object.values(table.$columns)) {\n const column = compileColumn(meta);\n columns.set(meta.columnName, column);\n // A private key must not power `GET /:table/:id`: per-id probing would\n // answer 200 or 404 on an identifier the schema hides, so over HTTP the\n // table is keyless — no detail route, and lists must name their own order.\n if (meta.primaryKey && !meta.isPrivate) primaryKey = column;\n if (meta.isPrivate) continue;\n selectable.add(meta.columnName);\n if (filterOperatorsForKind(meta.kind).length > 0) {\n queryable.add(meta.columnName);\n }\n }\n\n const compiled: MutableCrudTable = {\n name: table.$name,\n primaryKey,\n columns,\n selectable,\n queryable,\n relations: new Map(),\n decodeId: (raw) => {\n if (!primaryKey) throw new DatabasePluginError(\"INTERNAL\", \"read\");\n const value = primaryKey.decode(raw);\n if (\n typeof value !== \"string\" &&\n typeof value !== \"number\" &&\n typeof value !== \"bigint\"\n ) {\n throw invalidDatabaseInput([\"id\"], \"Not a valid identifier\");\n }\n return value;\n },\n projectPublicRow: (row) => projectRow(compiled, row),\n sanitizeSerializedRow: (row) =>\n sanitizeRow(compiled, row, 0, { nodes: 0, ancestors: new Set() }),\n };\n return compiled;\n}\n\n/**\n * Compile the HTTP contract for every exposed table and wire the relations\n * they share. A relation whose target is not exposed stays unreachable, so\n * enabling one table never widens another table's public surface.\n */\nexport function compileCrudTables(\n tables: Record<string, AppKitTable>,\n): Map<string, CrudTable> {\n const compiled = new Map<string, MutableCrudTable>();\n for (const table of Object.values(tables)) {\n compiled.set(table.$name, compileTable(table));\n }\n for (const table of Object.values(tables)) {\n const entry = compiled.get(table.$name);\n for (const relation of table.$relations) {\n const target = compiled.get(relation.targetTable);\n if (!entry || !target) continue;\n entry.relations.set(relation.name, {\n cardinality: relation.cardinality,\n target,\n });\n }\n }\n return compiled as Map<string, CrudTable>;\n}\n"],"mappings":"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"}
@@ -0,0 +1,40 @@
1
+ import { databaseSetupFailed } from "../../../database/errors.js";
2
+
3
+ //#region src/plugins/database/crud/exposure.ts
4
+ /** A table name also becomes a URL path segment, so keep it unambiguous. */
5
+ const ROUTABLE_TABLE = /^[A-Za-z][A-Za-z0-9_-]{0,63}$/;
6
+ /** Refuse names that cannot address exactly one table over HTTP. */
7
+ function assertRoutable(names) {
8
+ const lowercased = /* @__PURE__ */ new Set();
9
+ for (const name of names) {
10
+ if (!ROUTABLE_TABLE.test(name) || lowercased.has(name.toLowerCase())) throw databaseSetupFailed();
11
+ lowercased.add(name.toLowerCase());
12
+ }
13
+ }
14
+ /**
15
+ * Resolve the tables whose generated reads are explicitly turned on. The
16
+ * exposure value arrives untyped because a finalized schema widens its table
17
+ * names to `string`, so every name is re-checked against the declared schema.
18
+ */
19
+ function resolveExposedTables(exposure, declared) {
20
+ if (exposure === void 0 || exposure === false) return [];
21
+ if (exposure === true) {
22
+ assertRoutable(declared);
23
+ return [...declared];
24
+ }
25
+ if (typeof exposure !== "object" || exposure === null) throw databaseSetupFailed();
26
+ const requested = exposure.tables;
27
+ if (!Array.isArray(requested)) throw databaseSetupFailed();
28
+ const names = [];
29
+ for (const name of requested) {
30
+ if (typeof name !== "string" || !declared.includes(name)) throw databaseSetupFailed();
31
+ if (names.includes(name)) throw databaseSetupFailed();
32
+ names.push(name);
33
+ }
34
+ assertRoutable(names);
35
+ return names;
36
+ }
37
+
38
+ //#endregion
39
+ export { resolveExposedTables };
40
+ //# sourceMappingURL=exposure.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"exposure.js","names":[],"sources":["../../../../src/plugins/database/crud/exposure.ts"],"sourcesContent":["import { databaseSetupFailed } from \"../../../database/errors\";\n\n/** A table name also becomes a URL path segment, so keep it unambiguous. */\nconst ROUTABLE_TABLE = /^[A-Za-z][A-Za-z0-9_-]{0,63}$/;\n\n/** Refuse names that cannot address exactly one table over HTTP. */\nfunction assertRoutable(names: readonly string[]): void {\n const lowercased = new Set<string>();\n for (const name of names) {\n // Express matches paths case-insensitively, so near-duplicates would alias.\n if (!ROUTABLE_TABLE.test(name) || lowercased.has(name.toLowerCase())) {\n throw databaseSetupFailed();\n }\n lowercased.add(name.toLowerCase());\n }\n}\n\n/**\n * Resolve the tables whose generated reads are explicitly turned on. The\n * exposure value arrives untyped because a finalized schema widens its table\n * names to `string`, so every name is re-checked against the declared schema.\n */\nexport function resolveExposedTables(\n exposure: unknown,\n declared: readonly string[],\n): string[] {\n if (exposure === undefined || exposure === false) return [];\n if (exposure === true) {\n assertRoutable(declared);\n return [...declared];\n }\n if (typeof exposure !== \"object\" || exposure === null) {\n throw databaseSetupFailed();\n }\n const requested = (exposure as { tables?: unknown }).tables;\n if (!Array.isArray(requested)) throw databaseSetupFailed();\n\n const names: string[] = [];\n for (const name of requested) {\n // Unknown and duplicate names are configuration bugs, not empty routes.\n if (typeof name !== \"string\" || !declared.includes(name)) {\n throw databaseSetupFailed();\n }\n if (names.includes(name)) throw databaseSetupFailed();\n names.push(name);\n }\n assertRoutable(names);\n return names;\n}\n"],"mappings":";;;;AAGA,MAAM,iBAAiB;;AAGvB,SAAS,eAAe,OAAgC;CACtD,MAAM,6BAAa,IAAI,KAAa;AACpC,MAAK,MAAM,QAAQ,OAAO;AAExB,MAAI,CAAC,eAAe,KAAK,KAAK,IAAI,WAAW,IAAI,KAAK,aAAa,CAAC,CAClE,OAAM,qBAAqB;AAE7B,aAAW,IAAI,KAAK,aAAa,CAAC;;;;;;;;AAStC,SAAgB,qBACd,UACA,UACU;AACV,KAAI,aAAa,UAAa,aAAa,MAAO,QAAO,EAAE;AAC3D,KAAI,aAAa,MAAM;AACrB,iBAAe,SAAS;AACxB,SAAO,CAAC,GAAG,SAAS;;AAEtB,KAAI,OAAO,aAAa,YAAY,aAAa,KAC/C,OAAM,qBAAqB;CAE7B,MAAM,YAAa,SAAkC;AACrD,KAAI,CAAC,MAAM,QAAQ,UAAU,CAAE,OAAM,qBAAqB;CAE1D,MAAM,QAAkB,EAAE;AAC1B,MAAK,MAAM,QAAQ,WAAW;AAE5B,MAAI,OAAO,SAAS,YAAY,CAAC,SAAS,SAAS,KAAK,CACtD,OAAM,qBAAqB;AAE7B,MAAI,MAAM,SAAS,KAAK,CAAE,OAAM,qBAAqB;AACrD,QAAM,KAAK,KAAK;;AAElB,gBAAe,MAAM;AACrB,QAAO"}
@@ -0,0 +1,244 @@
1
+ import { filterOperatorsForKind } from "../../../database/schema-builder/types.js";
2
+ import { invalidDatabaseInput } from "../../../database/errors.js";
3
+ import { MAX_GROUP_ITEMS, MAX_MATERIALIZED_NODES, MAX_OFFSET, MAX_ORDER_FIELDS, MAX_QUERY_BYTES, MAX_WHERE_CONDITIONS, MAX_WHERE_DEPTH } from "../defaults.js";
4
+ import { DEFAULT_LIMIT, IN_CAP, MAX_INCLUDES, MAX_INCLUDE_DEPTH, MAX_INCLUDE_NODES, MAX_LIMIT, isFilterOperator } from "../../../database/contract/wire.js";
5
+ import "../../../database/contract/index.js";
6
+
7
+ //#region src/plugins/database/crud/query.ts
8
+ const LIST_PARAMS = new Set([
9
+ "where",
10
+ "order",
11
+ "select",
12
+ "include",
13
+ "limit",
14
+ "offset"
15
+ ]);
16
+ const DETAIL_PARAMS = new Set(["select", "include"]);
17
+ const CANONICAL_INT = /^-?(0|[1-9]\d*)$/;
18
+ /**
19
+ * Reject one query parameter. `parameter` is always a fixed name and `reason`
20
+ * a fixed sentence, so a rejection can never echo caller-supplied text back.
21
+ */
22
+ function reject(parameter, reason) {
23
+ throw invalidDatabaseInput([parameter], reason);
24
+ }
25
+ /** A decoded JSON object; arrays and null are separate wire shapes here. */
26
+ function isPlainObject(value) {
27
+ return typeof value === "object" && value !== null && !Array.isArray(value);
28
+ }
29
+ /** Read known, single-occurrence parameters from the raw query string. */
30
+ function parseParams(rawQuery, allowed) {
31
+ if (Buffer.byteLength(rawQuery, "utf8") > MAX_QUERY_BYTES) reject("query", "Query string exceeds the maximum size");
32
+ const params = /* @__PURE__ */ new Map();
33
+ for (const [key, value] of new URLSearchParams(rawQuery)) {
34
+ if (!allowed.has(key) || params.has(key)) reject("query", "Unknown or repeated query parameter");
35
+ params.set(key, value);
36
+ }
37
+ return params;
38
+ }
39
+ /** Parse one structured parameter without leaking the parser's diagnostics. */
40
+ function parseJson(raw, parameter) {
41
+ try {
42
+ return JSON.parse(raw);
43
+ } catch {
44
+ reject(parameter, "Expected JSON");
45
+ }
46
+ }
47
+ /** Accept one bounded integer, rejecting the forms `Number` would coerce. */
48
+ function decodeIntParam(raw, parameter, fallback, max) {
49
+ if (raw === void 0) return fallback;
50
+ if (!CANONICAL_INT.test(raw)) reject(parameter, "Expected a canonical decimal integer");
51
+ const value = Number(raw);
52
+ if (!Number.isSafeInteger(value) || value < 0 || value > max) reject(parameter, `Must be between 0 and ${max}`);
53
+ return value;
54
+ }
55
+ /** Narrow a projection to the table's public columns. */
56
+ function decodeSelect(table, value, parameter) {
57
+ if (!Array.isArray(value) || value.length === 0) reject(parameter, "Expected a non-empty array of public column names");
58
+ for (const name of value) if (typeof name !== "string" || !table.selectable.has(name)) reject(parameter, "Names an unknown or private column");
59
+ return value;
60
+ }
61
+ /** Order only by columns the table can sort on, in a declared direction. */
62
+ function decodeOrder(table, value, parameter) {
63
+ if (!isPlainObject(value)) reject(parameter, "Expected an object of column directions");
64
+ const entries = Object.entries(value);
65
+ if (entries.length === 0 || entries.length > MAX_ORDER_FIELDS) reject(parameter, `Expected 1 to ${MAX_ORDER_FIELDS} columns`);
66
+ const order = {};
67
+ for (const [column, direction] of entries) {
68
+ if (!table.queryable.has(column)) reject(parameter, "Names an unknown or unorderable column");
69
+ if (direction !== "asc" && direction !== "desc") reject(parameter, "Expected a direction of asc or desc");
70
+ order[column] = direction;
71
+ }
72
+ return order;
73
+ }
74
+ /** Move one filter operand onto its column's canonical runtime value. */
75
+ function decodeOperand(column, raw, parameter) {
76
+ const value = column.decode(raw);
77
+ if (value === void 0) reject(parameter, "Operand does not match its column type");
78
+ return value;
79
+ }
80
+ /**
81
+ * Decode the predicate on one column against the operator matrix its kind
82
+ * allows, counting conditions so a filter cannot grow without bound.
83
+ */
84
+ function decodeCondition(column, value, parameter, state) {
85
+ const countCondition = () => {
86
+ state.conditions += 1;
87
+ if (state.conditions > MAX_WHERE_CONDITIONS) reject(parameter, `Expected at most ${MAX_WHERE_CONDITIONS} conditions`);
88
+ };
89
+ if (value === null) reject(parameter, "Match a null column with is: null");
90
+ if (!isPlainObject(value)) {
91
+ if (Array.isArray(value)) reject(parameter, "Expected a scalar or an operator object");
92
+ countCondition();
93
+ return decodeOperand(column, value, parameter);
94
+ }
95
+ const nullable = !column.meta.notNull;
96
+ const supported = filterOperatorsForKind(column.meta.kind);
97
+ const operators = {};
98
+ const entries = Object.entries(value);
99
+ if (entries.length === 0) reject(parameter, "Filter cannot be empty");
100
+ for (const [operator, operand] of entries) {
101
+ countCondition();
102
+ if (operator === "is") {
103
+ if (operand !== null || !nullable) reject(parameter, "The is operator accepts only null on a nullable column");
104
+ operators.is = null;
105
+ continue;
106
+ }
107
+ if (!isFilterOperator(operator) || !supported.includes(operator)) reject(parameter, "Operator is not supported for this column");
108
+ if (operator !== "in") {
109
+ operators[operator] = decodeOperand(column, operand, parameter);
110
+ continue;
111
+ }
112
+ if (!Array.isArray(operand) || operand.length > IN_CAP) reject(parameter, `Expected an array of at most ${IN_CAP} values`);
113
+ operators.in = operand.map((item) => {
114
+ if (item === null) reject(parameter, "The in operator does not accept null");
115
+ return decodeOperand(column, item, parameter);
116
+ });
117
+ }
118
+ return operators;
119
+ }
120
+ /** Decode one filter level, recursing through bounded `and`/`or` groups. */
121
+ function decodeWhere(table, node, depth, parameter, state) {
122
+ if (depth > MAX_WHERE_DEPTH) reject(parameter, `Expected at most ${MAX_WHERE_DEPTH} nesting levels`);
123
+ if (!isPlainObject(node)) reject(parameter, "Expected an object");
124
+ const clause = {};
125
+ for (const [key, value] of Object.entries(node)) {
126
+ if (key === "and" || key === "or") {
127
+ if (!Array.isArray(value) || value.length === 0 || value.length > MAX_GROUP_ITEMS) reject(parameter, `Expected 1 to ${MAX_GROUP_ITEMS} group members`);
128
+ clause[key] = value.map((member) => decodeWhere(table, member, depth + 1, parameter, state));
129
+ continue;
130
+ }
131
+ const column = table.queryable.has(key) ? table.columns.get(key) : void 0;
132
+ if (!column) reject(parameter, "Names an unknown or unfilterable column");
133
+ clause[key] = decodeCondition(column, value, parameter, state);
134
+ }
135
+ return clause;
136
+ }
137
+ /**
138
+ * Decode one relation's options against the target table, not the parent, and
139
+ * give every to-many edge a limit so an unqualified include stays bounded.
140
+ */
141
+ function decodeIncludeOptions(target, value, depth, toMany, parameter) {
142
+ if (value === true) return toMany ? { limit: DEFAULT_LIMIT } : true;
143
+ if (!isPlainObject(value)) reject(parameter, "Expected true or an options object");
144
+ const options = {};
145
+ for (const [key, inner] of Object.entries(value)) switch (key) {
146
+ case "select":
147
+ options.select = decodeSelect(target, inner, parameter);
148
+ break;
149
+ case "where":
150
+ options.where = decodeWhere(target, inner, 1, parameter, { conditions: 0 });
151
+ break;
152
+ case "order":
153
+ options.order = decodeOrder(target, inner, parameter);
154
+ break;
155
+ case "limit":
156
+ if (!toMany) reject(parameter, "Only to-many relations accept a limit");
157
+ if (typeof inner !== "number" || !Number.isSafeInteger(inner) || inner < 0 || inner > MAX_LIMIT) reject(parameter, `Expected a limit between 0 and ${MAX_LIMIT}`);
158
+ options.limit = inner;
159
+ break;
160
+ case "include":
161
+ options.include = decodeInclude(target, inner, depth + 1, parameter);
162
+ break;
163
+ default: reject(parameter, "Unsupported relation option");
164
+ }
165
+ if (toMany && options.limit === void 0) options.limit = DEFAULT_LIMIT;
166
+ return options;
167
+ }
168
+ /** Decode one include level; a relation to an unexposed table has no edge. */
169
+ function decodeInclude(table, value, depth, parameter) {
170
+ if (depth > MAX_INCLUDE_DEPTH) reject(parameter, `Expected at most ${MAX_INCLUDE_DEPTH} relation edges`);
171
+ if (!isPlainObject(value)) reject(parameter, "Expected an object of relation names");
172
+ const entries = Object.entries(value);
173
+ if (entries.length > MAX_INCLUDES) reject(parameter, `Expected at most ${MAX_INCLUDES} relations per level`);
174
+ const include = {};
175
+ for (const [name, options] of entries) {
176
+ const relation = table.relations.get(name);
177
+ if (!relation) reject(parameter, "Names an unknown or unexposed relation");
178
+ include[name] = decodeIncludeOptions(relation.target, options, depth, relation.cardinality === "toMany", parameter);
179
+ }
180
+ return include;
181
+ }
182
+ /**
183
+ * Rows one include tree materializes per parent row, counting relation nodes
184
+ * on the way. Every term is a non-negative integer bounded by `MAX_LIMIT` and
185
+ * both running totals are checked after each addition, so neither can overflow.
186
+ */
187
+ function measureInclude(table, include, budget) {
188
+ let rows = 1;
189
+ if (!include) return rows;
190
+ for (const [name, value] of Object.entries(include)) {
191
+ const relation = table.relations.get(name);
192
+ if (!relation) continue;
193
+ budget.nodes += 1;
194
+ if (budget.nodes > MAX_INCLUDE_NODES) reject("include", `Expected at most ${MAX_INCLUDE_NODES} relations`);
195
+ const options = value === true ? void 0 : value;
196
+ const child = measureInclude(relation.target, options?.include, budget);
197
+ rows += relation.cardinality === "toMany" ? (options?.limit ?? DEFAULT_LIMIT) * child : child;
198
+ if (rows > MAX_MATERIALIZED_NODES) reject("include", "Relation fan-out exceeds the read budget");
199
+ }
200
+ return rows;
201
+ }
202
+ /** Reject an include tree whose cost is only visible once it is assembled. */
203
+ function assertReadBudget(table, include, rootRows) {
204
+ if (rootRows * measureInclude(table, include, { nodes: 0 }) > MAX_MATERIALIZED_NODES) reject("include", "Relation fan-out exceeds the read budget");
205
+ }
206
+ /** Decode the projection parameters both reads share. */
207
+ function decodeProjection(table, params) {
208
+ const rawSelect = params.get("select");
209
+ const rawInclude = params.get("include");
210
+ return {
211
+ select: rawSelect === void 0 ? void 0 : decodeSelect(table, parseJson(rawSelect, "select"), "select"),
212
+ include: rawInclude === void 0 ? void 0 : decodeInclude(table, parseJson(rawInclude, "include"), 1, "include")
213
+ };
214
+ }
215
+ /** Decode `GET /:table` and reject it before any database work is scheduled. */
216
+ function decodeListQuery(table, rawQuery) {
217
+ const params = parseParams(rawQuery, LIST_PARAMS);
218
+ const rawWhere = params.get("where");
219
+ const rawOrder = params.get("order");
220
+ const where = rawWhere === void 0 ? void 0 : decodeWhere(table, parseJson(rawWhere, "where"), 1, "where", { conditions: 0 });
221
+ const order = rawOrder === void 0 ? void 0 : decodeOrder(table, parseJson(rawOrder, "order"), "order");
222
+ const { select, include } = decodeProjection(table, params);
223
+ const limit = decodeIntParam(params.get("limit"), "limit", DEFAULT_LIMIT, MAX_LIMIT);
224
+ const offset = decodeIntParam(params.get("offset"), "offset", 0, MAX_OFFSET);
225
+ assertReadBudget(table, include, limit);
226
+ return {
227
+ where,
228
+ order,
229
+ select,
230
+ include,
231
+ limit,
232
+ offset
233
+ };
234
+ }
235
+ /** Decode `GET /:table/:id`, which supports projection and includes only. */
236
+ function decodeDetailQuery(table, rawQuery) {
237
+ const decoded = decodeProjection(table, parseParams(rawQuery, DETAIL_PARAMS));
238
+ assertReadBudget(table, decoded.include, 1);
239
+ return decoded;
240
+ }
241
+
242
+ //#endregion
243
+ export { decodeDetailQuery, decodeListQuery };
244
+ //# sourceMappingURL=query.js.map