@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
package/CLAUDE.md CHANGED
@@ -93,7 +93,7 @@ npx @databricks/appkit docs <query>
93
93
  - [Function: createWorkspaceClient()](./docs/api/appkit/Function.createWorkspaceClient.md): Construct an AppKit workspace client.
94
94
  - [Function: database()](./docs/api/appkit/Function.database.md): Create a typed database plugin registration for a finalized schema.
95
95
  - [Function: defineManifest()](./docs/api/appkit/Function.defineManifest.md): Validates a raw manifest (typically a manifest.json import) against the
96
- - [Function: defineSchema()](./docs/api/appkit/Function.defineSchema.md): Parameters
96
+ - [Function: defineSchema()](./docs/api/appkit/Function.defineSchema.md): Compile one declared schema. The returned type keeps the table names the
97
97
  - [Function: defineTool()](./docs/api/appkit/Function.defineTool.md): Defines a single tool entry for a plugin's internal registry.
98
98
  - [Function: enumColumn()](./docs/api/appkit/Function.enumColumn.md): Parameters
99
99
  - [Function: executeFromRegistry()](./docs/api/appkit/Function.executeFromRegistry.md): Validates tool-call arguments against the entry's schema and invokes its
@@ -171,7 +171,7 @@ npx @databricks/appkit docs <query>
171
171
  - [Interface: ResourceRequirement](./docs/api/appkit/Interface.ResourceRequirement.md): Declares a resource requirement for a plugin.
172
172
  - [Interface: RunAgentInput](./docs/api/appkit/Interface.RunAgentInput.md): Properties
173
173
  - [Interface: RunAgentResult](./docs/api/appkit/Interface.RunAgentResult.md): Properties
174
- - [Interface: Schema](./docs/api/appkit/Interface.Schema.md): Properties
174
+ - [Interface: Schema<TTableName>](./docs/api/appkit/Interface.Schema.md): One finalized schema. TTableName keeps the declared names in the type, so
175
175
  - [Interface: SearchRequest](./docs/api/appkit/Interface.SearchRequest.md): Properties
176
176
  - [Interface: SearchResponse<T>](./docs/api/appkit/Interface.SearchResponse.md): Type Parameters
177
177
  - [Interface: SearchResult<T>](./docs/api/appkit/Interface.SearchResult.md): Type Parameters
@@ -1,6 +1,6 @@
1
1
  //#region package.json
2
2
  var name = "@databricks/appkit";
3
- var version = "0.67.0";
3
+ var version = "0.68.0";
4
4
 
5
5
  //#endregion
6
6
  export { name, version };
@@ -1,3 +1,3 @@
1
- import { DEFAULT_LIMIT, FILTER_OPERATORS, IN_CAP, MAX_INCLUDES, MAX_LIMIT, MAX_OFFSET, MAX_WHERE_CONDITIONS, MAX_WHERE_DEPTH, MAX_WHERE_GROUP_ITEMS, isFilterOperator } from "./wire.js";
1
+ import { DEFAULT_LIMIT, FILTER_OPERATORS, IN_CAP, MAX_INCLUDES, MAX_INCLUDE_DEPTH, MAX_INCLUDE_NODES, MAX_LIMIT, MAX_OFFSET, MAX_WHERE_CONDITIONS, MAX_WHERE_DEPTH, MAX_WHERE_GROUP_ITEMS, isFilterOperator } from "./wire.js";
2
2
 
3
3
  export { };
@@ -1 +1 @@
1
- {"version":3,"file":"wire.d.ts","names":[],"sources":["../../../src/database/contract/wire.ts"],"mappings":";;KAkBY,OAAA;;KAEA,cAAA"}
1
+ {"version":3,"file":"wire.d.ts","names":[],"sources":["../../../src/database/contract/wire.ts"],"mappings":";;KAsBY,OAAA;;KAEA,cAAA"}
@@ -15,6 +15,10 @@ const MAX_WHERE_DEPTH = 5;
15
15
  const MAX_WHERE_GROUP_ITEMS = 20;
16
16
  /** Max column conditions accepted across one runtime predicate tree. */
17
17
  const MAX_WHERE_CONDITIONS = 50;
18
+ /** Max number of relation edges one include path may traverse. */
19
+ const MAX_INCLUDE_DEPTH = 2;
20
+ /** Max number of relation nodes across a complete include tree. */
21
+ const MAX_INCLUDE_NODES = 25;
18
22
  /** Filter operators usable in the runtime WHERE translator and the `where` spec type. */
19
23
  const FILTER_OPERATORS = Object.freeze([
20
24
  "eq",
@@ -33,5 +37,5 @@ function isFilterOperator(token) {
33
37
  }
34
38
 
35
39
  //#endregion
36
- export { DEFAULT_LIMIT, FILTER_OPERATORS, IN_CAP, MAX_INCLUDES, MAX_LIMIT, MAX_OFFSET, MAX_WHERE_CONDITIONS, MAX_WHERE_DEPTH, MAX_WHERE_GROUP_ITEMS, isFilterOperator };
40
+ export { DEFAULT_LIMIT, FILTER_OPERATORS, IN_CAP, MAX_INCLUDES, MAX_INCLUDE_DEPTH, MAX_INCLUDE_NODES, MAX_LIMIT, MAX_OFFSET, MAX_WHERE_CONDITIONS, MAX_WHERE_DEPTH, MAX_WHERE_GROUP_ITEMS, isFilterOperator };
37
41
  //# sourceMappingURL=wire.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"wire.js","names":[],"sources":["../../../src/database/contract/wire.ts"],"sourcesContent":["/** Max number of values allowed in an `in.(…)` list. */\nexport const IN_CAP = 100;\n/** Hard ceiling for a runtime query limit. */\nexport const MAX_LIMIT = 500;\n/** Hard ceiling for a runtime query offset. OFFSET scans are unbounded in cost. */\nexport const MAX_OFFSET = 10_000;\n/** Default page size when no `.limit()` is supplied. */\nexport const DEFAULT_LIMIT = 50;\n/** Max number of relations resolvable in a single `.include()`. */\nexport const MAX_INCLUDES = 10;\n/** Max nesting depth of `and`/`or` groups in one runtime predicate. */\nexport const MAX_WHERE_DEPTH = 5;\n/** Max members accepted by one runtime `and`/`or` group. */\nexport const MAX_WHERE_GROUP_ITEMS = 20;\n/** Max column conditions accepted across one runtime predicate tree. */\nexport const MAX_WHERE_CONDITIONS = 50;\n\n/** Scalar values accepted by primary-key operations. */\nexport type IdValue = string | number | bigint;\n/** Ordering accepted by typed clients and the runtime adapter. */\nexport type OrderDirection = \"asc\" | \"desc\";\n/** Filter operators usable in the runtime WHERE translator and the `where` spec type. */\nexport const FILTER_OPERATORS = Object.freeze([\n \"eq\",\n \"neq\",\n \"gt\",\n \"gte\",\n \"lt\",\n \"lte\",\n \"like\",\n \"ilike\",\n \"in\",\n \"is\",\n] as const);\n\nexport type FilterOperator = (typeof FILTER_OPERATORS)[number];\n\nexport function isFilterOperator(token: string): token is FilterOperator {\n return (FILTER_OPERATORS as readonly string[]).includes(token);\n}\n"],"mappings":";;AACA,MAAa,SAAS;;AAEtB,MAAa,YAAY;;AAEzB,MAAa,aAAa;;AAE1B,MAAa,gBAAgB;;AAE7B,MAAa,eAAe;;AAE5B,MAAa,kBAAkB;;AAE/B,MAAa,wBAAwB;;AAErC,MAAa,uBAAuB;;AAOpC,MAAa,mBAAmB,OAAO,OAAO;CAC5C;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACD,CAAU;AAIX,SAAgB,iBAAiB,OAAwC;AACvE,QAAQ,iBAAuC,SAAS,MAAM"}
1
+ {"version":3,"file":"wire.js","names":[],"sources":["../../../src/database/contract/wire.ts"],"sourcesContent":["/** Max number of values allowed in an `in.(…)` list. */\nexport const IN_CAP = 100;\n/** Hard ceiling for a runtime query limit. */\nexport const MAX_LIMIT = 500;\n/** Hard ceiling for a runtime query offset. OFFSET scans are unbounded in cost. */\nexport const MAX_OFFSET = 10_000;\n/** Default page size when no `.limit()` is supplied. */\nexport const DEFAULT_LIMIT = 50;\n/** Max number of relations resolvable in a single `.include()`. */\nexport const MAX_INCLUDES = 10;\n/** Max nesting depth of `and`/`or` groups in one runtime predicate. */\nexport const MAX_WHERE_DEPTH = 5;\n/** Max members accepted by one runtime `and`/`or` group. */\nexport const MAX_WHERE_GROUP_ITEMS = 20;\n/** Max column conditions accepted across one runtime predicate tree. */\nexport const MAX_WHERE_CONDITIONS = 50;\n/** Max number of relation edges one include path may traverse. */\nexport const MAX_INCLUDE_DEPTH = 2;\n/** Max number of relation nodes across a complete include tree. */\nexport const MAX_INCLUDE_NODES = 25;\n\n/** Scalar values accepted by primary-key operations. */\nexport type IdValue = string | number | bigint;\n/** Ordering accepted by typed clients and the runtime adapter. */\nexport type OrderDirection = \"asc\" | \"desc\";\n/** Filter operators usable in the runtime WHERE translator and the `where` spec type. */\nexport const FILTER_OPERATORS = Object.freeze([\n \"eq\",\n \"neq\",\n \"gt\",\n \"gte\",\n \"lt\",\n \"lte\",\n \"like\",\n \"ilike\",\n \"in\",\n \"is\",\n] as const);\n\nexport type FilterOperator = (typeof FILTER_OPERATORS)[number];\n\nexport function isFilterOperator(token: string): token is FilterOperator {\n return (FILTER_OPERATORS as readonly string[]).includes(token);\n}\n"],"mappings":";;AACA,MAAa,SAAS;;AAEtB,MAAa,YAAY;;AAEzB,MAAa,aAAa;;AAE1B,MAAa,gBAAgB;;AAE7B,MAAa,eAAe;;AAE5B,MAAa,kBAAkB;;AAE/B,MAAa,wBAAwB;;AAErC,MAAa,uBAAuB;;AAEpC,MAAa,oBAAoB;;AAEjC,MAAa,oBAAoB;;AAOjC,MAAa,mBAAmB,OAAO,OAAO;CAC5C;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACD,CAAU;AAIX,SAAgB,iBAAiB,OAAwC;AACvE,QAAQ,iBAAuC,SAAS,MAAM"}
@@ -9,6 +9,10 @@ const definitions = {
9
9
  message: "Invalid database request",
10
10
  statusCode: 400
11
11
  },
12
+ NOT_FOUND: {
13
+ message: "Database record not found",
14
+ statusCode: 404
15
+ },
12
16
  CONFLICT: {
13
17
  message: "Database conflict",
14
18
  statusCode: 409
@@ -21,6 +25,10 @@ const definitions = {
21
25
  message: "Database operation temporarily unavailable",
22
26
  statusCode: 503
23
27
  },
28
+ PAYLOAD_TOO_LARGE: {
29
+ message: "Database response is too large",
30
+ statusCode: 413
31
+ },
24
32
  INTERNAL: {
25
33
  message: "Database operation failed",
26
34
  statusCode: 500
@@ -33,7 +41,9 @@ const definitions = {
33
41
  const categoryByStatus = {
34
42
  400: "INVALID_REQUEST",
35
43
  403: "FORBIDDEN",
44
+ 404: "NOT_FOUND",
36
45
  409: "CONFLICT",
46
+ 413: "PAYLOAD_TOO_LARGE",
37
47
  503: "TRANSIENT"
38
48
  };
39
49
  /** AppKit-facing database failure with stable metadata and no driver details. */
@@ -41,11 +51,12 @@ var DatabasePluginError = class extends AppKitError {
41
51
  code = "DATABASE_PLUGIN_ERROR";
42
52
  isRetryable;
43
53
  statusCode;
44
- constructor(category, phase, runtimeMessage) {
54
+ constructor(category, phase, runtimeMessage, details) {
45
55
  const definition = definitions[category];
46
56
  super(phase === "runtime" && runtimeMessage ? runtimeMessage : definition.message, { clientMessage: definition.message });
47
57
  this.category = category;
48
58
  this.phase = phase;
59
+ this.details = details;
49
60
  this.statusCode = definition.statusCode;
50
61
  this.isRetryable = category === "TRANSIENT";
51
62
  this.name = "DatabasePluginError";
@@ -55,10 +66,35 @@ var DatabasePluginError = class extends AppKitError {
55
66
  function invalidDatabaseRequest(runtimeMessage) {
56
67
  return new DatabasePluginError("INVALID_REQUEST", "runtime", runtimeMessage);
57
68
  }
69
+ /** Refuse to publish a plugin whose configuration cannot be honored. */
70
+ function databaseSetupFailed() {
71
+ return new DatabasePluginError("SETUP_FAILED", "setup");
72
+ }
73
+ /** Reject untrusted request input, naming the field but never its value. */
74
+ function invalidDatabaseInput(path, message) {
75
+ return new DatabasePluginError("INVALID_REQUEST", "read", void 0, [{
76
+ path,
77
+ message
78
+ }]);
79
+ }
80
+ /**
81
+ * Name an unknown failure without its payload. A driver error carries the SQL
82
+ * text and its bound row values (for example a `DrizzleQueryError` retains
83
+ * `query` and `params`), so only constructor names walk into the log.
84
+ */
85
+ function describeUnclassifiedError(error) {
86
+ const names = [];
87
+ let current = error;
88
+ for (let depth = 0; depth < 5 && current instanceof Error; depth++) {
89
+ names.push(current.name);
90
+ current = current.cause;
91
+ }
92
+ return names.length > 0 ? names.join(" <- ") : typeof error;
93
+ }
58
94
  /** Add operation context without retaining an unknown error's details. */
59
95
  function classifyDatabaseError(error, phase) {
60
96
  if (error instanceof DatabasePluginError) return error.phase === phase ? error : new DatabasePluginError(error.category, phase);
61
- logger.error("Unclassified database error during %s: %O", phase, error);
97
+ logger.error("Unclassified database error during %s (%s)", phase, describeUnclassifiedError(error));
62
98
  return new DatabasePluginError("INTERNAL", phase);
63
99
  }
64
100
  /** Restore the safe database category carried through `Plugin.execute()`. */
@@ -67,5 +103,5 @@ function databaseErrorFromStatus(status, phase) {
67
103
  }
68
104
 
69
105
  //#endregion
70
- export { DatabasePluginError, classifyDatabaseError, databaseErrorFromStatus, invalidDatabaseRequest };
106
+ export { DatabasePluginError, classifyDatabaseError, databaseErrorFromStatus, databaseSetupFailed, invalidDatabaseInput, invalidDatabaseRequest };
71
107
  //# sourceMappingURL=errors.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"errors.js","names":[],"sources":["../../src/database/errors.ts"],"sourcesContent":["import { AppKitError } from \"../errors\";\nimport { createLogger } from \"../logging/logger\";\n\nconst logger = createLogger(\"database\");\n\nexport type DatabaseErrorCategory =\n | \"INVALID_REQUEST\"\n | \"CONFLICT\"\n | \"FORBIDDEN\"\n | \"TRANSIENT\"\n | \"INTERNAL\"\n | \"SETUP_FAILED\";\n\ntype DatabaseErrorPhase =\n | \"setup\"\n | \"shutdown\"\n | \"read\"\n | \"write\"\n | \"transaction\"\n | \"runtime\";\n\nconst definitions: Record<\n DatabaseErrorCategory,\n { readonly message: string; readonly statusCode: number }\n> = {\n INVALID_REQUEST: { message: \"Invalid database request\", statusCode: 400 },\n CONFLICT: { message: \"Database conflict\", statusCode: 409 },\n FORBIDDEN: { message: \"Database operation forbidden\", statusCode: 403 },\n TRANSIENT: {\n message: \"Database operation temporarily unavailable\",\n statusCode: 503,\n },\n INTERNAL: { message: \"Database operation failed\", statusCode: 500 },\n SETUP_FAILED: { message: \"Database setup failed\", statusCode: 500 },\n};\n\nconst categoryByStatus: Readonly<Record<number, DatabaseErrorCategory>> = {\n 400: \"INVALID_REQUEST\",\n 403: \"FORBIDDEN\",\n 409: \"CONFLICT\",\n 503: \"TRANSIENT\",\n};\n\n/** AppKit-facing database failure with stable metadata and no driver details. */\nexport class DatabasePluginError extends AppKitError {\n readonly code = \"DATABASE_PLUGIN_ERROR\";\n readonly isRetryable: boolean;\n readonly statusCode: number;\n\n constructor(\n readonly category: DatabaseErrorCategory,\n readonly phase: DatabaseErrorPhase,\n runtimeMessage?: string,\n ) {\n const definition = definitions[category];\n // Plugin boundaries replace runtime diagnostics with the stable message.\n super(\n phase === \"runtime\" && runtimeMessage\n ? runtimeMessage\n : definition.message,\n {\n clientMessage: definition.message,\n },\n );\n this.statusCode = definition.statusCode;\n this.isRetryable = category === \"TRANSIENT\";\n this.name = \"DatabasePluginError\";\n }\n}\n\n/** Keep runtime diagnostics internal until a plugin boundary classifies them. */\nexport function invalidDatabaseRequest(\n runtimeMessage?: string,\n): DatabasePluginError {\n return new DatabasePluginError(\"INVALID_REQUEST\", \"runtime\", runtimeMessage);\n}\n\n/** Add operation context without retaining an unknown error's details. */\nexport function classifyDatabaseError(\n error: unknown,\n phase: DatabaseErrorPhase,\n): DatabasePluginError {\n if (error instanceof DatabasePluginError) {\n return error.phase === phase\n ? error\n : new DatabasePluginError(error.category, phase);\n }\n logger.error(\"Unclassified database error during %s: %O\", phase, error);\n return new DatabasePluginError(\"INTERNAL\", phase);\n}\n\n/** Restore the safe database category carried through `Plugin.execute()`. */\nexport function databaseErrorFromStatus(\n status: number,\n phase: DatabaseErrorPhase,\n): DatabasePluginError {\n const category = categoryByStatus[status] ?? \"INTERNAL\";\n return new DatabasePluginError(category, phase);\n}\n"],"mappings":";;;;;AAGA,MAAM,SAAS,aAAa,WAAW;AAkBvC,MAAM,cAGF;CACF,iBAAiB;EAAE,SAAS;EAA4B,YAAY;EAAK;CACzE,UAAU;EAAE,SAAS;EAAqB,YAAY;EAAK;CAC3D,WAAW;EAAE,SAAS;EAAgC,YAAY;EAAK;CACvE,WAAW;EACT,SAAS;EACT,YAAY;EACb;CACD,UAAU;EAAE,SAAS;EAA6B,YAAY;EAAK;CACnE,cAAc;EAAE,SAAS;EAAyB,YAAY;EAAK;CACpE;AAED,MAAM,mBAAoE;CACxE,KAAK;CACL,KAAK;CACL,KAAK;CACL,KAAK;CACN;;AAGD,IAAa,sBAAb,cAAyC,YAAY;CACnD,AAAS,OAAO;CAChB,AAAS;CACT,AAAS;CAET,YACE,AAAS,UACT,AAAS,OACT,gBACA;EACA,MAAM,aAAa,YAAY;AAE/B,QACE,UAAU,aAAa,iBACnB,iBACA,WAAW,SACf,EACE,eAAe,WAAW,SAC3B,CACF;EAbQ;EACA;AAaT,OAAK,aAAa,WAAW;AAC7B,OAAK,cAAc,aAAa;AAChC,OAAK,OAAO;;;;AAKhB,SAAgB,uBACd,gBACqB;AACrB,QAAO,IAAI,oBAAoB,mBAAmB,WAAW,eAAe;;;AAI9E,SAAgB,sBACd,OACA,OACqB;AACrB,KAAI,iBAAiB,oBACnB,QAAO,MAAM,UAAU,QACnB,QACA,IAAI,oBAAoB,MAAM,UAAU,MAAM;AAEpD,QAAO,MAAM,6CAA6C,OAAO,MAAM;AACvE,QAAO,IAAI,oBAAoB,YAAY,MAAM;;;AAInD,SAAgB,wBACd,QACA,OACqB;AAErB,QAAO,IAAI,oBADM,iBAAiB,WAAW,YACJ,MAAM"}
1
+ {"version":3,"file":"errors.js","names":[],"sources":["../../src/database/errors.ts"],"sourcesContent":["import { AppKitError } from \"../errors\";\nimport { createLogger } from \"../logging/logger\";\n\nconst logger = createLogger(\"database\");\n\nexport type DatabaseErrorCategory =\n | \"INVALID_REQUEST\"\n | \"NOT_FOUND\"\n | \"CONFLICT\"\n | \"FORBIDDEN\"\n | \"TRANSIENT\"\n | \"PAYLOAD_TOO_LARGE\"\n | \"INTERNAL\"\n | \"SETUP_FAILED\";\n\n/** Which request field a rejection concerns; it never carries caller values. */\nexport interface DatabaseErrorDetail {\n readonly path: readonly string[];\n readonly message: string;\n}\n\ntype DatabaseErrorPhase =\n | \"setup\"\n | \"shutdown\"\n | \"read\"\n | \"write\"\n | \"transaction\"\n | \"runtime\";\n\nconst definitions: Record<\n DatabaseErrorCategory,\n { readonly message: string; readonly statusCode: number }\n> = {\n INVALID_REQUEST: { message: \"Invalid database request\", statusCode: 400 },\n NOT_FOUND: { message: \"Database record not found\", statusCode: 404 },\n CONFLICT: { message: \"Database conflict\", statusCode: 409 },\n FORBIDDEN: { message: \"Database operation forbidden\", statusCode: 403 },\n TRANSIENT: {\n message: \"Database operation temporarily unavailable\",\n statusCode: 503,\n },\n PAYLOAD_TOO_LARGE: {\n message: \"Database response is too large\",\n statusCode: 413,\n },\n INTERNAL: { message: \"Database operation failed\", statusCode: 500 },\n SETUP_FAILED: { message: \"Database setup failed\", statusCode: 500 },\n};\n\nconst categoryByStatus: Readonly<Record<number, DatabaseErrorCategory>> = {\n 400: \"INVALID_REQUEST\",\n 403: \"FORBIDDEN\",\n 404: \"NOT_FOUND\",\n 409: \"CONFLICT\",\n 413: \"PAYLOAD_TOO_LARGE\",\n 503: \"TRANSIENT\",\n};\n\n/** AppKit-facing database failure with stable metadata and no driver details. */\nexport class DatabasePluginError extends AppKitError {\n readonly code = \"DATABASE_PLUGIN_ERROR\";\n readonly isRetryable: boolean;\n readonly statusCode: number;\n\n constructor(\n readonly category: DatabaseErrorCategory,\n readonly phase: DatabaseErrorPhase,\n runtimeMessage?: string,\n readonly details?: readonly DatabaseErrorDetail[],\n ) {\n const definition = definitions[category];\n // Plugin boundaries replace runtime diagnostics with the stable message.\n super(\n phase === \"runtime\" && runtimeMessage\n ? runtimeMessage\n : definition.message,\n {\n clientMessage: definition.message,\n },\n );\n this.statusCode = definition.statusCode;\n this.isRetryable = category === \"TRANSIENT\";\n this.name = \"DatabasePluginError\";\n }\n}\n\n/** Keep runtime diagnostics internal until a plugin boundary classifies them. */\nexport function invalidDatabaseRequest(\n runtimeMessage?: string,\n): DatabasePluginError {\n return new DatabasePluginError(\"INVALID_REQUEST\", \"runtime\", runtimeMessage);\n}\n\n/** Refuse to publish a plugin whose configuration cannot be honored. */\nexport function databaseSetupFailed(): DatabasePluginError {\n return new DatabasePluginError(\"SETUP_FAILED\", \"setup\");\n}\n\n/** Reject untrusted request input, naming the field but never its value. */\nexport function invalidDatabaseInput(\n path: readonly string[],\n message: string,\n): DatabasePluginError {\n return new DatabasePluginError(\"INVALID_REQUEST\", \"read\", undefined, [\n { path, message },\n ]);\n}\n\n/**\n * Name an unknown failure without its payload. A driver error carries the SQL\n * text and its bound row values (for example a `DrizzleQueryError` retains\n * `query` and `params`), so only constructor names walk into the log.\n */\nfunction describeUnclassifiedError(error: unknown): string {\n const names: string[] = [];\n let current: unknown = error;\n for (let depth = 0; depth < 5 && current instanceof Error; depth++) {\n names.push(current.name);\n current = current.cause;\n }\n return names.length > 0 ? names.join(\" <- \") : typeof error;\n}\n\n/** Add operation context without retaining an unknown error's details. */\nexport function classifyDatabaseError(\n error: unknown,\n phase: DatabaseErrorPhase,\n): DatabasePluginError {\n if (error instanceof DatabasePluginError) {\n return error.phase === phase\n ? error\n : new DatabasePluginError(error.category, phase);\n }\n logger.error(\n \"Unclassified database error during %s (%s)\",\n phase,\n describeUnclassifiedError(error),\n );\n return new DatabasePluginError(\"INTERNAL\", phase);\n}\n\n/** Restore the safe database category carried through `Plugin.execute()`. */\nexport function databaseErrorFromStatus(\n status: number,\n phase: DatabaseErrorPhase,\n): DatabasePluginError {\n const category = categoryByStatus[status] ?? \"INTERNAL\";\n return new DatabasePluginError(category, phase);\n}\n"],"mappings":";;;;;AAGA,MAAM,SAAS,aAAa,WAAW;AA0BvC,MAAM,cAGF;CACF,iBAAiB;EAAE,SAAS;EAA4B,YAAY;EAAK;CACzE,WAAW;EAAE,SAAS;EAA6B,YAAY;EAAK;CACpE,UAAU;EAAE,SAAS;EAAqB,YAAY;EAAK;CAC3D,WAAW;EAAE,SAAS;EAAgC,YAAY;EAAK;CACvE,WAAW;EACT,SAAS;EACT,YAAY;EACb;CACD,mBAAmB;EACjB,SAAS;EACT,YAAY;EACb;CACD,UAAU;EAAE,SAAS;EAA6B,YAAY;EAAK;CACnE,cAAc;EAAE,SAAS;EAAyB,YAAY;EAAK;CACpE;AAED,MAAM,mBAAoE;CACxE,KAAK;CACL,KAAK;CACL,KAAK;CACL,KAAK;CACL,KAAK;CACL,KAAK;CACN;;AAGD,IAAa,sBAAb,cAAyC,YAAY;CACnD,AAAS,OAAO;CAChB,AAAS;CACT,AAAS;CAET,YACE,AAAS,UACT,AAAS,OACT,gBACA,AAAS,SACT;EACA,MAAM,aAAa,YAAY;AAE/B,QACE,UAAU,aAAa,iBACnB,iBACA,WAAW,SACf,EACE,eAAe,WAAW,SAC3B,CACF;EAdQ;EACA;EAEA;AAYT,OAAK,aAAa,WAAW;AAC7B,OAAK,cAAc,aAAa;AAChC,OAAK,OAAO;;;;AAKhB,SAAgB,uBACd,gBACqB;AACrB,QAAO,IAAI,oBAAoB,mBAAmB,WAAW,eAAe;;;AAI9E,SAAgB,sBAA2C;AACzD,QAAO,IAAI,oBAAoB,gBAAgB,QAAQ;;;AAIzD,SAAgB,qBACd,MACA,SACqB;AACrB,QAAO,IAAI,oBAAoB,mBAAmB,QAAQ,QAAW,CACnE;EAAE;EAAM;EAAS,CAClB,CAAC;;;;;;;AAQJ,SAAS,0BAA0B,OAAwB;CACzD,MAAM,QAAkB,EAAE;CAC1B,IAAI,UAAmB;AACvB,MAAK,IAAI,QAAQ,GAAG,QAAQ,KAAK,mBAAmB,OAAO,SAAS;AAClE,QAAM,KAAK,QAAQ,KAAK;AACxB,YAAU,QAAQ;;AAEpB,QAAO,MAAM,SAAS,IAAI,MAAM,KAAK,OAAO,GAAG,OAAO;;;AAIxD,SAAgB,sBACd,OACA,OACqB;AACrB,KAAI,iBAAiB,oBACnB,QAAO,MAAM,UAAU,QACnB,QACA,IAAI,oBAAoB,MAAM,UAAU,MAAM;AAEpD,QAAO,MACL,8CACA,OACA,0BAA0B,MAAM,CACjC;AACD,QAAO,IAAI,oBAAoB,YAAY,MAAM;;;AAInD,SAAgB,wBACd,QACA,OACqB;AAErB,QAAO,IAAI,oBADM,iBAAiB,WAAW,YACJ,MAAM"}
@@ -1 +1 @@
1
- {"version":3,"file":"data-path.js","names":[],"sources":["../../../src/database/runtime/data-path.ts"],"sourcesContent":["import {\n DEFAULT_LIMIT,\n type FilterOperator,\n type IdValue,\n MAX_LIMIT,\n MAX_OFFSET,\n type OrderDirection,\n} from \"../contract\";\nimport { invalidDatabaseRequest } from \"../errors\";\nimport type { AppKitTable, ColumnMeta } from \"../schema-builder\";\n\nexport type { IdValue, OrderDirection };\nexport type ScalarValue = string | number | bigint | boolean | null;\n/** Operators for one column; array operands are reserved for `in`. */\nexport type FilterOps = Partial<\n Record<FilterOperator, ScalarValue | readonly ScalarValue[]>\n>;\nexport type WhereValue = ScalarValue | readonly ScalarValue[] | FilterOps;\n/** Direct-column predicates with explicit `and` and `or` predicate groups. */\nexport type WhereClause = Readonly<\n Record<string, WhereValue | readonly WhereClause[]>\n>;\n\nexport type OrderSpec = Readonly<Record<string, OrderDirection>>;\n\nexport interface IncludeOptions {\n readonly select?: readonly string[];\n readonly where?: WhereClause;\n readonly order?: OrderSpec;\n readonly limit?: number;\n}\n\n/** Selection and bounds for one declared relation edge. */\nexport type IncludeSpec = Readonly<Record<string, boolean | IncludeOptions>>;\n\n/** A bounded root read; adapters apply defaults and validate explicit bounds. */\nexport interface QuerySpec {\n readonly where?: WhereClause;\n readonly order?: OrderSpec;\n readonly select?: readonly string[];\n readonly include?: IncludeSpec;\n readonly limit?: number;\n readonly offset?: number;\n}\n\nexport type Row = Record<string, unknown>;\n\n/** Combine predicates without making callers understand the wire shape. */\nexport function andWhere(\n existing: WhereClause | undefined,\n next: WhereClause,\n): WhereClause {\n return existing === undefined ? next : { and: [existing, next] };\n}\n\n/** Internal AppKit execution port; field names are schema-owned identifiers. */\nexport interface DataPath {\n /** Read a bounded collection from one finalized table. */\n select(table: AppKitTable, spec: QuerySpec): Promise<Row[]>;\n /** Read by the sole primary key while preserving supported query state. */\n findOne(\n table: AppKitTable,\n id: IdValue,\n spec?: Pick<QuerySpec, \"where\" | \"select\" | \"include\">,\n ): Promise<Row | null>;\n count(table: AppKitTable, where?: WhereClause): Promise<number>;\n /** Return exactly one inserted row; zero or many is an invariant failure. */\n insert(table: AppKitTable, values: Row): Promise<Row>;\n /** Return null for zero updated rows and reject more than one. */\n update(table: AppKitTable, id: IdValue, values: Row): Promise<Row | null>;\n /** Return exactly one row for a validated primary-key or unique conflict. */\n upsert(table: AppKitTable, values: Row, onConflict: string): Promise<Row>;\n /** Return false for zero deleted rows, true for one, and reject many. */\n delete(table: AppKitTable, id: IdValue): Promise<boolean>;\n /** Execute tagged SQL whose interpolations are parameter values, not SQL. */\n raw<T = Row>(\n strings: TemplateStringsArray,\n ...values: unknown[]\n ): Promise<T[]>;\n /** Run the callback with one transaction-bound DataPath. */\n transaction<T>(callback: (tx: DataPath) => Promise<T>): Promise<T>;\n}\n\n/** Validate an explicit root or relation row limit. */\nexport function validateLimit(limit: number): number {\n if (!Number.isInteger(limit) || limit < 0 || limit > MAX_LIMIT) {\n throw invalidDatabaseRequest(\n `limit must be an integer between 0 and ${MAX_LIMIT}`,\n );\n }\n return limit;\n}\n\n/** Apply the conservative collection default when no limit is supplied. */\nexport function limitOrDefault(limit?: number): number {\n return limit === undefined ? DEFAULT_LIMIT : validateLimit(limit);\n}\n\n/** Validate an explicit root or relation row offset. */\nexport function validateOffset(offset: number): number {\n if (!Number.isSafeInteger(offset) || offset < 0 || offset > MAX_OFFSET) {\n throw invalidDatabaseRequest(\n `offset must be an integer between 0 and ${MAX_OFFSET}`,\n );\n }\n return offset;\n}\n\n/** Resolve the sole primary key required by keyed operations. */\nexport function primaryKeyMeta(table: AppKitTable): ColumnMeta {\n const primaryKeys = Object.values(table.$columns).filter(\n (column) => column.primaryKey,\n );\n if (primaryKeys.length !== 1) {\n throw invalidDatabaseRequest(`Table \"${table.$name}\" has no primary key`);\n }\n return primaryKeys[0];\n}\n\n/** Resolve an upsert target that PostgreSQL can use for conflict detection. */\nexport function conflictTargetMeta(\n table: AppKitTable,\n columnName: string,\n): ColumnMeta {\n const column = table.$columns[columnName];\n if (!column || (!column.primaryKey && !column.unique)) {\n throw invalidDatabaseRequest(\n `Column \"${table.$name}.${columnName}\" is not a conflict target`,\n );\n }\n return column;\n}\n"],"mappings":";;;;;;AAgDA,SAAgB,SACd,UACA,MACa;AACb,QAAO,aAAa,SAAY,OAAO,EAAE,KAAK,CAAC,UAAU,KAAK,EAAE;;;AAgClE,SAAgB,cAAc,OAAuB;AACnD,KAAI,CAAC,OAAO,UAAU,MAAM,IAAI,QAAQ,KAAK,QAAQ,UACnD,OAAM,uBACJ,0CAA0C,YAC3C;AAEH,QAAO;;;AAIT,SAAgB,eAAe,OAAwB;AACrD,QAAO,UAAU,SAAY,gBAAgB,cAAc,MAAM;;;AAInE,SAAgB,eAAe,QAAwB;AACrD,KAAI,CAAC,OAAO,cAAc,OAAO,IAAI,SAAS,KAAK,SAAS,WAC1D,OAAM,uBACJ,2CAA2C,aAC5C;AAEH,QAAO;;;AAIT,SAAgB,eAAe,OAAgC;CAC7D,MAAM,cAAc,OAAO,OAAO,MAAM,SAAS,CAAC,QAC/C,WAAW,OAAO,WACpB;AACD,KAAI,YAAY,WAAW,EACzB,OAAM,uBAAuB,UAAU,MAAM,MAAM,sBAAsB;AAE3E,QAAO,YAAY;;;AAIrB,SAAgB,mBACd,OACA,YACY;CACZ,MAAM,SAAS,MAAM,SAAS;AAC9B,KAAI,CAAC,UAAW,CAAC,OAAO,cAAc,CAAC,OAAO,OAC5C,OAAM,uBACJ,WAAW,MAAM,MAAM,GAAG,WAAW,4BACtC;AAEH,QAAO"}
1
+ {"version":3,"file":"data-path.js","names":[],"sources":["../../../src/database/runtime/data-path.ts"],"sourcesContent":["import {\n DEFAULT_LIMIT,\n type FilterOperator,\n type IdValue,\n MAX_LIMIT,\n MAX_OFFSET,\n type OrderDirection,\n} from \"../contract\";\nimport { invalidDatabaseRequest } from \"../errors\";\nimport type { AppKitTable, ColumnMeta } from \"../schema-builder\";\n\nexport type { IdValue, OrderDirection };\nexport type ScalarValue = string | number | bigint | boolean | null;\n/** Operators for one column; array operands are reserved for `in`. */\nexport type FilterOps = Partial<\n Record<FilterOperator, ScalarValue | readonly ScalarValue[]>\n>;\nexport type WhereValue = ScalarValue | readonly ScalarValue[] | FilterOps;\n/** Direct-column predicates with explicit `and` and `or` predicate groups. */\nexport type WhereClause = Readonly<\n Record<string, WhereValue | readonly WhereClause[]>\n>;\n\nexport type OrderSpec = Readonly<Record<string, OrderDirection>>;\n\nexport interface IncludeOptions {\n readonly select?: readonly string[];\n readonly where?: WhereClause;\n readonly order?: OrderSpec;\n readonly limit?: number;\n readonly include?: IncludeSpec;\n}\n\n/** Selection and bounds for one declared relation edge. */\nexport type IncludeSpec = Readonly<Record<string, boolean | IncludeOptions>>;\n\n/** A bounded root read; adapters apply defaults and validate explicit bounds. */\nexport interface QuerySpec {\n readonly where?: WhereClause;\n readonly order?: OrderSpec;\n readonly select?: readonly string[];\n readonly include?: IncludeSpec;\n readonly limit?: number;\n readonly offset?: number;\n}\n\nexport type Row = Record<string, unknown>;\n\n/** Combine predicates without making callers understand the wire shape. */\nexport function andWhere(\n existing: WhereClause | undefined,\n next: WhereClause,\n): WhereClause {\n return existing === undefined ? next : { and: [existing, next] };\n}\n\n/** Internal AppKit execution port; field names are schema-owned identifiers. */\nexport interface DataPath {\n /** Read a bounded collection from one finalized table. */\n select(table: AppKitTable, spec: QuerySpec): Promise<Row[]>;\n /** Read by the sole primary key while preserving supported query state. */\n findOne(\n table: AppKitTable,\n id: IdValue,\n spec?: Pick<QuerySpec, \"where\" | \"select\" | \"include\">,\n ): Promise<Row | null>;\n count(table: AppKitTable, where?: WhereClause): Promise<number>;\n /** Return exactly one inserted row; zero or many is an invariant failure. */\n insert(table: AppKitTable, values: Row): Promise<Row>;\n /** Return null for zero updated rows and reject more than one. */\n update(table: AppKitTable, id: IdValue, values: Row): Promise<Row | null>;\n /** Return exactly one row for a validated primary-key or unique conflict. */\n upsert(table: AppKitTable, values: Row, onConflict: string): Promise<Row>;\n /** Return false for zero deleted rows, true for one, and reject many. */\n delete(table: AppKitTable, id: IdValue): Promise<boolean>;\n /** Execute tagged SQL whose interpolations are parameter values, not SQL. */\n raw<T = Row>(\n strings: TemplateStringsArray,\n ...values: unknown[]\n ): Promise<T[]>;\n /** Run the callback with one transaction-bound DataPath. */\n transaction<T>(callback: (tx: DataPath) => Promise<T>): Promise<T>;\n}\n\n/** Validate an explicit root or relation row limit. */\nexport function validateLimit(limit: number): number {\n if (!Number.isInteger(limit) || limit < 0 || limit > MAX_LIMIT) {\n throw invalidDatabaseRequest(\n `limit must be an integer between 0 and ${MAX_LIMIT}`,\n );\n }\n return limit;\n}\n\n/** Apply the conservative collection default when no limit is supplied. */\nexport function limitOrDefault(limit?: number): number {\n return limit === undefined ? DEFAULT_LIMIT : validateLimit(limit);\n}\n\n/** Validate an explicit root or relation row offset. */\nexport function validateOffset(offset: number): number {\n if (!Number.isSafeInteger(offset) || offset < 0 || offset > MAX_OFFSET) {\n throw invalidDatabaseRequest(\n `offset must be an integer between 0 and ${MAX_OFFSET}`,\n );\n }\n return offset;\n}\n\n/** Resolve the sole primary key required by keyed operations. */\nexport function primaryKeyMeta(table: AppKitTable): ColumnMeta {\n const primaryKeys = Object.values(table.$columns).filter(\n (column) => column.primaryKey,\n );\n if (primaryKeys.length !== 1) {\n throw invalidDatabaseRequest(`Table \"${table.$name}\" has no primary key`);\n }\n return primaryKeys[0];\n}\n\n/** Resolve an upsert target that PostgreSQL can use for conflict detection. */\nexport function conflictTargetMeta(\n table: AppKitTable,\n columnName: string,\n): ColumnMeta {\n const column = table.$columns[columnName];\n if (!column || (!column.primaryKey && !column.unique)) {\n throw invalidDatabaseRequest(\n `Column \"${table.$name}.${columnName}\" is not a conflict target`,\n );\n }\n return column;\n}\n"],"mappings":";;;;;;AAiDA,SAAgB,SACd,UACA,MACa;AACb,QAAO,aAAa,SAAY,OAAO,EAAE,KAAK,CAAC,UAAU,KAAK,EAAE;;;AAgClE,SAAgB,cAAc,OAAuB;AACnD,KAAI,CAAC,OAAO,UAAU,MAAM,IAAI,QAAQ,KAAK,QAAQ,UACnD,OAAM,uBACJ,0CAA0C,YAC3C;AAEH,QAAO;;;AAIT,SAAgB,eAAe,OAAwB;AACrD,QAAO,UAAU,SAAY,gBAAgB,cAAc,MAAM;;;AAInE,SAAgB,eAAe,QAAwB;AACrD,KAAI,CAAC,OAAO,cAAc,OAAO,IAAI,SAAS,KAAK,SAAS,WAC1D,OAAM,uBACJ,2CAA2C,aAC5C;AAEH,QAAO;;;AAIT,SAAgB,eAAe,OAAgC;CAC7D,MAAM,cAAc,OAAO,OAAO,MAAM,SAAS,CAAC,QAC/C,WAAW,OAAO,WACpB;AACD,KAAI,YAAY,WAAW,EACzB,OAAM,uBAAuB,UAAU,MAAM,MAAM,sBAAsB;AAE3E,QAAO,YAAY;;;AAIrB,SAAgB,mBACd,OACA,YACY;CACZ,MAAM,SAAS,MAAM,SAAS;AAC9B,KAAI,CAAC,UAAW,CAAC,OAAO,cAAc,CAAC,OAAO,OAC5C,OAAM,uBACJ,WAAW,MAAM,MAAM,GAAG,WAAW,4BACtC;AAEH,QAAO"}
@@ -65,7 +65,7 @@ function sqlStateOf(error) {
65
65
  /** Classify SQLSTATE without retaining the driver error or its properties. */
66
66
  function classifyDriverError(error) {
67
67
  const code = sqlStateOf(error);
68
- const category = code === "40001" || code === "40P01" ? "TRANSIENT" : code === "42501" ? "FORBIDDEN" : code?.startsWith("23") ? "CONFLICT" : "INTERNAL";
68
+ const category = code === "40001" || code === "40P01" || code === "57014" ? "TRANSIENT" : code === "42501" ? "FORBIDDEN" : code?.startsWith("23") ? "CONFLICT" : "INTERNAL";
69
69
  logger.error("Database driver error classified as %s (SQLSTATE %s)", category, code ?? "unknown");
70
70
  return new DatabasePluginError(category, "runtime");
71
71
  }
@@ -1 +1 @@
1
- {"version":3,"file":"drizzle-data-path.js","names":[],"sources":["../../../../src/database/runtime/engine/drizzle-data-path.ts"],"sourcesContent":["import { and, eq, isSQLWrapper, type SQL, sql } from \"drizzle-orm\";\nimport { drizzle, type NodePgDatabase } from \"drizzle-orm/node-postgres\";\nimport type { PgTable } from \"drizzle-orm/pg-core\";\nimport type { Pool } from \"pg\";\n\nimport { createLogger } from \"../../../logging/logger\";\nimport {\n type DatabaseErrorCategory,\n DatabasePluginError,\n invalidDatabaseRequest,\n} from \"../../errors\";\nimport type { AppKitTable, ColumnMeta, Schema } from \"../../schema-builder\";\nimport { buildEngineRelations } from \"../../schema-builder/engine/relations\";\nimport { columnValueSchema } from \"../../schema-builder/validators\";\nimport {\n conflictTargetMeta,\n type DataPath,\n type IdValue,\n limitOrDefault,\n primaryKeyMeta,\n type Row,\n validateOffset,\n type WhereClause,\n} from \"../data-path\";\nimport {\n columnOf,\n type ColumnAccess,\n defaultColumns,\n publicColumnNames,\n returningColumns,\n selectToColumns,\n translateInclude,\n translateOrder,\n translateWhere,\n} from \"./translate\";\n\nconst logger = createLogger(\"database\");\n\n/** Concrete Drizzle seam shared by the adapter and its focused tests. */\nexport type DrizzleDb = NodePgDatabase<Record<string, never>>;\n\nexport interface DrizzleDataPathOptions {\n /** Explicit capability for trusted server code that must read private data. */\n readonly columnAccess?: ColumnAccess;\n}\n\n/** Bind finalized AppKit metadata to a relational Drizzle database. */\nexport function createDrizzleDb(pool: Pool, schema: Schema): DrizzleDb {\n const completeSchema: Record<string, unknown> = Object.assign(\n Object.create(null),\n schema.$engine,\n buildEngineRelations(schema.$tables),\n );\n return drizzle(pool, { schema: completeSchema }) as unknown as DrizzleDb;\n}\n\ninterface RelationalQueryBuilder {\n findMany(config: Record<string, unknown>): Promise<Row[]>;\n findFirst(config: Record<string, unknown>): Promise<Row | undefined>;\n}\n\n/** Reject same-name or forged tables by requiring finalized object identity. */\nfunction assertRegisteredTable(schema: Schema, table: AppKitTable): void {\n if (schema.$tables[table.$name] !== table) {\n throw invalidDatabaseRequest(`Table \"${table.$name}\" is not registered`);\n }\n}\n\nfunction relationalQueryBuilder(\n db: DrizzleDb,\n schema: Schema,\n table: AppKitTable,\n): RelationalQueryBuilder {\n assertRegisteredTable(schema, table);\n const query = (db.query as unknown as Record<string, RelationalQueryBuilder>)[\n table.$name\n ];\n if (!query) {\n throw invalidDatabaseRequest(`Table \"${table.$name}\" is not registered`);\n }\n return query;\n}\n\nfunction selectedColumns(\n table: AppKitTable,\n select?: readonly string[],\n access: ColumnAccess = \"public\",\n): Record<string, true> {\n return select === undefined\n ? defaultColumns(table)\n : selectToColumns(table, select, access);\n}\n\ntype MutationKind = \"insert\" | \"update\";\n\ninterface MutationSchema {\n safeParse(\n value: unknown,\n ):\n | { readonly success: true; readonly data: unknown }\n | { readonly success: false };\n}\n\n/** Apply the finalized write schema again at the persistence boundary. */\nfunction mutationValues(\n table: AppKitTable,\n values: Row,\n kind: MutationKind,\n): Row {\n if (values === null || typeof values !== \"object\" || Array.isArray(values)) {\n throw invalidDatabaseRequest(\"Database mutation values must be an object\");\n }\n for (const [key, value] of Object.entries(values)) {\n if (!Object.hasOwn(table.$columns, key)) {\n throw invalidDatabaseRequest(`Unknown column \"${table.$name}.${key}\"`);\n }\n if (isSQLWrapper(value)) {\n throw invalidDatabaseRequest(\n \"Database mutation values cannot contain SQL\",\n );\n }\n }\n const schema = (\n kind === \"insert\" ? table.$insertSchema : table.$updateSchema\n ) as MutationSchema;\n const result = schema.safeParse(values);\n if (!result.success) {\n throw invalidDatabaseRequest(`Invalid database ${kind} values`);\n }\n return result.data as Row;\n}\n\n/** Never change row identity or the conflict key during the update half. */\nfunction upsertUpdateValues(\n table: AppKitTable,\n parameters: Row,\n conflictTarget: string,\n): Row {\n const updates: Row = {};\n for (const [key, value] of Object.entries(parameters)) {\n const column = table.$columns[key];\n if (\n column.primaryKey ||\n column.serverGenerated ||\n column.columnName === conflictTarget\n ) {\n continue;\n }\n updates[key] = value;\n }\n if (Object.keys(updates).length > 0) return updates;\n\n // A safe self-assignment preserves the exactly-one-row upsert contract.\n return { [conflictTarget]: columnOf(table, conflictTarget) };\n}\n\n// Drizzle wraps driver failures in DrizzleQueryError, so the SQLSTATE sits on a\n// nested `cause` rather than the thrown error. Walk a bounded chain to find it.\nconst MAX_CAUSE_DEPTH = 5;\n\nfunction sqlStateOf(error: unknown): string | undefined {\n let current = error;\n for (let depth = 0; depth < MAX_CAUSE_DEPTH; depth++) {\n if (!current || typeof current !== \"object\") return undefined;\n try {\n const candidate = Reflect.get(current, \"code\");\n // SQLSTATE is always a five-character alphanumeric class code.\n if (typeof candidate === \"string\" && /^[0-9A-Z]{5}$/.test(candidate)) {\n return candidate;\n }\n current = Reflect.get(current, \"cause\");\n } catch {\n return undefined;\n }\n }\n return undefined;\n}\n\n/** Classify SQLSTATE without retaining the driver error or its properties. */\nfunction classifyDriverError(error: unknown): DatabasePluginError {\n const code = sqlStateOf(error);\n const category: DatabaseErrorCategory =\n code === \"40001\" || code === \"40P01\"\n ? \"TRANSIENT\"\n : code === \"42501\"\n ? \"FORBIDDEN\"\n : code?.startsWith(\"23\")\n ? \"CONFLICT\"\n : \"INTERNAL\";\n logger.error(\n \"Database driver error classified as %s (SQLSTATE %s)\",\n category,\n code ?? \"unknown\",\n );\n return new DatabasePluginError(category, \"runtime\");\n}\n\nasync function runDatabaseOperation<T>(\n operation: () => Promise<T>,\n): Promise<T> {\n try {\n return await operation();\n } catch (error) {\n if (error instanceof DatabasePluginError) throw error;\n throw classifyDriverError(error);\n }\n}\n\n/** Apply the adapter's column capability before rows reach cardinality checks. */\nfunction mutationRows(\n table: AppKitTable,\n rows: Row[],\n access: ColumnAccess,\n): Row[] {\n if (access === \"trusted\") return rows;\n const names = publicColumnNames(table);\n return rows.map((row) => {\n const projected: Row = {};\n for (const name of names) {\n if (Object.hasOwn(row, name)) projected[name] = row[name];\n }\n return projected;\n });\n}\n\n/** Resolve and validate the sole key once before a keyed operation executes. */\nfunction validatedPrimaryKey(\n table: AppKitTable,\n id: unknown,\n): { readonly meta: ColumnMeta; readonly value: IdValue } {\n const meta = primaryKeyMeta(table);\n const result = columnValueSchema(meta).safeParse(id);\n if (!result.success) {\n throw invalidDatabaseRequest(\n `Invalid primary-key value for \"${table.$name}.${meta.columnName}\"`,\n );\n }\n return { meta, value: result.data as IdValue };\n}\n\n// Enforce the single-row DataPath contract before results reach callers.\nfunction expectExactlyOne(rows: Row[]): Row {\n if (rows.length !== 1) {\n throw new DatabasePluginError(\"INTERNAL\", \"runtime\");\n }\n return rows[0];\n}\n\nfunction expectZeroOrOne(rows: Row[]): Row | null {\n if (rows.length > 1) {\n throw new DatabasePluginError(\"INTERNAL\", \"runtime\");\n }\n return rows[0] ?? null;\n}\n\n/** Adapt a Drizzle database to AppKit's internal execution port. */\nexport function createDrizzleDataPath(\n db: DrizzleDb,\n schema: Schema,\n options: DrizzleDataPathOptions = {},\n): DataPath {\n const columnAccess = options.columnAccess ?? \"public\";\n const pgTable = (table: AppKitTable): PgTable => {\n assertRegisteredTable(schema, table);\n return table.$engine as unknown as PgTable;\n };\n const whereSql = (\n table: AppKitTable,\n where?: WhereClause,\n ): SQL | undefined =>\n where === undefined\n ? undefined\n : translateWhere(table, where, columnAccess);\n\n return {\n async select(table, spec) {\n return runDatabaseOperation(() =>\n relationalQueryBuilder(db, schema, table).findMany({\n where: whereSql(table, spec.where),\n orderBy:\n spec.order === undefined\n ? undefined\n : translateOrder(table, spec.order, columnAccess),\n columns: selectedColumns(table, spec.select, columnAccess),\n with:\n spec.include === undefined\n ? undefined\n : translateInclude(table, schema, spec.include, columnAccess),\n limit: limitOrDefault(spec.limit),\n offset:\n spec.offset === undefined ? undefined : validateOffset(spec.offset),\n }),\n );\n },\n\n async findOne(table, id, spec) {\n const { meta: primaryKey, value: validatedId } = validatedPrimaryKey(\n table,\n id,\n );\n const primaryKeyPredicate = eq(\n columnOf(table, primaryKey.columnName),\n validatedId,\n );\n const additionalPredicate =\n spec?.where === undefined\n ? undefined\n : translateWhere(table, spec.where, columnAccess);\n const row = await runDatabaseOperation(() =>\n relationalQueryBuilder(db, schema, table).findFirst({\n where:\n additionalPredicate === undefined\n ? primaryKeyPredicate\n : and(primaryKeyPredicate, additionalPredicate),\n columns: selectedColumns(table, spec?.select, columnAccess),\n with:\n spec?.include === undefined\n ? undefined\n : translateInclude(table, schema, spec.include, columnAccess),\n }),\n );\n return row ?? null;\n },\n\n async count(table, where) {\n return runDatabaseOperation(() =>\n db.$count(pgTable(table), whereSql(table, where)),\n );\n },\n\n async insert(table, values) {\n const engineTable = pgTable(table);\n const parameters = mutationValues(table, values, \"insert\");\n const rows = await runDatabaseOperation(() =>\n db\n .insert(engineTable)\n .values(parameters)\n .returning(returningColumns(table, columnAccess)),\n );\n return expectExactlyOne(mutationRows(table, rows as Row[], columnAccess));\n },\n\n async update(table, id, values) {\n const engineTable = pgTable(table);\n const parameters = mutationValues(table, values, \"update\");\n const { meta: primaryKey, value: validatedId } = validatedPrimaryKey(\n table,\n id,\n );\n const rows = await runDatabaseOperation(() =>\n db\n .update(engineTable)\n .set(parameters)\n .where(eq(columnOf(table, primaryKey.columnName), validatedId))\n .returning(returningColumns(table, columnAccess)),\n );\n return expectZeroOrOne(mutationRows(table, rows as Row[], columnAccess));\n },\n\n async upsert(table, values, onConflict) {\n const engineTable = pgTable(table);\n const parameters = mutationValues(table, values, \"insert\");\n const target = conflictTargetMeta(table, onConflict);\n const updates = upsertUpdateValues(table, parameters, target.columnName);\n const rows = await runDatabaseOperation(() =>\n db\n .insert(engineTable)\n .values(parameters)\n .onConflictDoUpdate({\n target: columnOf(table, target.columnName),\n set: updates,\n })\n .returning(returningColumns(table, columnAccess)),\n );\n return expectExactlyOne(mutationRows(table, rows as Row[], columnAccess));\n },\n\n async delete(table, id) {\n const { meta: primaryKey, value: validatedId } = validatedPrimaryKey(\n table,\n id,\n );\n const rows = await runDatabaseOperation(() =>\n db\n .delete(pgTable(table))\n .where(eq(columnOf(table, primaryKey.columnName), validatedId))\n .returning({ id: columnOf(table, primaryKey.columnName) }),\n );\n return expectZeroOrOne(rows as Row[]) !== null;\n },\n\n async raw<T = Row>(\n strings: TemplateStringsArray,\n ...values: unknown[]\n ): Promise<T[]> {\n // SQL wrappers carry structure; tagged interpolations may carry values only.\n if (values.some((value) => isSQLWrapper(value))) {\n throw invalidDatabaseRequest(\n \"Tagged SQL interpolations must be parameter values\",\n );\n }\n const result = await runDatabaseOperation(() =>\n db.execute(sql(strings, ...values.map((value) => sql.param(value)))),\n );\n return ((result as { rows?: unknown }).rows ?? result) as T[];\n },\n\n async transaction<T>(callback: (tx: DataPath) => Promise<T>): Promise<T> {\n let callbackFailed = false;\n let callbackError: unknown;\n try {\n return await db.transaction(async (transaction) => {\n try {\n return await callback(\n createDrizzleDataPath(\n transaction as unknown as DrizzleDb,\n schema,\n { columnAccess },\n ),\n );\n } catch (error) {\n callbackFailed = true;\n callbackError = error;\n throw error;\n }\n });\n } catch (error) {\n // Callback errors are application-owned; sanitize only tx lifecycle errors.\n if (\n callbackFailed &&\n (error === callbackError ||\n callbackError instanceof DatabasePluginError)\n ) {\n throw callbackError;\n }\n if (error instanceof DatabasePluginError) throw error;\n throw classifyDriverError(error);\n }\n },\n };\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"drizzle-data-path.js","names":[],"sources":["../../../../src/database/runtime/engine/drizzle-data-path.ts"],"sourcesContent":["import { and, eq, isSQLWrapper, type SQL, sql } from \"drizzle-orm\";\nimport { drizzle, type NodePgDatabase } from \"drizzle-orm/node-postgres\";\nimport type { PgTable } from \"drizzle-orm/pg-core\";\nimport type { Pool } from \"pg\";\n\nimport { createLogger } from \"../../../logging/logger\";\nimport {\n type DatabaseErrorCategory,\n DatabasePluginError,\n invalidDatabaseRequest,\n} from \"../../errors\";\nimport type { AppKitTable, ColumnMeta, Schema } from \"../../schema-builder\";\nimport { buildEngineRelations } from \"../../schema-builder/engine/relations\";\nimport { columnValueSchema } from \"../../schema-builder/validators\";\nimport {\n conflictTargetMeta,\n type DataPath,\n type IdValue,\n limitOrDefault,\n primaryKeyMeta,\n type Row,\n validateOffset,\n type WhereClause,\n} from \"../data-path\";\nimport {\n columnOf,\n type ColumnAccess,\n defaultColumns,\n publicColumnNames,\n returningColumns,\n selectToColumns,\n translateInclude,\n translateOrder,\n translateWhere,\n} from \"./translate\";\n\nconst logger = createLogger(\"database\");\n\n/** Concrete Drizzle seam shared by the adapter and its focused tests. */\nexport type DrizzleDb = NodePgDatabase<Record<string, never>>;\n\nexport interface DrizzleDataPathOptions {\n /** Explicit capability for trusted server code that must read private data. */\n readonly columnAccess?: ColumnAccess;\n}\n\n/** Bind finalized AppKit metadata to a relational Drizzle database. */\nexport function createDrizzleDb(pool: Pool, schema: Schema): DrizzleDb {\n const completeSchema: Record<string, unknown> = Object.assign(\n Object.create(null),\n schema.$engine,\n buildEngineRelations(schema.$tables),\n );\n return drizzle(pool, { schema: completeSchema }) as unknown as DrizzleDb;\n}\n\ninterface RelationalQueryBuilder {\n findMany(config: Record<string, unknown>): Promise<Row[]>;\n findFirst(config: Record<string, unknown>): Promise<Row | undefined>;\n}\n\n/** Reject same-name or forged tables by requiring finalized object identity. */\nfunction assertRegisteredTable(schema: Schema, table: AppKitTable): void {\n if (schema.$tables[table.$name] !== table) {\n throw invalidDatabaseRequest(`Table \"${table.$name}\" is not registered`);\n }\n}\n\nfunction relationalQueryBuilder(\n db: DrizzleDb,\n schema: Schema,\n table: AppKitTable,\n): RelationalQueryBuilder {\n assertRegisteredTable(schema, table);\n const query = (db.query as unknown as Record<string, RelationalQueryBuilder>)[\n table.$name\n ];\n if (!query) {\n throw invalidDatabaseRequest(`Table \"${table.$name}\" is not registered`);\n }\n return query;\n}\n\nfunction selectedColumns(\n table: AppKitTable,\n select?: readonly string[],\n access: ColumnAccess = \"public\",\n): Record<string, true> {\n return select === undefined\n ? defaultColumns(table)\n : selectToColumns(table, select, access);\n}\n\ntype MutationKind = \"insert\" | \"update\";\n\ninterface MutationSchema {\n safeParse(\n value: unknown,\n ):\n | { readonly success: true; readonly data: unknown }\n | { readonly success: false };\n}\n\n/** Apply the finalized write schema again at the persistence boundary. */\nfunction mutationValues(\n table: AppKitTable,\n values: Row,\n kind: MutationKind,\n): Row {\n if (values === null || typeof values !== \"object\" || Array.isArray(values)) {\n throw invalidDatabaseRequest(\"Database mutation values must be an object\");\n }\n for (const [key, value] of Object.entries(values)) {\n if (!Object.hasOwn(table.$columns, key)) {\n throw invalidDatabaseRequest(`Unknown column \"${table.$name}.${key}\"`);\n }\n if (isSQLWrapper(value)) {\n throw invalidDatabaseRequest(\n \"Database mutation values cannot contain SQL\",\n );\n }\n }\n const schema = (\n kind === \"insert\" ? table.$insertSchema : table.$updateSchema\n ) as MutationSchema;\n const result = schema.safeParse(values);\n if (!result.success) {\n throw invalidDatabaseRequest(`Invalid database ${kind} values`);\n }\n return result.data as Row;\n}\n\n/** Never change row identity or the conflict key during the update half. */\nfunction upsertUpdateValues(\n table: AppKitTable,\n parameters: Row,\n conflictTarget: string,\n): Row {\n const updates: Row = {};\n for (const [key, value] of Object.entries(parameters)) {\n const column = table.$columns[key];\n if (\n column.primaryKey ||\n column.serverGenerated ||\n column.columnName === conflictTarget\n ) {\n continue;\n }\n updates[key] = value;\n }\n if (Object.keys(updates).length > 0) return updates;\n\n // A safe self-assignment preserves the exactly-one-row upsert contract.\n return { [conflictTarget]: columnOf(table, conflictTarget) };\n}\n\n// Drizzle wraps driver failures in DrizzleQueryError, so the SQLSTATE sits on a\n// nested `cause` rather than the thrown error. Walk a bounded chain to find it.\nconst MAX_CAUSE_DEPTH = 5;\n\nfunction sqlStateOf(error: unknown): string | undefined {\n let current = error;\n for (let depth = 0; depth < MAX_CAUSE_DEPTH; depth++) {\n if (!current || typeof current !== \"object\") return undefined;\n try {\n const candidate = Reflect.get(current, \"code\");\n // SQLSTATE is always a five-character alphanumeric class code.\n if (typeof candidate === \"string\" && /^[0-9A-Z]{5}$/.test(candidate)) {\n return candidate;\n }\n current = Reflect.get(current, \"cause\");\n } catch {\n return undefined;\n }\n }\n return undefined;\n}\n\n/** Classify SQLSTATE without retaining the driver error or its properties. */\nfunction classifyDriverError(error: unknown): DatabasePluginError {\n const code = sqlStateOf(error);\n const category: DatabaseErrorCategory =\n code === \"40001\" || code === \"40P01\" || code === \"57014\"\n ? \"TRANSIENT\"\n : code === \"42501\"\n ? \"FORBIDDEN\"\n : code?.startsWith(\"23\")\n ? \"CONFLICT\"\n : \"INTERNAL\";\n logger.error(\n \"Database driver error classified as %s (SQLSTATE %s)\",\n category,\n code ?? \"unknown\",\n );\n return new DatabasePluginError(category, \"runtime\");\n}\n\nasync function runDatabaseOperation<T>(\n operation: () => Promise<T>,\n): Promise<T> {\n try {\n return await operation();\n } catch (error) {\n if (error instanceof DatabasePluginError) throw error;\n throw classifyDriverError(error);\n }\n}\n\n/** Apply the adapter's column capability before rows reach cardinality checks. */\nfunction mutationRows(\n table: AppKitTable,\n rows: Row[],\n access: ColumnAccess,\n): Row[] {\n if (access === \"trusted\") return rows;\n const names = publicColumnNames(table);\n return rows.map((row) => {\n const projected: Row = {};\n for (const name of names) {\n if (Object.hasOwn(row, name)) projected[name] = row[name];\n }\n return projected;\n });\n}\n\n/** Resolve and validate the sole key once before a keyed operation executes. */\nfunction validatedPrimaryKey(\n table: AppKitTable,\n id: unknown,\n): { readonly meta: ColumnMeta; readonly value: IdValue } {\n const meta = primaryKeyMeta(table);\n const result = columnValueSchema(meta).safeParse(id);\n if (!result.success) {\n throw invalidDatabaseRequest(\n `Invalid primary-key value for \"${table.$name}.${meta.columnName}\"`,\n );\n }\n return { meta, value: result.data as IdValue };\n}\n\n// Enforce the single-row DataPath contract before results reach callers.\nfunction expectExactlyOne(rows: Row[]): Row {\n if (rows.length !== 1) {\n throw new DatabasePluginError(\"INTERNAL\", \"runtime\");\n }\n return rows[0];\n}\n\nfunction expectZeroOrOne(rows: Row[]): Row | null {\n if (rows.length > 1) {\n throw new DatabasePluginError(\"INTERNAL\", \"runtime\");\n }\n return rows[0] ?? null;\n}\n\n/** Adapt a Drizzle database to AppKit's internal execution port. */\nexport function createDrizzleDataPath(\n db: DrizzleDb,\n schema: Schema,\n options: DrizzleDataPathOptions = {},\n): DataPath {\n const columnAccess = options.columnAccess ?? \"public\";\n const pgTable = (table: AppKitTable): PgTable => {\n assertRegisteredTable(schema, table);\n return table.$engine as unknown as PgTable;\n };\n const whereSql = (\n table: AppKitTable,\n where?: WhereClause,\n ): SQL | undefined =>\n where === undefined\n ? undefined\n : translateWhere(table, where, columnAccess);\n\n return {\n async select(table, spec) {\n return runDatabaseOperation(() =>\n relationalQueryBuilder(db, schema, table).findMany({\n where: whereSql(table, spec.where),\n orderBy:\n spec.order === undefined\n ? undefined\n : translateOrder(table, spec.order, columnAccess),\n columns: selectedColumns(table, spec.select, columnAccess),\n with:\n spec.include === undefined\n ? undefined\n : translateInclude(table, schema, spec.include, columnAccess),\n limit: limitOrDefault(spec.limit),\n offset:\n spec.offset === undefined ? undefined : validateOffset(spec.offset),\n }),\n );\n },\n\n async findOne(table, id, spec) {\n const { meta: primaryKey, value: validatedId } = validatedPrimaryKey(\n table,\n id,\n );\n const primaryKeyPredicate = eq(\n columnOf(table, primaryKey.columnName),\n validatedId,\n );\n const additionalPredicate =\n spec?.where === undefined\n ? undefined\n : translateWhere(table, spec.where, columnAccess);\n const row = await runDatabaseOperation(() =>\n relationalQueryBuilder(db, schema, table).findFirst({\n where:\n additionalPredicate === undefined\n ? primaryKeyPredicate\n : and(primaryKeyPredicate, additionalPredicate),\n columns: selectedColumns(table, spec?.select, columnAccess),\n with:\n spec?.include === undefined\n ? undefined\n : translateInclude(table, schema, spec.include, columnAccess),\n }),\n );\n return row ?? null;\n },\n\n async count(table, where) {\n return runDatabaseOperation(() =>\n db.$count(pgTable(table), whereSql(table, where)),\n );\n },\n\n async insert(table, values) {\n const engineTable = pgTable(table);\n const parameters = mutationValues(table, values, \"insert\");\n const rows = await runDatabaseOperation(() =>\n db\n .insert(engineTable)\n .values(parameters)\n .returning(returningColumns(table, columnAccess)),\n );\n return expectExactlyOne(mutationRows(table, rows as Row[], columnAccess));\n },\n\n async update(table, id, values) {\n const engineTable = pgTable(table);\n const parameters = mutationValues(table, values, \"update\");\n const { meta: primaryKey, value: validatedId } = validatedPrimaryKey(\n table,\n id,\n );\n const rows = await runDatabaseOperation(() =>\n db\n .update(engineTable)\n .set(parameters)\n .where(eq(columnOf(table, primaryKey.columnName), validatedId))\n .returning(returningColumns(table, columnAccess)),\n );\n return expectZeroOrOne(mutationRows(table, rows as Row[], columnAccess));\n },\n\n async upsert(table, values, onConflict) {\n const engineTable = pgTable(table);\n const parameters = mutationValues(table, values, \"insert\");\n const target = conflictTargetMeta(table, onConflict);\n const updates = upsertUpdateValues(table, parameters, target.columnName);\n const rows = await runDatabaseOperation(() =>\n db\n .insert(engineTable)\n .values(parameters)\n .onConflictDoUpdate({\n target: columnOf(table, target.columnName),\n set: updates,\n })\n .returning(returningColumns(table, columnAccess)),\n );\n return expectExactlyOne(mutationRows(table, rows as Row[], columnAccess));\n },\n\n async delete(table, id) {\n const { meta: primaryKey, value: validatedId } = validatedPrimaryKey(\n table,\n id,\n );\n const rows = await runDatabaseOperation(() =>\n db\n .delete(pgTable(table))\n .where(eq(columnOf(table, primaryKey.columnName), validatedId))\n .returning({ id: columnOf(table, primaryKey.columnName) }),\n );\n return expectZeroOrOne(rows as Row[]) !== null;\n },\n\n async raw<T = Row>(\n strings: TemplateStringsArray,\n ...values: unknown[]\n ): Promise<T[]> {\n // SQL wrappers carry structure; tagged interpolations may carry values only.\n if (values.some((value) => isSQLWrapper(value))) {\n throw invalidDatabaseRequest(\n \"Tagged SQL interpolations must be parameter values\",\n );\n }\n const result = await runDatabaseOperation(() =>\n db.execute(sql(strings, ...values.map((value) => sql.param(value)))),\n );\n return ((result as { rows?: unknown }).rows ?? result) as T[];\n },\n\n async transaction<T>(callback: (tx: DataPath) => Promise<T>): Promise<T> {\n let callbackFailed = false;\n let callbackError: unknown;\n try {\n return await db.transaction(async (transaction) => {\n try {\n return await callback(\n createDrizzleDataPath(\n transaction as unknown as DrizzleDb,\n schema,\n { columnAccess },\n ),\n );\n } catch (error) {\n callbackFailed = true;\n callbackError = error;\n throw error;\n }\n });\n } catch (error) {\n // Callback errors are application-owned; sanitize only tx lifecycle errors.\n if (\n callbackFailed &&\n (error === callbackError ||\n callbackError instanceof DatabasePluginError)\n ) {\n throw callbackError;\n }\n if (error instanceof DatabasePluginError) throw error;\n throw classifyDriverError(error);\n }\n },\n };\n}\n"],"mappings":"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"}
@@ -1,7 +1,7 @@
1
1
  import { filterOperatorsForKind } from "../../schema-builder/types.js";
2
2
  import { columnValueSchema } from "../../schema-builder/validators.js";
3
3
  import { invalidDatabaseRequest } from "../../errors.js";
4
- import { DEFAULT_LIMIT, IN_CAP, MAX_INCLUDES, MAX_WHERE_CONDITIONS, MAX_WHERE_DEPTH, MAX_WHERE_GROUP_ITEMS, isFilterOperator } from "../../contract/wire.js";
4
+ import { DEFAULT_LIMIT, IN_CAP, MAX_INCLUDES, MAX_INCLUDE_DEPTH, MAX_INCLUDE_NODES, MAX_WHERE_CONDITIONS, MAX_WHERE_DEPTH, MAX_WHERE_GROUP_ITEMS, isFilterOperator } from "../../contract/wire.js";
5
5
  import "../../contract/index.js";
6
6
  import { validateLimit } from "../data-path.js";
7
7
  import { and, asc, desc, eq, gt, gte, ilike, inArray, isNull, like, lt, lte, ne, or, sql } from "drizzle-orm";
@@ -141,8 +141,12 @@ function tableByName(schema, name) {
141
141
  if (!table) throw invalidDatabaseRequest(`Unknown table "${name}"`);
142
142
  return table;
143
143
  }
144
- /** Translate one relation edge into Drizzle's relational `with` config. */
144
+ /** Translate relation edges into Drizzle's relational `with` config. */
145
145
  function translateInclude(table, schema, include, access = "public") {
146
+ return translateIncludeTree(table, schema, include, access, 1, { nodes: 0 });
147
+ }
148
+ function translateIncludeTree(table, schema, include, access, depth, budget) {
149
+ if (depth > MAX_INCLUDE_DEPTH) throw invalidDatabaseRequest(`include exceeds the ${MAX_INCLUDE_DEPTH}-edge depth limit`);
146
150
  const entries = Object.entries(include);
147
151
  if (entries.length > MAX_INCLUDES) throw invalidDatabaseRequest(`include exceeds the ${MAX_INCLUDES}-relation limit`);
148
152
  const config = {};
@@ -150,6 +154,8 @@ function translateInclude(table, schema, include, access = "public") {
150
154
  const relation = table.$relations.find((candidate) => candidate.name === relationName);
151
155
  if (!relation) throw invalidDatabaseRequest(`Unknown relation "${table.$name}.${relationName}"`);
152
156
  if (rawOptions === false) continue;
157
+ budget.nodes += 1;
158
+ if (budget.nodes > MAX_INCLUDE_NODES) throw invalidDatabaseRequest(`include exceeds the ${MAX_INCLUDE_NODES}-node limit`);
153
159
  const target = tableByName(schema, relation.targetTable);
154
160
  if (rawOptions === true) {
155
161
  config[relationName] = {
@@ -166,6 +172,7 @@ function translateInclude(table, schema, include, access = "public") {
166
172
  if (relation.cardinality !== "toMany") throw invalidDatabaseRequest("Only to-many relations accept a limit");
167
173
  relationConfig.limit = validateLimit(options.limit);
168
174
  } else if (relation.cardinality === "toMany") relationConfig.limit = DEFAULT_LIMIT;
175
+ if (options.include !== void 0) relationConfig.with = translateIncludeTree(target, schema, options.include, access, depth + 1, budget);
169
176
  config[relationName] = relationConfig;
170
177
  }
171
178
  return config;
@@ -1 +1 @@
1
- {"version":3,"file":"translate.js","names":[],"sources":["../../../../src/database/runtime/engine/translate.ts"],"sourcesContent":["import {\n and,\n asc,\n desc,\n eq,\n gt,\n gte,\n ilike,\n inArray,\n isNull,\n like,\n lt,\n lte,\n ne,\n or,\n type SQL,\n sql,\n} from \"drizzle-orm\";\nimport type { AnyPgColumn } from \"drizzle-orm/pg-core\";\n\nimport {\n DEFAULT_LIMIT,\n type FilterOperator,\n IN_CAP,\n isFilterOperator,\n MAX_INCLUDES,\n MAX_WHERE_CONDITIONS,\n MAX_WHERE_DEPTH,\n MAX_WHERE_GROUP_ITEMS,\n} from \"../../contract\";\nimport { invalidDatabaseRequest } from \"../../errors\";\nimport type { AppKitTable, ColumnMeta, Schema } from \"../../schema-builder\";\nimport { filterOperatorsForKind } from \"../../schema-builder/types\";\nimport { columnValueSchema } from \"../../schema-builder/validators\";\nimport {\n type FilterOps,\n type IncludeOptions,\n type IncludeSpec,\n type OrderSpec,\n validateLimit,\n type WhereClause,\n} from \"../data-path\";\n\n/** Read policy selected by the trusted adapter constructor, never by query input. */\nexport type ColumnAccess = \"public\" | \"trusted\";\n\nfunction columnMetaOf(table: AppKitTable, key: string): ColumnMeta {\n const column = table.$columns[key];\n if (!column) {\n throw invalidDatabaseRequest(`Unknown column \"${table.$name}.${key}\"`);\n }\n return column;\n}\n\nfunction readableColumnMeta(\n table: AppKitTable,\n key: string,\n access: ColumnAccess,\n): ColumnMeta {\n const column = columnMetaOf(table, key);\n if (column.isPrivate && access !== \"trusted\") {\n throw invalidDatabaseRequest(\n `Column \"${table.$name}.${key}\" is not readable`,\n );\n }\n return column;\n}\n\n/** Resolve SQL identifiers only through columns finalized by the schema builder. */\nexport function columnOf(table: AppKitTable, key: string): AnyPgColumn {\n return columnMetaOf(table, key).engineColumn as unknown as AnyPgColumn;\n}\n\n/** Column names that default reads and mutation returns may expose. */\nexport function publicColumnNames(table: AppKitTable): string[] {\n return Object.values(table.$columns)\n .filter((column) => !column.isPrivate)\n .map((column) => column.columnName);\n}\n\n/** Default reads select all finalized columns except private application data. */\nexport function defaultColumns(table: AppKitTable): Record<string, true> {\n const columns: Record<string, true> = {};\n for (const name of publicColumnNames(table)) columns[name] = true;\n return columns;\n}\n\n/** Drizzle `.returning()` fields allowed by the adapter's read capability. */\nexport function returningColumns(\n table: AppKitTable,\n access: ColumnAccess = \"public\",\n): Record<string, AnyPgColumn> {\n const columns: Record<string, AnyPgColumn> = {};\n const names =\n access === \"trusted\"\n ? Object.values(table.$columns).map((column) => column.columnName)\n : publicColumnNames(table);\n for (const name of names) {\n columns[name] = columnOf(table, name);\n }\n return columns;\n}\n\nfunction supportsOperator(meta: ColumnMeta, operator: FilterOperator): boolean {\n if (operator === \"is\") {\n return !meta.notNull && meta.kind !== \"json\" && meta.kind !== \"unknown\";\n }\n return filterOperatorsForKind(meta.kind).includes(operator);\n}\n\nfunction assertColumnValue(\n table: AppKitTable,\n meta: ColumnMeta,\n operator: FilterOperator,\n value: unknown,\n): void {\n if (!columnValueSchema(meta).safeParse(value).success) {\n throw invalidDatabaseRequest(\n `Invalid ${operator} operand for \"${table.$name}.${meta.columnName}\"`,\n );\n }\n}\n\nfunction inList(\n table: AppKitTable,\n meta: ColumnMeta,\n column: AnyPgColumn,\n value: unknown,\n): SQL {\n if (!Array.isArray(value)) {\n throw invalidDatabaseRequest('The \"in\" operator requires an array');\n }\n if (value.length > IN_CAP) {\n throw invalidDatabaseRequest(`in list exceeds the ${IN_CAP}-value limit`);\n }\n for (const item of value) {\n if (item === null) {\n throw invalidDatabaseRequest('The \"in\" operator does not accept null');\n }\n assertColumnValue(table, meta, \"in\", item);\n }\n return value.length === 0 ? sql.raw(\"false\") : inArray(column, value);\n}\n\nfunction translateOperator(\n table: AppKitTable,\n meta: ColumnMeta,\n column: AnyPgColumn,\n operator: FilterOperator,\n value: unknown,\n): SQL {\n if (!supportsOperator(meta, operator)) {\n throw invalidDatabaseRequest(\n `Operator \"${operator}\" is not supported for \"${table.$name}.${meta.columnName}\"`,\n );\n }\n if (operator === \"is\") {\n if (value !== null) {\n throw invalidDatabaseRequest('The \"is\" operator accepts only null');\n }\n return isNull(column);\n }\n if (operator === \"in\") return inList(table, meta, column, value);\n\n assertColumnValue(table, meta, operator, value);\n switch (operator) {\n case \"eq\":\n return eq(column, value);\n case \"neq\":\n return ne(column, value);\n case \"gt\":\n return gt(column, value);\n case \"gte\":\n return gte(column, value);\n case \"lt\":\n return lt(column, value);\n case \"lte\":\n return lte(column, value);\n case \"like\":\n return like(column, value as string);\n case \"ilike\":\n return ilike(column, value as string);\n default:\n throw invalidDatabaseRequest(`Unsupported filter operator \"${operator}\"`);\n }\n}\n\ninterface WhereBudget {\n conditions: number;\n}\n\nfunction chargeWhereCondition(budget: WhereBudget): void {\n budget.conditions += 1;\n if (budget.conditions > MAX_WHERE_CONDITIONS) {\n throw invalidDatabaseRequest(\n `where exceeds the ${MAX_WHERE_CONDITIONS}-condition limit`,\n );\n }\n}\n\nfunction translateWhereNode(\n table: AppKitTable,\n clause: WhereClause,\n access: ColumnAccess,\n depth: number,\n budget: WhereBudget,\n): SQL | undefined {\n if (depth > MAX_WHERE_DEPTH) {\n throw invalidDatabaseRequest(\n `where exceeds the ${MAX_WHERE_DEPTH}-level depth limit`,\n );\n }\n if (clause === null || typeof clause !== \"object\" || Array.isArray(clause)) {\n throw invalidDatabaseRequest(\"where must be an object\");\n }\n const conditions: SQL[] = [];\n for (const [key, value] of Object.entries(clause)) {\n if (key === \"and\" || key === \"or\") {\n if (\n !Array.isArray(value) ||\n value.length === 0 ||\n value.length > MAX_WHERE_GROUP_ITEMS\n ) {\n if (Array.isArray(value) && value.length > MAX_WHERE_GROUP_ITEMS) {\n throw invalidDatabaseRequest(\n `${key} exceeds the ${MAX_WHERE_GROUP_ITEMS}-predicate limit`,\n );\n }\n throw invalidDatabaseRequest(\n `${key} requires a non-empty predicate array`,\n );\n }\n const groups = value.map((group) => {\n const translated = translateWhereNode(\n table,\n group as WhereClause,\n access,\n depth + 1,\n budget,\n );\n if (!translated) {\n throw invalidDatabaseRequest(`${key} predicates cannot be empty`);\n }\n return translated;\n });\n const combined = key === \"and\" ? and(...groups) : or(...groups);\n if (combined) {\n conditions.push(combined);\n }\n continue;\n }\n\n const meta = readableColumnMeta(table, key, access);\n const column = meta.engineColumn as unknown as AnyPgColumn;\n if (Array.isArray(value)) {\n chargeWhereCondition(budget);\n conditions.push(translateOperator(table, meta, column, \"in\", value));\n } else if (\n value !== null &&\n typeof value === \"object\" &&\n !Array.isArray(value)\n ) {\n const operators = Object.entries(value as FilterOps);\n if (operators.length === 0) {\n throw invalidDatabaseRequest(\n `Filter for \"${table.$name}.${key}\" cannot be empty`,\n );\n }\n for (const [operator, operand] of operators) {\n chargeWhereCondition(budget);\n if (!isFilterOperator(operator)) {\n throw invalidDatabaseRequest(`Unknown filter operator \"${operator}\"`);\n }\n conditions.push(\n translateOperator(table, meta, column, operator, operand),\n );\n }\n } else {\n chargeWhereCondition(budget);\n conditions.push(translateOperator(table, meta, column, \"eq\", value));\n }\n }\n return conditions.length > 0 ? and(...conditions) : undefined;\n}\n\n/** Translate one bounded direct-column predicate tree. */\nexport function translateWhere(\n table: AppKitTable,\n clause: WhereClause,\n access: ColumnAccess = \"public\",\n): SQL | undefined {\n return translateWhereNode(table, clause, access, 1, { conditions: 0 });\n}\n\nexport function translateOrder(\n table: AppKitTable,\n order: OrderSpec,\n access: ColumnAccess = \"public\",\n): SQL[] {\n return Object.entries(order).map(([key, direction]) => {\n if (direction !== \"asc\" && direction !== \"desc\") {\n throw invalidDatabaseRequest(`Unknown order direction \"${direction}\"`);\n }\n const column = readableColumnMeta(table, key, access)\n .engineColumn as unknown as AnyPgColumn;\n return direction === \"desc\" ? desc(column) : asc(column);\n });\n}\n\nexport function selectToColumns(\n table: AppKitTable,\n select: readonly string[],\n access: ColumnAccess = \"public\",\n): Record<string, true> {\n const columns: Record<string, true> = {};\n for (const key of select) {\n readableColumnMeta(table, key, access);\n columns[key] = true;\n }\n return columns;\n}\n\nfunction tableByName(schema: Schema, name: string): AppKitTable {\n const table = schema.$tables[name];\n if (!table) throw invalidDatabaseRequest(`Unknown table \"${name}\"`);\n return table;\n}\n\n/** Translate one relation edge into Drizzle's relational `with` config. */\nexport function translateInclude(\n table: AppKitTable,\n schema: Schema,\n include: IncludeSpec,\n access: ColumnAccess = \"public\",\n): Record<string, unknown> {\n const entries = Object.entries(include);\n if (entries.length > MAX_INCLUDES) {\n throw invalidDatabaseRequest(\n `include exceeds the ${MAX_INCLUDES}-relation limit`,\n );\n }\n\n const config: Record<string, unknown> = {};\n for (const [relationName, rawOptions] of entries) {\n const relation = table.$relations.find(\n (candidate) => candidate.name === relationName,\n );\n if (!relation) {\n throw invalidDatabaseRequest(\n `Unknown relation \"${table.$name}.${relationName}\"`,\n );\n }\n if (rawOptions === false) continue;\n\n const target = tableByName(schema, relation.targetTable);\n if (rawOptions === true) {\n config[relationName] = {\n columns: defaultColumns(target),\n ...(relation.cardinality === \"toMany\" ? { limit: DEFAULT_LIMIT } : {}),\n };\n continue;\n }\n\n const options = rawOptions as IncludeOptions;\n const relationConfig: Record<string, unknown> = {\n columns:\n options.select === undefined\n ? defaultColumns(target)\n : selectToColumns(target, options.select, access),\n };\n if (options.where !== undefined) {\n relationConfig.where = translateWhere(target, options.where, access);\n }\n if (options.order !== undefined) {\n relationConfig.orderBy = translateOrder(target, options.order, access);\n }\n if (options.limit !== undefined) {\n if (relation.cardinality !== \"toMany\") {\n throw invalidDatabaseRequest(\"Only to-many relations accept a limit\");\n }\n relationConfig.limit = validateLimit(options.limit);\n } else if (relation.cardinality === \"toMany\") {\n relationConfig.limit = DEFAULT_LIMIT;\n }\n config[relationName] = relationConfig;\n }\n return config;\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"translate.js","names":[],"sources":["../../../../src/database/runtime/engine/translate.ts"],"sourcesContent":["import {\n and,\n asc,\n desc,\n eq,\n gt,\n gte,\n ilike,\n inArray,\n isNull,\n like,\n lt,\n lte,\n ne,\n or,\n type SQL,\n sql,\n} from \"drizzle-orm\";\nimport type { AnyPgColumn } from \"drizzle-orm/pg-core\";\n\nimport {\n DEFAULT_LIMIT,\n type FilterOperator,\n IN_CAP,\n isFilterOperator,\n MAX_INCLUDE_DEPTH,\n MAX_INCLUDE_NODES,\n MAX_INCLUDES,\n MAX_WHERE_CONDITIONS,\n MAX_WHERE_DEPTH,\n MAX_WHERE_GROUP_ITEMS,\n} from \"../../contract\";\nimport { invalidDatabaseRequest } from \"../../errors\";\nimport type { AppKitTable, ColumnMeta, Schema } from \"../../schema-builder\";\nimport { filterOperatorsForKind } from \"../../schema-builder/types\";\nimport { columnValueSchema } from \"../../schema-builder/validators\";\nimport {\n type FilterOps,\n type IncludeOptions,\n type IncludeSpec,\n type OrderSpec,\n validateLimit,\n type WhereClause,\n} from \"../data-path\";\n\n/** Read policy selected by the trusted adapter constructor, never by query input. */\nexport type ColumnAccess = \"public\" | \"trusted\";\n\nfunction columnMetaOf(table: AppKitTable, key: string): ColumnMeta {\n const column = table.$columns[key];\n if (!column) {\n throw invalidDatabaseRequest(`Unknown column \"${table.$name}.${key}\"`);\n }\n return column;\n}\n\nfunction readableColumnMeta(\n table: AppKitTable,\n key: string,\n access: ColumnAccess,\n): ColumnMeta {\n const column = columnMetaOf(table, key);\n if (column.isPrivate && access !== \"trusted\") {\n throw invalidDatabaseRequest(\n `Column \"${table.$name}.${key}\" is not readable`,\n );\n }\n return column;\n}\n\n/** Resolve SQL identifiers only through columns finalized by the schema builder. */\nexport function columnOf(table: AppKitTable, key: string): AnyPgColumn {\n return columnMetaOf(table, key).engineColumn as unknown as AnyPgColumn;\n}\n\n/** Column names that default reads and mutation returns may expose. */\nexport function publicColumnNames(table: AppKitTable): string[] {\n return Object.values(table.$columns)\n .filter((column) => !column.isPrivate)\n .map((column) => column.columnName);\n}\n\n/** Default reads select all finalized columns except private application data. */\nexport function defaultColumns(table: AppKitTable): Record<string, true> {\n const columns: Record<string, true> = {};\n for (const name of publicColumnNames(table)) columns[name] = true;\n return columns;\n}\n\n/** Drizzle `.returning()` fields allowed by the adapter's read capability. */\nexport function returningColumns(\n table: AppKitTable,\n access: ColumnAccess = \"public\",\n): Record<string, AnyPgColumn> {\n const columns: Record<string, AnyPgColumn> = {};\n const names =\n access === \"trusted\"\n ? Object.values(table.$columns).map((column) => column.columnName)\n : publicColumnNames(table);\n for (const name of names) {\n columns[name] = columnOf(table, name);\n }\n return columns;\n}\n\nfunction supportsOperator(meta: ColumnMeta, operator: FilterOperator): boolean {\n if (operator === \"is\") {\n return !meta.notNull && meta.kind !== \"json\" && meta.kind !== \"unknown\";\n }\n return filterOperatorsForKind(meta.kind).includes(operator);\n}\n\nfunction assertColumnValue(\n table: AppKitTable,\n meta: ColumnMeta,\n operator: FilterOperator,\n value: unknown,\n): void {\n if (!columnValueSchema(meta).safeParse(value).success) {\n throw invalidDatabaseRequest(\n `Invalid ${operator} operand for \"${table.$name}.${meta.columnName}\"`,\n );\n }\n}\n\nfunction inList(\n table: AppKitTable,\n meta: ColumnMeta,\n column: AnyPgColumn,\n value: unknown,\n): SQL {\n if (!Array.isArray(value)) {\n throw invalidDatabaseRequest('The \"in\" operator requires an array');\n }\n if (value.length > IN_CAP) {\n throw invalidDatabaseRequest(`in list exceeds the ${IN_CAP}-value limit`);\n }\n for (const item of value) {\n if (item === null) {\n throw invalidDatabaseRequest('The \"in\" operator does not accept null');\n }\n assertColumnValue(table, meta, \"in\", item);\n }\n return value.length === 0 ? sql.raw(\"false\") : inArray(column, value);\n}\n\nfunction translateOperator(\n table: AppKitTable,\n meta: ColumnMeta,\n column: AnyPgColumn,\n operator: FilterOperator,\n value: unknown,\n): SQL {\n if (!supportsOperator(meta, operator)) {\n throw invalidDatabaseRequest(\n `Operator \"${operator}\" is not supported for \"${table.$name}.${meta.columnName}\"`,\n );\n }\n if (operator === \"is\") {\n if (value !== null) {\n throw invalidDatabaseRequest('The \"is\" operator accepts only null');\n }\n return isNull(column);\n }\n if (operator === \"in\") return inList(table, meta, column, value);\n\n assertColumnValue(table, meta, operator, value);\n switch (operator) {\n case \"eq\":\n return eq(column, value);\n case \"neq\":\n return ne(column, value);\n case \"gt\":\n return gt(column, value);\n case \"gte\":\n return gte(column, value);\n case \"lt\":\n return lt(column, value);\n case \"lte\":\n return lte(column, value);\n case \"like\":\n return like(column, value as string);\n case \"ilike\":\n return ilike(column, value as string);\n default:\n throw invalidDatabaseRequest(`Unsupported filter operator \"${operator}\"`);\n }\n}\n\ninterface WhereBudget {\n conditions: number;\n}\n\nfunction chargeWhereCondition(budget: WhereBudget): void {\n budget.conditions += 1;\n if (budget.conditions > MAX_WHERE_CONDITIONS) {\n throw invalidDatabaseRequest(\n `where exceeds the ${MAX_WHERE_CONDITIONS}-condition limit`,\n );\n }\n}\n\nfunction translateWhereNode(\n table: AppKitTable,\n clause: WhereClause,\n access: ColumnAccess,\n depth: number,\n budget: WhereBudget,\n): SQL | undefined {\n if (depth > MAX_WHERE_DEPTH) {\n throw invalidDatabaseRequest(\n `where exceeds the ${MAX_WHERE_DEPTH}-level depth limit`,\n );\n }\n if (clause === null || typeof clause !== \"object\" || Array.isArray(clause)) {\n throw invalidDatabaseRequest(\"where must be an object\");\n }\n const conditions: SQL[] = [];\n for (const [key, value] of Object.entries(clause)) {\n if (key === \"and\" || key === \"or\") {\n if (\n !Array.isArray(value) ||\n value.length === 0 ||\n value.length > MAX_WHERE_GROUP_ITEMS\n ) {\n if (Array.isArray(value) && value.length > MAX_WHERE_GROUP_ITEMS) {\n throw invalidDatabaseRequest(\n `${key} exceeds the ${MAX_WHERE_GROUP_ITEMS}-predicate limit`,\n );\n }\n throw invalidDatabaseRequest(\n `${key} requires a non-empty predicate array`,\n );\n }\n const groups = value.map((group) => {\n const translated = translateWhereNode(\n table,\n group as WhereClause,\n access,\n depth + 1,\n budget,\n );\n if (!translated) {\n throw invalidDatabaseRequest(`${key} predicates cannot be empty`);\n }\n return translated;\n });\n const combined = key === \"and\" ? and(...groups) : or(...groups);\n if (combined) {\n conditions.push(combined);\n }\n continue;\n }\n\n const meta = readableColumnMeta(table, key, access);\n const column = meta.engineColumn as unknown as AnyPgColumn;\n if (Array.isArray(value)) {\n chargeWhereCondition(budget);\n conditions.push(translateOperator(table, meta, column, \"in\", value));\n } else if (\n value !== null &&\n typeof value === \"object\" &&\n !Array.isArray(value)\n ) {\n const operators = Object.entries(value as FilterOps);\n if (operators.length === 0) {\n throw invalidDatabaseRequest(\n `Filter for \"${table.$name}.${key}\" cannot be empty`,\n );\n }\n for (const [operator, operand] of operators) {\n chargeWhereCondition(budget);\n if (!isFilterOperator(operator)) {\n throw invalidDatabaseRequest(`Unknown filter operator \"${operator}\"`);\n }\n conditions.push(\n translateOperator(table, meta, column, operator, operand),\n );\n }\n } else {\n chargeWhereCondition(budget);\n conditions.push(translateOperator(table, meta, column, \"eq\", value));\n }\n }\n return conditions.length > 0 ? and(...conditions) : undefined;\n}\n\n/** Translate one bounded direct-column predicate tree. */\nexport function translateWhere(\n table: AppKitTable,\n clause: WhereClause,\n access: ColumnAccess = \"public\",\n): SQL | undefined {\n return translateWhereNode(table, clause, access, 1, { conditions: 0 });\n}\n\nexport function translateOrder(\n table: AppKitTable,\n order: OrderSpec,\n access: ColumnAccess = \"public\",\n): SQL[] {\n return Object.entries(order).map(([key, direction]) => {\n if (direction !== \"asc\" && direction !== \"desc\") {\n throw invalidDatabaseRequest(`Unknown order direction \"${direction}\"`);\n }\n const column = readableColumnMeta(table, key, access)\n .engineColumn as unknown as AnyPgColumn;\n return direction === \"desc\" ? desc(column) : asc(column);\n });\n}\n\nexport function selectToColumns(\n table: AppKitTable,\n select: readonly string[],\n access: ColumnAccess = \"public\",\n): Record<string, true> {\n const columns: Record<string, true> = {};\n for (const key of select) {\n readableColumnMeta(table, key, access);\n columns[key] = true;\n }\n return columns;\n}\n\nfunction tableByName(schema: Schema, name: string): AppKitTable {\n const table = schema.$tables[name];\n if (!table) throw invalidDatabaseRequest(`Unknown table \"${name}\"`);\n return table;\n}\n\n/** Translate relation edges into Drizzle's relational `with` config. */\nexport function translateInclude(\n table: AppKitTable,\n schema: Schema,\n include: IncludeSpec,\n access: ColumnAccess = \"public\",\n): Record<string, unknown> {\n return translateIncludeTree(table, schema, include, access, 1, { nodes: 0 });\n}\n\nfunction translateIncludeTree(\n table: AppKitTable,\n schema: Schema,\n include: IncludeSpec,\n access: ColumnAccess,\n depth: number,\n budget: { nodes: number },\n): Record<string, unknown> {\n if (depth > MAX_INCLUDE_DEPTH) {\n throw invalidDatabaseRequest(\n `include exceeds the ${MAX_INCLUDE_DEPTH}-edge depth limit`,\n );\n }\n const entries = Object.entries(include);\n if (entries.length > MAX_INCLUDES) {\n throw invalidDatabaseRequest(\n `include exceeds the ${MAX_INCLUDES}-relation limit`,\n );\n }\n\n const config: Record<string, unknown> = {};\n for (const [relationName, rawOptions] of entries) {\n const relation = table.$relations.find(\n (candidate) => candidate.name === relationName,\n );\n if (!relation) {\n throw invalidDatabaseRequest(\n `Unknown relation \"${table.$name}.${relationName}\"`,\n );\n }\n if (rawOptions === false) continue;\n\n budget.nodes += 1;\n if (budget.nodes > MAX_INCLUDE_NODES) {\n throw invalidDatabaseRequest(\n `include exceeds the ${MAX_INCLUDE_NODES}-node limit`,\n );\n }\n\n const target = tableByName(schema, relation.targetTable);\n if (rawOptions === true) {\n config[relationName] = {\n columns: defaultColumns(target),\n ...(relation.cardinality === \"toMany\" ? { limit: DEFAULT_LIMIT } : {}),\n };\n continue;\n }\n\n const options = rawOptions as IncludeOptions;\n const relationConfig: Record<string, unknown> = {\n columns:\n options.select === undefined\n ? defaultColumns(target)\n : selectToColumns(target, options.select, access),\n };\n if (options.where !== undefined) {\n relationConfig.where = translateWhere(target, options.where, access);\n }\n if (options.order !== undefined) {\n relationConfig.orderBy = translateOrder(target, options.order, access);\n }\n if (options.limit !== undefined) {\n if (relation.cardinality !== \"toMany\") {\n throw invalidDatabaseRequest(\"Only to-many relations accept a limit\");\n }\n relationConfig.limit = validateLimit(options.limit);\n } else if (relation.cardinality === \"toMany\") {\n relationConfig.limit = DEFAULT_LIMIT;\n }\n if (options.include !== undefined) {\n relationConfig.with = translateIncludeTree(\n target,\n schema,\n options.include,\n access,\n depth + 1,\n budget,\n );\n }\n config[relationName] = relationConfig;\n }\n return config;\n}\n"],"mappings":"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"}
@@ -6,7 +6,11 @@ interface SchemaBuilderContext {
6
6
  table<C extends Record<string, ColumnBuilder>>(name: string, columns: C): TableHandle<C>;
7
7
  enum(name: string, values: readonly string[]): ColumnBuilder;
8
8
  }
9
- declare function defineSchema(builder: (context: SchemaBuilderContext) => Record<string, AppKitTable>, options?: DefineSchemaOptions): Schema;
9
+ /**
10
+ * Compile one declared schema. The returned type keeps the table names the
11
+ * builder returned, so `crudRoutes` and `hooks` can name only real tables.
12
+ */
13
+ declare function defineSchema<TTables extends Record<string, AppKitTable>>(builder: (context: SchemaBuilderContext) => TTables, options?: DefineSchemaOptions): Schema<Extract<keyof TTables, string>>;
10
14
  //#endregion
11
15
  export { defineSchema };
12
16
  //# sourceMappingURL=define-schema.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"define-schema.d.ts","names":[],"sources":["../../../src/database/schema-builder/define-schema.ts"],"mappings":";;;;UAoCiB,oBAAA;EACf,KAAA,WAAgB,MAAA,SAAe,aAAA,GAC7B,IAAA,UACA,OAAA,EAAS,CAAA,GACR,WAAA,CAAY,CAAA;EACf,IAAA,CAAK,IAAA,UAAc,MAAA,sBAA4B,aAAA;AAAA;AAAA,iBA4SjC,YAAA,CACd,OAAA,GAAU,OAAA,EAAS,oBAAA,KAAyB,MAAA,SAAe,WAAA,GAC3D,OAAA,GAAU,mBAAA,GACT,MAAA"}
1
+ {"version":3,"file":"define-schema.d.ts","names":[],"sources":["../../../src/database/schema-builder/define-schema.ts"],"mappings":";;;;UAoCiB,oBAAA;EACf,KAAA,WAAgB,MAAA,SAAe,aAAA,GAC7B,IAAA,UACA,OAAA,EAAS,CAAA,GACR,WAAA,CAAY,CAAA;EACf,IAAA,CAAK,IAAA,UAAc,MAAA,sBAA4B,aAAA;AAAA;;;;;iBAgTjC,YAAA,iBAA6B,MAAA,SAAe,WAAA,EAAA,CAC1D,OAAA,GAAU,OAAA,EAAS,oBAAA,KAAyB,OAAA,EAC5C,OAAA,GAAU,mBAAA,GACT,MAAA,CAAO,OAAA,OAAc,OAAA"}
@@ -184,6 +184,10 @@ function publishSchema(raw, candidates, schemaName) {
184
184
  function assertFinalizedSchema(value) {
185
185
  if (value === null || typeof value !== "object" || value[FINALIZED_SCHEMA] !== true) throw new TypeError("Expected a finalized AppKit database schema");
186
186
  }
187
+ /**
188
+ * Compile one declared schema. The returned type keeps the table names the
189
+ * builder returned, so `crudRoutes` and `hooks` can name only real tables.
190
+ */
187
191
  function defineSchema(builder, options) {
188
192
  const schemaName = options?.schemaName ?? "public";
189
193
  if (!schemaName) throw new SchemaBuildError("Schema name cannot be empty");
@@ -1 +1 @@
1
- {"version":3,"file":"define-schema.js","names":[],"sources":["../../../src/database/schema-builder/define-schema.ts"],"sourcesContent":["import { ColumnBuilder, enumColumn, validateLiteralDefaults } from \"./columns\";\nimport { buildEngineTables } from \"./engine/tables\";\nimport { resolveForeignKeys } from \"./fk\";\nimport { buildRelations } from \"./relations\";\nimport {\n type AppKitTable,\n type ColumnMeta,\n type ColumnRef,\n type DefineSchemaOptions,\n type MutableColumnMeta,\n type ResolvedRelation,\n type Schema,\n SchemaBuildError,\n type TableHandle,\n} from \"./types\";\nimport { deriveInsertSchema, deriveUpdateSchema } from \"./validators\";\n\ninterface RawTable {\n readonly name: string;\n readonly metas: Record<string, MutableColumnMeta>;\n readonly handle: Record<string, ColumnRef>;\n}\n\ninterface FinalizedTableCandidate {\n readonly columns: Readonly<Record<string, ColumnMeta>>;\n readonly engine: Schema[\"$engine\"][string];\n readonly relations: readonly ResolvedRelation[];\n readonly insertSchema: unknown;\n readonly updateSchema: unknown;\n}\n\ninterface DeclarationState {\n readonly raw: Map<string, RawTable>;\n readonly handleNames: Map<object, string>;\n}\n\nexport interface SchemaBuilderContext {\n table<C extends Record<string, ColumnBuilder>>(\n name: string,\n columns: C,\n ): TableHandle<C>;\n enum(name: string, values: readonly string[]): ColumnBuilder;\n}\n\nconst RESERVED_OBJECT_KEYS = new Set([\"__proto__\", \"prototype\", \"constructor\"]);\nconst RESERVED_DATABASE_EXPORT_KEYS = new Set([\"sql\", \"transaction\"]);\n// A global symbol lets typegen recognize schemas loaded through another AppKit instance.\nconst FINALIZED_SCHEMA = Symbol.for(\"@databricks/appkit.database.schema\");\nconst TABLE_METADATA_KEYS = [\n \"$name\",\n \"$schemaName\",\n \"$columns\",\n \"$engine\",\n \"$relations\",\n \"$insertSchema\",\n \"$updateSchema\",\n] as const;\n\nfunction nullRecord<T>(): Record<string, T> {\n return Object.create(null) as Record<string, T>;\n}\n\nfunction assertRecord(value: unknown, label: string): asserts value is object {\n if (value === null || typeof value !== \"object\" || Array.isArray(value)) {\n throw new SchemaBuildError(`${label} must be an object`);\n }\n}\n\nfunction assertName(value: string, label: string): void {\n if (!value || RESERVED_OBJECT_KEYS.has(value)) {\n throw new SchemaBuildError(`${label} \"${value}\" is reserved`);\n }\n}\n\nfunction assertTableName(value: string): void {\n assertName(value, \"Table name\");\n if (RESERVED_DATABASE_EXPORT_KEYS.has(value)) {\n throw new SchemaBuildError(`Table name \"${value}\" is reserved`);\n }\n}\n\nfunction finalizeColumn(meta: MutableColumnMeta): ColumnMeta {\n if (!meta.engineColumn) {\n throw new SchemaBuildError(\n `Engine column \"${meta.columnName}\" is missing during finalization`,\n );\n }\n const { fkRef: _fkRef, ...published } = meta;\n return Object.freeze({\n ...published,\n enumValues: meta.enumValues\n ? Object.freeze([...meta.enumValues])\n : undefined,\n fk: meta.fk ? Object.freeze({ ...meta.fk }) : undefined,\n engineColumn: meta.engineColumn,\n }) as ColumnMeta;\n}\n\nfunction finalizeRelations(\n relations: readonly ResolvedRelation[],\n): readonly ResolvedRelation[] {\n return Object.freeze(\n relations.map((relation) => Object.freeze({ ...relation })),\n );\n}\n\n/** Clone builders into per-table metadata and create stable column handles. */\nfunction declareTable<C extends Record<string, ColumnBuilder>>(\n state: DeclarationState,\n name: string,\n columns: C,\n): TableHandle<C> {\n assertTableName(name);\n if (state.raw.has(name)) {\n throw new SchemaBuildError(`Duplicate table \"${name}\"`);\n }\n assertRecord(columns, `Columns for table \"${name}\"`);\n\n const metas = nullRecord<MutableColumnMeta>();\n const handle = Object.create(null) as Record<string, ColumnRef>;\n for (const [key, column] of Object.entries(columns)) {\n assertName(key, `Column name on table \"${name}\"`);\n if (key.startsWith(\"$\") || key === \"and\" || key === \"or\") {\n throw new SchemaBuildError(\n `Column \"${name}.${key}\" collides with AppKit runtime metadata`,\n );\n }\n if (!(column instanceof ColumnBuilder)) {\n throw new SchemaBuildError(\n `Column \"${name}.${key}\" was not created by an AppKit column builder`,\n );\n }\n\n const meta = column._cloneMeta();\n meta.name = key;\n meta.columnName = key;\n metas[key] = meta;\n\n const reference: ColumnRef = Object.freeze({\n __isColumnRef: true,\n tableName: name,\n columnName: key,\n });\n Object.defineProperty(handle, key, {\n enumerable: true,\n value: reference,\n });\n }\n\n state.raw.set(name, { name, metas, handle });\n state.handleNames.set(handle, name);\n return handle as unknown as TableHandle<C>;\n}\n\nfunction createBuilderContext(state: DeclarationState): SchemaBuilderContext {\n return {\n table(name, columns) {\n return declareTable(state, name, columns);\n },\n enum(name, values) {\n return enumColumn(name, values);\n },\n };\n}\n\n/** Require every exact table handle once under its declared identity. */\nfunction validateReturnedTables(\n returned: unknown,\n state: DeclarationState,\n): void {\n assertRecord(returned, \"defineSchema() return value\");\n const returnedHandles = new Set<object>();\n for (const [key, value] of Object.entries(returned)) {\n const name =\n value !== null && typeof value === \"object\"\n ? state.handleNames.get(value)\n : undefined;\n if (!name) {\n throw new SchemaBuildError(\n `defineSchema returned a value for \"${key}\" that was not produced by ctx.table()`,\n );\n }\n if (key !== name) {\n throw new SchemaBuildError(\n `defineSchema returned table \"${name}\" under key \"${key}\"; aliases are not supported`,\n );\n }\n if (returnedHandles.has(value)) {\n throw new SchemaBuildError(`Table \"${name}\" was returned more than once`);\n }\n returnedHandles.add(value);\n }\n if (returnedHandles.size !== state.raw.size) {\n const omitted = [...state.raw.values()]\n .filter((table) => !returnedHandles.has(table.handle))\n .map((table) => table.name);\n throw new SchemaBuildError(\n `defineSchema omitted declared table${omitted.length === 1 ? \"\" : \"s\"}: ${omitted.join(\", \")}`,\n );\n }\n}\n\n/** Detect declaration-handle tampering before metadata publication. */\nfunction validateHandles(raw: ReadonlyMap<string, RawTable>): void {\n for (const table of raw.values()) {\n const ownKeys = Reflect.ownKeys(table.handle);\n const hasOnlyDeclaredColumns =\n ownKeys.length === Object.keys(table.metas).length &&\n ownKeys.every(\n (key) => typeof key === \"string\" && Object.hasOwn(table.metas, key),\n );\n const hasMetadataCollision = TABLE_METADATA_KEYS.some((key) =>\n Object.hasOwn(table.handle, key),\n );\n if (\n Object.getPrototypeOf(table.handle) !== null ||\n !Object.isExtensible(table.handle) ||\n !hasOnlyDeclaredColumns ||\n hasMetadataCollision\n ) {\n throw new SchemaBuildError(\n `Table handle \"${table.name}\" was modified during schema declaration`,\n );\n }\n }\n}\n\n/** Reject composite primary keys, which keyed operations cannot represent. */\nfunction validatePrimaryKeys(raw: ReadonlyMap<string, RawTable>): void {\n for (const table of raw.values()) {\n const primaryKeys = Object.values(table.metas).filter(\n (meta) => meta.primaryKey,\n );\n if (primaryKeys.length > 1) {\n throw new SchemaBuildError(\n `Table \"${table.name}\" declares multiple primary-key columns; composite primary keys are not supported`,\n );\n }\n }\n}\n\n/** Reject table names that collide with generated Drizzle relation keys. */\nfunction validateRelationKeys(\n raw: ReadonlyMap<string, RawTable>,\n relations: ReadonlyMap<string, readonly ResolvedRelation[]>,\n): void {\n for (const [name, tableRelations] of relations) {\n if (tableRelations.length > 0 && raw.has(`${name}Relations`)) {\n throw new SchemaBuildError(\n `Table \"${name}Relations\" collides with generated relation metadata for \"${name}\"`,\n );\n }\n }\n}\n\n/** Prepare every engine object and validator without mutating handles. */\nfunction prepareTables(\n raw: ReadonlyMap<string, RawTable>,\n schemaName: string,\n relations: ReadonlyMap<string, readonly ResolvedRelation[]>,\n): Map<string, FinalizedTableCandidate> {\n const built = buildEngineTables(raw.values(), schemaName);\n const candidates = new Map<string, FinalizedTableCandidate>();\n for (const table of raw.values()) {\n const builtTable = built.get(table.name);\n if (!builtTable) {\n throw new SchemaBuildError(\n `Engine table \"${table.name}\" was not constructed`,\n );\n }\n const columns = nullRecord<ColumnMeta>();\n for (const [key, meta] of Object.entries(builtTable.columns)) {\n columns[key] = finalizeColumn(meta);\n }\n Object.freeze(columns);\n const tableRelations = finalizeRelations(relations.get(table.name) ?? []);\n const validatorTable = { $columns: columns } as AppKitTable;\n candidates.set(table.name, {\n columns,\n engine: builtTable.engine,\n relations: tableRelations,\n insertSchema: deriveInsertSchema(validatorTable),\n updateSchema: deriveUpdateSchema(validatorTable),\n });\n }\n return candidates;\n}\n\n/** Atomically publish prepared metadata as the final schema transition. */\nfunction publishSchema(\n raw: ReadonlyMap<string, RawTable>,\n candidates: ReadonlyMap<string, FinalizedTableCandidate>,\n schemaName: string,\n): Schema {\n const publications = [...raw.values()].map((table) => {\n const candidate = candidates.get(table.name);\n if (!candidate) {\n throw new SchemaBuildError(\n `Table \"${table.name}\" was not prepared for finalization`,\n );\n }\n return { table, candidate };\n });\n\n const tables = nullRecord<AppKitTable>();\n const engine = nullRecord<Schema[\"$engine\"][string]>();\n for (const { table, candidate } of publications) {\n Object.defineProperties(table.handle, {\n $name: { value: table.name },\n $schemaName: { value: schemaName },\n $columns: { value: candidate.columns },\n $engine: { value: candidate.engine },\n $relations: { value: candidate.relations },\n $insertSchema: { value: candidate.insertSchema },\n $updateSchema: { value: candidate.updateSchema },\n });\n const finalized = Object.freeze(table.handle) as unknown as AppKitTable;\n tables[table.name] = finalized;\n engine[table.name] = candidate.engine;\n }\n\n const schema = {\n $schemaName: schemaName,\n $tables: Object.freeze(tables),\n $engine: Object.freeze(engine),\n };\n Object.defineProperty(schema, FINALIZED_SCHEMA, { value: true });\n return Object.freeze(schema);\n}\n\n/** @internal Reject values that were not finalized by `defineSchema()`. */\nexport function assertFinalizedSchema(value: unknown): asserts value is Schema {\n if (\n value === null ||\n typeof value !== \"object\" ||\n (value as Record<PropertyKey, unknown>)[FINALIZED_SCHEMA] !== true\n ) {\n throw new TypeError(\"Expected a finalized AppKit database schema\");\n }\n}\n\nexport function defineSchema(\n builder: (context: SchemaBuilderContext) => Record<string, AppKitTable>,\n options?: DefineSchemaOptions,\n): Schema {\n const schemaName = options?.schemaName ?? \"public\";\n if (!schemaName) throw new SchemaBuildError(\"Schema name cannot be empty\");\n\n const state: DeclarationState = {\n raw: new Map(),\n handleNames: new Map(),\n };\n const returned = builder(createBuilderContext(state));\n\n validateReturnedTables(returned, state);\n validateHandles(state.raw);\n validatePrimaryKeys(state.raw);\n resolveForeignKeys(state.raw);\n // FK literals are checked against the inherited target storage, not the placeholder.\n validateLiteralDefaults(state.raw.values());\n\n const relations = buildRelations(state.raw);\n validateRelationKeys(state.raw, relations);\n const candidates = prepareTables(state.raw, schemaName, relations);\n return publishSchema(state.raw, candidates, schemaName);\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"define-schema.js","names":[],"sources":["../../../src/database/schema-builder/define-schema.ts"],"sourcesContent":["import { ColumnBuilder, enumColumn, validateLiteralDefaults } from \"./columns\";\nimport { buildEngineTables } from \"./engine/tables\";\nimport { resolveForeignKeys } from \"./fk\";\nimport { buildRelations } from \"./relations\";\nimport {\n type AppKitTable,\n type ColumnMeta,\n type ColumnRef,\n type DefineSchemaOptions,\n type MutableColumnMeta,\n type ResolvedRelation,\n type Schema,\n SchemaBuildError,\n type TableHandle,\n} from \"./types\";\nimport { deriveInsertSchema, deriveUpdateSchema } from \"./validators\";\n\ninterface RawTable {\n readonly name: string;\n readonly metas: Record<string, MutableColumnMeta>;\n readonly handle: Record<string, ColumnRef>;\n}\n\ninterface FinalizedTableCandidate {\n readonly columns: Readonly<Record<string, ColumnMeta>>;\n readonly engine: Schema[\"$engine\"][string];\n readonly relations: readonly ResolvedRelation[];\n readonly insertSchema: unknown;\n readonly updateSchema: unknown;\n}\n\ninterface DeclarationState {\n readonly raw: Map<string, RawTable>;\n readonly handleNames: Map<object, string>;\n}\n\nexport interface SchemaBuilderContext {\n table<C extends Record<string, ColumnBuilder>>(\n name: string,\n columns: C,\n ): TableHandle<C>;\n enum(name: string, values: readonly string[]): ColumnBuilder;\n}\n\nconst RESERVED_OBJECT_KEYS = new Set([\"__proto__\", \"prototype\", \"constructor\"]);\nconst RESERVED_DATABASE_EXPORT_KEYS = new Set([\"sql\", \"transaction\"]);\n// A global symbol lets typegen recognize schemas loaded through another AppKit instance.\nconst FINALIZED_SCHEMA = Symbol.for(\"@databricks/appkit.database.schema\");\nconst TABLE_METADATA_KEYS = [\n \"$name\",\n \"$schemaName\",\n \"$columns\",\n \"$engine\",\n \"$relations\",\n \"$insertSchema\",\n \"$updateSchema\",\n] as const;\n\nfunction nullRecord<T>(): Record<string, T> {\n return Object.create(null) as Record<string, T>;\n}\n\nfunction assertRecord(value: unknown, label: string): asserts value is object {\n if (value === null || typeof value !== \"object\" || Array.isArray(value)) {\n throw new SchemaBuildError(`${label} must be an object`);\n }\n}\n\nfunction assertName(value: string, label: string): void {\n if (!value || RESERVED_OBJECT_KEYS.has(value)) {\n throw new SchemaBuildError(`${label} \"${value}\" is reserved`);\n }\n}\n\nfunction assertTableName(value: string): void {\n assertName(value, \"Table name\");\n if (RESERVED_DATABASE_EXPORT_KEYS.has(value)) {\n throw new SchemaBuildError(`Table name \"${value}\" is reserved`);\n }\n}\n\nfunction finalizeColumn(meta: MutableColumnMeta): ColumnMeta {\n if (!meta.engineColumn) {\n throw new SchemaBuildError(\n `Engine column \"${meta.columnName}\" is missing during finalization`,\n );\n }\n const { fkRef: _fkRef, ...published } = meta;\n return Object.freeze({\n ...published,\n enumValues: meta.enumValues\n ? Object.freeze([...meta.enumValues])\n : undefined,\n fk: meta.fk ? Object.freeze({ ...meta.fk }) : undefined,\n engineColumn: meta.engineColumn,\n }) as ColumnMeta;\n}\n\nfunction finalizeRelations(\n relations: readonly ResolvedRelation[],\n): readonly ResolvedRelation[] {\n return Object.freeze(\n relations.map((relation) => Object.freeze({ ...relation })),\n );\n}\n\n/** Clone builders into per-table metadata and create stable column handles. */\nfunction declareTable<C extends Record<string, ColumnBuilder>>(\n state: DeclarationState,\n name: string,\n columns: C,\n): TableHandle<C> {\n assertTableName(name);\n if (state.raw.has(name)) {\n throw new SchemaBuildError(`Duplicate table \"${name}\"`);\n }\n assertRecord(columns, `Columns for table \"${name}\"`);\n\n const metas = nullRecord<MutableColumnMeta>();\n const handle = Object.create(null) as Record<string, ColumnRef>;\n for (const [key, column] of Object.entries(columns)) {\n assertName(key, `Column name on table \"${name}\"`);\n if (key.startsWith(\"$\") || key === \"and\" || key === \"or\") {\n throw new SchemaBuildError(\n `Column \"${name}.${key}\" collides with AppKit runtime metadata`,\n );\n }\n if (!(column instanceof ColumnBuilder)) {\n throw new SchemaBuildError(\n `Column \"${name}.${key}\" was not created by an AppKit column builder`,\n );\n }\n\n const meta = column._cloneMeta();\n meta.name = key;\n meta.columnName = key;\n metas[key] = meta;\n\n const reference: ColumnRef = Object.freeze({\n __isColumnRef: true,\n tableName: name,\n columnName: key,\n });\n Object.defineProperty(handle, key, {\n enumerable: true,\n value: reference,\n });\n }\n\n state.raw.set(name, { name, metas, handle });\n state.handleNames.set(handle, name);\n return handle as unknown as TableHandle<C>;\n}\n\nfunction createBuilderContext(state: DeclarationState): SchemaBuilderContext {\n return {\n table(name, columns) {\n return declareTable(state, name, columns);\n },\n enum(name, values) {\n return enumColumn(name, values);\n },\n };\n}\n\n/** Require every exact table handle once under its declared identity. */\nfunction validateReturnedTables(\n returned: unknown,\n state: DeclarationState,\n): void {\n assertRecord(returned, \"defineSchema() return value\");\n const returnedHandles = new Set<object>();\n for (const [key, value] of Object.entries(returned)) {\n const name =\n value !== null && typeof value === \"object\"\n ? state.handleNames.get(value)\n : undefined;\n if (!name) {\n throw new SchemaBuildError(\n `defineSchema returned a value for \"${key}\" that was not produced by ctx.table()`,\n );\n }\n if (key !== name) {\n throw new SchemaBuildError(\n `defineSchema returned table \"${name}\" under key \"${key}\"; aliases are not supported`,\n );\n }\n if (returnedHandles.has(value)) {\n throw new SchemaBuildError(`Table \"${name}\" was returned more than once`);\n }\n returnedHandles.add(value);\n }\n if (returnedHandles.size !== state.raw.size) {\n const omitted = [...state.raw.values()]\n .filter((table) => !returnedHandles.has(table.handle))\n .map((table) => table.name);\n throw new SchemaBuildError(\n `defineSchema omitted declared table${omitted.length === 1 ? \"\" : \"s\"}: ${omitted.join(\", \")}`,\n );\n }\n}\n\n/** Detect declaration-handle tampering before metadata publication. */\nfunction validateHandles(raw: ReadonlyMap<string, RawTable>): void {\n for (const table of raw.values()) {\n const ownKeys = Reflect.ownKeys(table.handle);\n const hasOnlyDeclaredColumns =\n ownKeys.length === Object.keys(table.metas).length &&\n ownKeys.every(\n (key) => typeof key === \"string\" && Object.hasOwn(table.metas, key),\n );\n const hasMetadataCollision = TABLE_METADATA_KEYS.some((key) =>\n Object.hasOwn(table.handle, key),\n );\n if (\n Object.getPrototypeOf(table.handle) !== null ||\n !Object.isExtensible(table.handle) ||\n !hasOnlyDeclaredColumns ||\n hasMetadataCollision\n ) {\n throw new SchemaBuildError(\n `Table handle \"${table.name}\" was modified during schema declaration`,\n );\n }\n }\n}\n\n/** Reject composite primary keys, which keyed operations cannot represent. */\nfunction validatePrimaryKeys(raw: ReadonlyMap<string, RawTable>): void {\n for (const table of raw.values()) {\n const primaryKeys = Object.values(table.metas).filter(\n (meta) => meta.primaryKey,\n );\n if (primaryKeys.length > 1) {\n throw new SchemaBuildError(\n `Table \"${table.name}\" declares multiple primary-key columns; composite primary keys are not supported`,\n );\n }\n }\n}\n\n/** Reject table names that collide with generated Drizzle relation keys. */\nfunction validateRelationKeys(\n raw: ReadonlyMap<string, RawTable>,\n relations: ReadonlyMap<string, readonly ResolvedRelation[]>,\n): void {\n for (const [name, tableRelations] of relations) {\n if (tableRelations.length > 0 && raw.has(`${name}Relations`)) {\n throw new SchemaBuildError(\n `Table \"${name}Relations\" collides with generated relation metadata for \"${name}\"`,\n );\n }\n }\n}\n\n/** Prepare every engine object and validator without mutating handles. */\nfunction prepareTables(\n raw: ReadonlyMap<string, RawTable>,\n schemaName: string,\n relations: ReadonlyMap<string, readonly ResolvedRelation[]>,\n): Map<string, FinalizedTableCandidate> {\n const built = buildEngineTables(raw.values(), schemaName);\n const candidates = new Map<string, FinalizedTableCandidate>();\n for (const table of raw.values()) {\n const builtTable = built.get(table.name);\n if (!builtTable) {\n throw new SchemaBuildError(\n `Engine table \"${table.name}\" was not constructed`,\n );\n }\n const columns = nullRecord<ColumnMeta>();\n for (const [key, meta] of Object.entries(builtTable.columns)) {\n columns[key] = finalizeColumn(meta);\n }\n Object.freeze(columns);\n const tableRelations = finalizeRelations(relations.get(table.name) ?? []);\n const validatorTable = { $columns: columns } as AppKitTable;\n candidates.set(table.name, {\n columns,\n engine: builtTable.engine,\n relations: tableRelations,\n insertSchema: deriveInsertSchema(validatorTable),\n updateSchema: deriveUpdateSchema(validatorTable),\n });\n }\n return candidates;\n}\n\n/** Atomically publish prepared metadata as the final schema transition. */\nfunction publishSchema(\n raw: ReadonlyMap<string, RawTable>,\n candidates: ReadonlyMap<string, FinalizedTableCandidate>,\n schemaName: string,\n): Schema {\n const publications = [...raw.values()].map((table) => {\n const candidate = candidates.get(table.name);\n if (!candidate) {\n throw new SchemaBuildError(\n `Table \"${table.name}\" was not prepared for finalization`,\n );\n }\n return { table, candidate };\n });\n\n const tables = nullRecord<AppKitTable>();\n const engine = nullRecord<Schema[\"$engine\"][string]>();\n for (const { table, candidate } of publications) {\n Object.defineProperties(table.handle, {\n $name: { value: table.name },\n $schemaName: { value: schemaName },\n $columns: { value: candidate.columns },\n $engine: { value: candidate.engine },\n $relations: { value: candidate.relations },\n $insertSchema: { value: candidate.insertSchema },\n $updateSchema: { value: candidate.updateSchema },\n });\n const finalized = Object.freeze(table.handle) as unknown as AppKitTable;\n tables[table.name] = finalized;\n engine[table.name] = candidate.engine;\n }\n\n const schema = {\n $schemaName: schemaName,\n $tables: Object.freeze(tables),\n $engine: Object.freeze(engine),\n };\n Object.defineProperty(schema, FINALIZED_SCHEMA, { value: true });\n return Object.freeze(schema);\n}\n\n/** @internal Reject values that were not finalized by `defineSchema()`. */\nexport function assertFinalizedSchema(value: unknown): asserts value is Schema {\n if (\n value === null ||\n typeof value !== \"object\" ||\n (value as Record<PropertyKey, unknown>)[FINALIZED_SCHEMA] !== true\n ) {\n throw new TypeError(\"Expected a finalized AppKit database schema\");\n }\n}\n\n/**\n * Compile one declared schema. The returned type keeps the table names the\n * builder returned, so `crudRoutes` and `hooks` can name only real tables.\n */\nexport function defineSchema<TTables extends Record<string, AppKitTable>>(\n builder: (context: SchemaBuilderContext) => TTables,\n options?: DefineSchemaOptions,\n): Schema<Extract<keyof TTables, string>> {\n const schemaName = options?.schemaName ?? \"public\";\n if (!schemaName) throw new SchemaBuildError(\"Schema name cannot be empty\");\n\n const state: DeclarationState = {\n raw: new Map(),\n handleNames: new Map(),\n };\n const returned = builder(createBuilderContext(state));\n\n validateReturnedTables(returned, state);\n validateHandles(state.raw);\n validatePrimaryKeys(state.raw);\n resolveForeignKeys(state.raw);\n // FK literals are checked against the inherited target storage, not the placeholder.\n validateLiteralDefaults(state.raw.values());\n\n const relations = buildRelations(state.raw);\n validateRelationKeys(state.raw, relations);\n const candidates = prepareTables(state.raw, schemaName, relations);\n return publishSchema(state.raw, candidates, schemaName);\n}\n"],"mappings":"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"}
@@ -111,9 +111,14 @@ type TableHandle<C extends Record<string, unknown>> = AppKitTable & { readonly [
111
111
  interface DefineSchemaOptions {
112
112
  readonly schemaName?: string;
113
113
  }
114
- interface Schema {
114
+ /**
115
+ * One finalized schema. `TTableName` keeps the declared names in the type, so
116
+ * configuration that addresses a table by name is checked against the schema
117
+ * it was written for. Code that accepts any schema uses the default.
118
+ */
119
+ interface Schema<TTableName extends string = string> {
115
120
  readonly $schemaName: string;
116
- readonly $tables: Readonly<Record<string, AppKitTable>>;
121
+ readonly $tables: Readonly<Record<TTableName, AppKitTable>>;
117
122
  readonly $engine: Readonly<Record<string, EngineTable>>;
118
123
  }
119
124
  //#endregion