@databricks/appkit 0.67.0 → 0.69.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 (76) hide show
  1. package/CLAUDE.md +2 -2
  2. package/dist/appkit/package.js +1 -1
  3. package/dist/core/agent/skills/types.d.ts +6 -1
  4. package/dist/core/agent/skills/types.d.ts.map +1 -1
  5. package/dist/database/contract/index.js +1 -1
  6. package/dist/database/contract/wire.d.ts.map +1 -1
  7. package/dist/database/contract/wire.js +5 -1
  8. package/dist/database/contract/wire.js.map +1 -1
  9. package/dist/database/errors.js +39 -3
  10. package/dist/database/errors.js.map +1 -1
  11. package/dist/database/runtime/data-path.js.map +1 -1
  12. package/dist/database/runtime/engine/drizzle-data-path.js +1 -1
  13. package/dist/database/runtime/engine/drizzle-data-path.js.map +1 -1
  14. package/dist/database/runtime/engine/translate.js +9 -2
  15. package/dist/database/runtime/engine/translate.js.map +1 -1
  16. package/dist/database/schema-builder/define-schema.d.ts +5 -1
  17. package/dist/database/schema-builder/define-schema.d.ts.map +1 -1
  18. package/dist/database/schema-builder/define-schema.js +4 -0
  19. package/dist/database/schema-builder/define-schema.js.map +1 -1
  20. package/dist/database/schema-builder/types.d.ts +7 -2
  21. package/dist/database/schema-builder/types.d.ts.map +1 -1
  22. package/dist/database/schema-builder/types.js.map +1 -1
  23. package/dist/plugins/agents/active-stream-tracker.js +52 -0
  24. package/dist/plugins/agents/active-stream-tracker.js.map +1 -0
  25. package/dist/plugins/agents/adapter-extensions.js +50 -0
  26. package/dist/plugins/agents/adapter-extensions.js.map +1 -0
  27. package/dist/plugins/agents/agents.d.ts +6 -77
  28. package/dist/plugins/agents/agents.d.ts.map +1 -1
  29. package/dist/plugins/agents/agents.js +57 -431
  30. package/dist/plugins/agents/agents.js.map +1 -1
  31. package/dist/plugins/agents/approval.js +28 -0
  32. package/dist/plugins/agents/approval.js.map +1 -0
  33. package/dist/plugins/agents/builtin-tools.js +39 -0
  34. package/dist/plugins/agents/builtin-tools.js.map +1 -0
  35. package/dist/plugins/agents/prompt.js +27 -0
  36. package/dist/plugins/agents/prompt.js.map +1 -0
  37. package/dist/plugins/agents/registry.js +56 -0
  38. package/dist/plugins/agents/registry.js.map +1 -0
  39. package/dist/plugins/agents/resolve-config.js +40 -0
  40. package/dist/plugins/agents/resolve-config.js.map +1 -0
  41. package/dist/plugins/agents/skill-loader.js +139 -0
  42. package/dist/plugins/agents/skill-loader.js.map +1 -0
  43. package/dist/plugins/agents/tool-dispatch.js +124 -0
  44. package/dist/plugins/agents/tool-dispatch.js.map +1 -0
  45. package/dist/plugins/database/crud/codecs.js +78 -0
  46. package/dist/plugins/database/crud/codecs.js.map +1 -0
  47. package/dist/plugins/database/crud/contract.js +151 -0
  48. package/dist/plugins/database/crud/contract.js.map +1 -0
  49. package/dist/plugins/database/crud/exposure.js +40 -0
  50. package/dist/plugins/database/crud/exposure.js.map +1 -0
  51. package/dist/plugins/database/crud/query.js +244 -0
  52. package/dist/plugins/database/crud/query.js.map +1 -0
  53. package/dist/plugins/database/crud/routes.js +124 -0
  54. package/dist/plugins/database/crud/routes.js.map +1 -0
  55. package/dist/plugins/database/database.d.ts +6 -0
  56. package/dist/plugins/database/database.d.ts.map +1 -1
  57. package/dist/plugins/database/database.js +58 -4
  58. package/dist/plugins/database/database.js.map +1 -1
  59. package/dist/plugins/database/defaults.js +28 -1
  60. package/dist/plugins/database/defaults.js.map +1 -1
  61. package/dist/plugins/database/entity-client.js +1 -1
  62. package/dist/plugins/database/entity-types.d.ts +13 -4
  63. package/dist/plugins/database/entity-types.d.ts.map +1 -1
  64. package/dist/plugins/database/lifecycle.js +2 -1
  65. package/dist/plugins/database/lifecycle.js.map +1 -1
  66. package/dist/plugins/database/types.d.ts +32 -0
  67. package/dist/plugins/database/types.d.ts.map +1 -1
  68. package/dist/shared/src/schemas/manifest.d.ts +54 -54
  69. package/docs/api/appkit/Function.database.md +3 -3
  70. package/docs/api/appkit/Function.defineSchema.md +14 -6
  71. package/docs/api/appkit/Interface.Schema.md +10 -2
  72. package/docs/api/appkit/TypeAlias.IDatabaseConfig.md +20 -0
  73. package/docs/api/appkit.md +2 -2
  74. package/llms.txt +2 -2
  75. package/package.json +1 -1
  76. package/sbom.cdx.json +1 -1
@@ -0,0 +1,151 @@
1
+ import { filterOperatorsForKind } from "../../../database/schema-builder/types.js";
2
+ import { DatabasePluginError, invalidDatabaseInput } from "../../../database/errors.js";
3
+ import { MAX_SERIALIZED_DEPTH, MAX_SERIALIZED_NODES } from "../defaults.js";
4
+ import { compileColumn } from "./codecs.js";
5
+
6
+ //#region src/plugins/database/crud/contract.ts
7
+ /** A bare object literal; a `Date`, class instance, or `Map` is not JSON. */
8
+ function isPlainObject(value) {
9
+ if (value === null || typeof value !== "object" || Array.isArray(value)) return false;
10
+ const prototype = Object.getPrototypeOf(value);
11
+ return prototype === Object.prototype || prototype === null;
12
+ }
13
+ /** A serializer that breaks its contract is trusted code failing, not input. */
14
+ function serializerFault() {
15
+ throw new DatabasePluginError("INTERNAL", "read");
16
+ }
17
+ /** Charge one value against the output budget before descending into it. */
18
+ function countNode(depth, state) {
19
+ state.nodes += 1;
20
+ if (state.nodes > MAX_SERIALIZED_NODES || depth > MAX_SERIALIZED_DEPTH) serializerFault();
21
+ }
22
+ /** Walk a container while its ancestors are tracked, so a cycle cannot pass. */
23
+ function enterObject(value, state, visit) {
24
+ if (state.ancestors.has(value)) serializerFault();
25
+ state.ancestors.add(value);
26
+ try {
27
+ return visit();
28
+ } finally {
29
+ state.ancestors.delete(value);
30
+ }
31
+ }
32
+ /** Accept a serializer's own added value only where it is already JSON. */
33
+ function sanitizeJson(value, depth, state) {
34
+ countNode(depth, state);
35
+ if (value === null || typeof value === "string" || typeof value === "boolean") return value;
36
+ if (typeof value === "number") {
37
+ if (!Number.isFinite(value)) serializerFault();
38
+ return value;
39
+ }
40
+ if (Array.isArray(value)) return enterObject(value, state, () => value.map((item) => sanitizeJson(item, depth + 1, state)));
41
+ if (!isPlainObject(value)) serializerFault();
42
+ return enterObject(value, state, () => {
43
+ const out = Object.create(null);
44
+ for (const [key, child] of Object.entries(value)) {
45
+ if (child === void 0) continue;
46
+ out[key] = sanitizeJson(child, depth + 1, state);
47
+ }
48
+ return out;
49
+ });
50
+ }
51
+ /** Keep an included row under its own table's policy, one row or many. */
52
+ function sanitizeRelation(target, value, depth, state) {
53
+ if (value === null) return null;
54
+ if (!Array.isArray(value)) return sanitizeRow(target, value, depth, state);
55
+ countNode(depth, state);
56
+ return enterObject(value, state, () => value.map((row) => sanitizeRow(target, row, depth + 1, state)));
57
+ }
58
+ /** Re-apply the private-column policy wherever the output stays contracted. */
59
+ function sanitizeRow(table, row, depth, state) {
60
+ countNode(depth, state);
61
+ if (!isPlainObject(row)) serializerFault();
62
+ return enterObject(row, state, () => {
63
+ const out = {};
64
+ for (const [key, child] of Object.entries(row)) {
65
+ if (child === void 0) continue;
66
+ if (table.columns.get(key)?.meta.isPrivate) continue;
67
+ const relation = table.relations.get(key);
68
+ out[key] = relation ? sanitizeRelation(relation.target, child, depth + 1, state) : sanitizeJson(child, depth + 1, state);
69
+ }
70
+ return out;
71
+ });
72
+ }
73
+ /** Project an included row through its own table; absent to-one reads null. */
74
+ function projectRelation(target, value) {
75
+ if (value === null || value === void 0) return null;
76
+ return Array.isArray(value) ? value.map((row) => target.projectPublicRow(row)) : target.projectPublicRow(value);
77
+ }
78
+ /** Build the public JSON for one driver row, dropping anything uncontracted. */
79
+ function projectRow(table, row) {
80
+ const out = {};
81
+ for (const [key, value] of Object.entries(row)) {
82
+ const column = table.columns.get(key);
83
+ if (column) {
84
+ if (!column.meta.isPrivate) out[key] = column.encode(value);
85
+ continue;
86
+ }
87
+ const relation = table.relations.get(key);
88
+ if (relation) out[key] = projectRelation(relation.target, value);
89
+ }
90
+ return out;
91
+ }
92
+ /** Compile one table's allowlists and codecs from its finalized metadata. */
93
+ function compileTable(table) {
94
+ const columns = /* @__PURE__ */ new Map();
95
+ const selectable = /* @__PURE__ */ new Set();
96
+ const queryable = /* @__PURE__ */ new Set();
97
+ let primaryKey;
98
+ for (const meta of Object.values(table.$columns)) {
99
+ const column = compileColumn(meta);
100
+ columns.set(meta.columnName, column);
101
+ if (meta.primaryKey && !meta.isPrivate) primaryKey = column;
102
+ if (meta.isPrivate) continue;
103
+ selectable.add(meta.columnName);
104
+ if (filterOperatorsForKind(meta.kind).length > 0) queryable.add(meta.columnName);
105
+ }
106
+ const compiled = {
107
+ name: table.$name,
108
+ primaryKey,
109
+ columns,
110
+ selectable,
111
+ queryable,
112
+ relations: /* @__PURE__ */ new Map(),
113
+ decodeId: (raw) => {
114
+ if (!primaryKey) throw new DatabasePluginError("INTERNAL", "read");
115
+ const value = primaryKey.decode(raw);
116
+ if (typeof value !== "string" && typeof value !== "number" && typeof value !== "bigint") throw invalidDatabaseInput(["id"], "Not a valid identifier");
117
+ return value;
118
+ },
119
+ projectPublicRow: (row) => projectRow(compiled, row),
120
+ sanitizeSerializedRow: (row) => sanitizeRow(compiled, row, 0, {
121
+ nodes: 0,
122
+ ancestors: /* @__PURE__ */ new Set()
123
+ })
124
+ };
125
+ return compiled;
126
+ }
127
+ /**
128
+ * Compile the HTTP contract for every exposed table and wire the relations
129
+ * they share. A relation whose target is not exposed stays unreachable, so
130
+ * enabling one table never widens another table's public surface.
131
+ */
132
+ function compileCrudTables(tables) {
133
+ const compiled = /* @__PURE__ */ new Map();
134
+ for (const table of Object.values(tables)) compiled.set(table.$name, compileTable(table));
135
+ for (const table of Object.values(tables)) {
136
+ const entry = compiled.get(table.$name);
137
+ for (const relation of table.$relations) {
138
+ const target = compiled.get(relation.targetTable);
139
+ if (!entry || !target) continue;
140
+ entry.relations.set(relation.name, {
141
+ cardinality: relation.cardinality,
142
+ target
143
+ });
144
+ }
145
+ }
146
+ return compiled;
147
+ }
148
+
149
+ //#endregion
150
+ export { compileCrudTables };
151
+ //# sourceMappingURL=contract.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"contract.js","names":[],"sources":["../../../../src/plugins/database/crud/contract.ts"],"sourcesContent":["import {\n DatabasePluginError,\n invalidDatabaseInput,\n} from \"../../../database/errors\";\nimport type { IdValue, Row } from \"../../../database/runtime\";\nimport type { AppKitTable } from \"../../../database/schema-builder\";\nimport { filterOperatorsForKind } from \"../../../database/schema-builder/types\";\nimport { MAX_SERIALIZED_DEPTH, MAX_SERIALIZED_NODES } from \"../defaults\";\nimport { type CompiledColumn, compileColumn, type JsonValue } from \"./codecs\";\n\n/** One relation edge wired to the contract of its target table. */\nexport interface CrudRelation {\n readonly cardinality: \"toOne\" | \"toMany\";\n readonly target: CrudTable;\n}\n\n/** Private HTTP contract compiled once for one explicitly exposed table. */\nexport interface CrudTable {\n readonly name: string;\n readonly primaryKey?: CompiledColumn;\n readonly columns: ReadonlyMap<string, CompiledColumn>;\n /** Public columns a request may project. */\n readonly selectable: ReadonlySet<string>;\n /** Public columns a request may filter or order by. */\n readonly queryable: ReadonlySet<string>;\n readonly relations: ReadonlyMap<string, CrudRelation>;\n decodeId(raw: string): IdValue;\n projectPublicRow(row: Row): JsonValue;\n sanitizeSerializedRow(row: unknown): JsonValue;\n}\n\ntype MutableCrudTable = Omit<CrudTable, \"relations\"> & {\n readonly relations: Map<string, CrudRelation>;\n};\n\ninterface SanitizeState {\n nodes: number;\n readonly ancestors: Set<object>;\n}\n\n/** A bare object literal; a `Date`, class instance, or `Map` is not JSON. */\nfunction isPlainObject(value: unknown): value is Record<string, unknown> {\n if (value === null || typeof value !== \"object\" || Array.isArray(value)) {\n return false;\n }\n const prototype = Object.getPrototypeOf(value);\n return prototype === Object.prototype || prototype === null;\n}\n\n/** A serializer that breaks its contract is trusted code failing, not input. */\nfunction serializerFault(): never {\n throw new DatabasePluginError(\"INTERNAL\", \"read\");\n}\n\n/** Charge one value against the output budget before descending into it. */\nfunction countNode(depth: number, state: SanitizeState): void {\n state.nodes += 1;\n if (state.nodes > MAX_SERIALIZED_NODES || depth > MAX_SERIALIZED_DEPTH) {\n serializerFault();\n }\n}\n\n/** Walk a container while its ancestors are tracked, so a cycle cannot pass. */\nfunction enterObject<T>(\n value: object,\n state: SanitizeState,\n visit: () => T,\n): T {\n if (state.ancestors.has(value)) serializerFault();\n state.ancestors.add(value);\n try {\n return visit();\n } finally {\n state.ancestors.delete(value);\n }\n}\n\n/** Accept a serializer's own added value only where it is already JSON. */\nfunction sanitizeJson(\n value: unknown,\n depth: number,\n state: SanitizeState,\n): JsonValue {\n countNode(depth, state);\n if (\n value === null ||\n typeof value === \"string\" ||\n typeof value === \"boolean\"\n ) {\n return value;\n }\n if (typeof value === \"number\") {\n if (!Number.isFinite(value)) serializerFault();\n return value;\n }\n if (Array.isArray(value)) {\n return enterObject(value, state, () =>\n value.map((item) => sanitizeJson(item, depth + 1, state)),\n );\n }\n if (!isPlainObject(value)) serializerFault();\n return enterObject(value, state, () => {\n // A null prototype keeps `__proto__` an ordinary key instead of a setter.\n const out: Record<string, JsonValue> = Object.create(null);\n for (const [key, child] of Object.entries(value)) {\n if (child === undefined) continue;\n out[key] = sanitizeJson(child, depth + 1, state);\n }\n return out;\n });\n}\n\n/** Keep an included row under its own table's policy, one row or many. */\nfunction sanitizeRelation(\n target: CrudTable,\n value: unknown,\n depth: number,\n state: SanitizeState,\n): JsonValue {\n if (value === null) return null;\n if (!Array.isArray(value)) return sanitizeRow(target, value, depth, state);\n countNode(depth, state);\n return enterObject(value, state, () =>\n value.map((row) => sanitizeRow(target, row, depth + 1, state)),\n );\n}\n\n/** Re-apply the private-column policy wherever the output stays contracted. */\nfunction sanitizeRow(\n table: CrudTable,\n row: unknown,\n depth: number,\n state: SanitizeState,\n): JsonValue {\n countNode(depth, state);\n if (!isPlainObject(row)) serializerFault();\n return enterObject(row, state, () => {\n const out: Record<string, JsonValue> = {};\n for (const [key, child] of Object.entries(row)) {\n if (child === undefined) continue;\n if (table.columns.get(key)?.meta.isPrivate) continue;\n const relation = table.relations.get(key);\n out[key] = relation\n ? sanitizeRelation(relation.target, child, depth + 1, state)\n : sanitizeJson(child, depth + 1, state);\n }\n return out;\n });\n}\n\n/** Project an included row through its own table; absent to-one reads null. */\nfunction projectRelation(target: CrudTable, value: unknown): JsonValue {\n if (value === null || value === undefined) return null;\n return Array.isArray(value)\n ? value.map((row) => target.projectPublicRow(row as Row))\n : target.projectPublicRow(value as Row);\n}\n\n/** Build the public JSON for one driver row, dropping anything uncontracted. */\nfunction projectRow(table: CrudTable, row: Row): JsonValue {\n const out: Record<string, JsonValue> = {};\n for (const [key, value] of Object.entries(row)) {\n const column = table.columns.get(key);\n if (column) {\n // Whatever the driver returned, only public contracted columns ship.\n if (!column.meta.isPrivate) out[key] = column.encode(value);\n continue;\n }\n const relation = table.relations.get(key);\n if (relation) out[key] = projectRelation(relation.target, value);\n }\n return out;\n}\n\n/** Compile one table's allowlists and codecs from its finalized metadata. */\nfunction compileTable(table: AppKitTable): MutableCrudTable {\n const columns = new Map<string, CompiledColumn>();\n const selectable = new Set<string>();\n const queryable = new Set<string>();\n let primaryKey: CompiledColumn | undefined;\n\n for (const meta of Object.values(table.$columns)) {\n const column = compileColumn(meta);\n columns.set(meta.columnName, column);\n // A private key must not power `GET /:table/:id`: per-id probing would\n // answer 200 or 404 on an identifier the schema hides, so over HTTP the\n // table is keyless — no detail route, and lists must name their own order.\n if (meta.primaryKey && !meta.isPrivate) primaryKey = column;\n if (meta.isPrivate) continue;\n selectable.add(meta.columnName);\n if (filterOperatorsForKind(meta.kind).length > 0) {\n queryable.add(meta.columnName);\n }\n }\n\n const compiled: MutableCrudTable = {\n name: table.$name,\n primaryKey,\n columns,\n selectable,\n queryable,\n relations: new Map(),\n decodeId: (raw) => {\n if (!primaryKey) throw new DatabasePluginError(\"INTERNAL\", \"read\");\n const value = primaryKey.decode(raw);\n if (\n typeof value !== \"string\" &&\n typeof value !== \"number\" &&\n typeof value !== \"bigint\"\n ) {\n throw invalidDatabaseInput([\"id\"], \"Not a valid identifier\");\n }\n return value;\n },\n projectPublicRow: (row) => projectRow(compiled, row),\n sanitizeSerializedRow: (row) =>\n sanitizeRow(compiled, row, 0, { nodes: 0, ancestors: new Set() }),\n };\n return compiled;\n}\n\n/**\n * Compile the HTTP contract for every exposed table and wire the relations\n * they share. A relation whose target is not exposed stays unreachable, so\n * enabling one table never widens another table's public surface.\n */\nexport function compileCrudTables(\n tables: Record<string, AppKitTable>,\n): Map<string, CrudTable> {\n const compiled = new Map<string, MutableCrudTable>();\n for (const table of Object.values(tables)) {\n compiled.set(table.$name, compileTable(table));\n }\n for (const table of Object.values(tables)) {\n const entry = compiled.get(table.$name);\n for (const relation of table.$relations) {\n const target = compiled.get(relation.targetTable);\n if (!entry || !target) continue;\n entry.relations.set(relation.name, {\n cardinality: relation.cardinality,\n target,\n });\n }\n }\n return compiled as Map<string, CrudTable>;\n}\n"],"mappings":"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"}
@@ -0,0 +1,40 @@
1
+ import { databaseSetupFailed } from "../../../database/errors.js";
2
+
3
+ //#region src/plugins/database/crud/exposure.ts
4
+ /** A table name also becomes a URL path segment, so keep it unambiguous. */
5
+ const ROUTABLE_TABLE = /^[A-Za-z][A-Za-z0-9_-]{0,63}$/;
6
+ /** Refuse names that cannot address exactly one table over HTTP. */
7
+ function assertRoutable(names) {
8
+ const lowercased = /* @__PURE__ */ new Set();
9
+ for (const name of names) {
10
+ if (!ROUTABLE_TABLE.test(name) || lowercased.has(name.toLowerCase())) throw databaseSetupFailed();
11
+ lowercased.add(name.toLowerCase());
12
+ }
13
+ }
14
+ /**
15
+ * Resolve the tables whose generated reads are explicitly turned on. The
16
+ * exposure value arrives untyped because a finalized schema widens its table
17
+ * names to `string`, so every name is re-checked against the declared schema.
18
+ */
19
+ function resolveExposedTables(exposure, declared) {
20
+ if (exposure === void 0 || exposure === false) return [];
21
+ if (exposure === true) {
22
+ assertRoutable(declared);
23
+ return [...declared];
24
+ }
25
+ if (typeof exposure !== "object" || exposure === null) throw databaseSetupFailed();
26
+ const requested = exposure.tables;
27
+ if (!Array.isArray(requested)) throw databaseSetupFailed();
28
+ const names = [];
29
+ for (const name of requested) {
30
+ if (typeof name !== "string" || !declared.includes(name)) throw databaseSetupFailed();
31
+ if (names.includes(name)) throw databaseSetupFailed();
32
+ names.push(name);
33
+ }
34
+ assertRoutable(names);
35
+ return names;
36
+ }
37
+
38
+ //#endregion
39
+ export { resolveExposedTables };
40
+ //# sourceMappingURL=exposure.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"exposure.js","names":[],"sources":["../../../../src/plugins/database/crud/exposure.ts"],"sourcesContent":["import { databaseSetupFailed } from \"../../../database/errors\";\n\n/** A table name also becomes a URL path segment, so keep it unambiguous. */\nconst ROUTABLE_TABLE = /^[A-Za-z][A-Za-z0-9_-]{0,63}$/;\n\n/** Refuse names that cannot address exactly one table over HTTP. */\nfunction assertRoutable(names: readonly string[]): void {\n const lowercased = new Set<string>();\n for (const name of names) {\n // Express matches paths case-insensitively, so near-duplicates would alias.\n if (!ROUTABLE_TABLE.test(name) || lowercased.has(name.toLowerCase())) {\n throw databaseSetupFailed();\n }\n lowercased.add(name.toLowerCase());\n }\n}\n\n/**\n * Resolve the tables whose generated reads are explicitly turned on. The\n * exposure value arrives untyped because a finalized schema widens its table\n * names to `string`, so every name is re-checked against the declared schema.\n */\nexport function resolveExposedTables(\n exposure: unknown,\n declared: readonly string[],\n): string[] {\n if (exposure === undefined || exposure === false) return [];\n if (exposure === true) {\n assertRoutable(declared);\n return [...declared];\n }\n if (typeof exposure !== \"object\" || exposure === null) {\n throw databaseSetupFailed();\n }\n const requested = (exposure as { tables?: unknown }).tables;\n if (!Array.isArray(requested)) throw databaseSetupFailed();\n\n const names: string[] = [];\n for (const name of requested) {\n // Unknown and duplicate names are configuration bugs, not empty routes.\n if (typeof name !== \"string\" || !declared.includes(name)) {\n throw databaseSetupFailed();\n }\n if (names.includes(name)) throw databaseSetupFailed();\n names.push(name);\n }\n assertRoutable(names);\n return names;\n}\n"],"mappings":";;;;AAGA,MAAM,iBAAiB;;AAGvB,SAAS,eAAe,OAAgC;CACtD,MAAM,6BAAa,IAAI,KAAa;AACpC,MAAK,MAAM,QAAQ,OAAO;AAExB,MAAI,CAAC,eAAe,KAAK,KAAK,IAAI,WAAW,IAAI,KAAK,aAAa,CAAC,CAClE,OAAM,qBAAqB;AAE7B,aAAW,IAAI,KAAK,aAAa,CAAC;;;;;;;;AAStC,SAAgB,qBACd,UACA,UACU;AACV,KAAI,aAAa,UAAa,aAAa,MAAO,QAAO,EAAE;AAC3D,KAAI,aAAa,MAAM;AACrB,iBAAe,SAAS;AACxB,SAAO,CAAC,GAAG,SAAS;;AAEtB,KAAI,OAAO,aAAa,YAAY,aAAa,KAC/C,OAAM,qBAAqB;CAE7B,MAAM,YAAa,SAAkC;AACrD,KAAI,CAAC,MAAM,QAAQ,UAAU,CAAE,OAAM,qBAAqB;CAE1D,MAAM,QAAkB,EAAE;AAC1B,MAAK,MAAM,QAAQ,WAAW;AAE5B,MAAI,OAAO,SAAS,YAAY,CAAC,SAAS,SAAS,KAAK,CACtD,OAAM,qBAAqB;AAE7B,MAAI,MAAM,SAAS,KAAK,CAAE,OAAM,qBAAqB;AACrD,QAAM,KAAK,KAAK;;AAElB,gBAAe,MAAM;AACrB,QAAO"}
@@ -0,0 +1,244 @@
1
+ import { filterOperatorsForKind } from "../../../database/schema-builder/types.js";
2
+ import { invalidDatabaseInput } from "../../../database/errors.js";
3
+ import { MAX_GROUP_ITEMS, MAX_MATERIALIZED_NODES, MAX_OFFSET, MAX_ORDER_FIELDS, MAX_QUERY_BYTES, MAX_WHERE_CONDITIONS, MAX_WHERE_DEPTH } from "../defaults.js";
4
+ import { DEFAULT_LIMIT, IN_CAP, MAX_INCLUDES, MAX_INCLUDE_DEPTH, MAX_INCLUDE_NODES, MAX_LIMIT, isFilterOperator } from "../../../database/contract/wire.js";
5
+ import "../../../database/contract/index.js";
6
+
7
+ //#region src/plugins/database/crud/query.ts
8
+ const LIST_PARAMS = new Set([
9
+ "where",
10
+ "order",
11
+ "select",
12
+ "include",
13
+ "limit",
14
+ "offset"
15
+ ]);
16
+ const DETAIL_PARAMS = new Set(["select", "include"]);
17
+ const CANONICAL_INT = /^-?(0|[1-9]\d*)$/;
18
+ /**
19
+ * Reject one query parameter. `parameter` is always a fixed name and `reason`
20
+ * a fixed sentence, so a rejection can never echo caller-supplied text back.
21
+ */
22
+ function reject(parameter, reason) {
23
+ throw invalidDatabaseInput([parameter], reason);
24
+ }
25
+ /** A decoded JSON object; arrays and null are separate wire shapes here. */
26
+ function isPlainObject(value) {
27
+ return typeof value === "object" && value !== null && !Array.isArray(value);
28
+ }
29
+ /** Read known, single-occurrence parameters from the raw query string. */
30
+ function parseParams(rawQuery, allowed) {
31
+ if (Buffer.byteLength(rawQuery, "utf8") > MAX_QUERY_BYTES) reject("query", "Query string exceeds the maximum size");
32
+ const params = /* @__PURE__ */ new Map();
33
+ for (const [key, value] of new URLSearchParams(rawQuery)) {
34
+ if (!allowed.has(key) || params.has(key)) reject("query", "Unknown or repeated query parameter");
35
+ params.set(key, value);
36
+ }
37
+ return params;
38
+ }
39
+ /** Parse one structured parameter without leaking the parser's diagnostics. */
40
+ function parseJson(raw, parameter) {
41
+ try {
42
+ return JSON.parse(raw);
43
+ } catch {
44
+ reject(parameter, "Expected JSON");
45
+ }
46
+ }
47
+ /** Accept one bounded integer, rejecting the forms `Number` would coerce. */
48
+ function decodeIntParam(raw, parameter, fallback, max) {
49
+ if (raw === void 0) return fallback;
50
+ if (!CANONICAL_INT.test(raw)) reject(parameter, "Expected a canonical decimal integer");
51
+ const value = Number(raw);
52
+ if (!Number.isSafeInteger(value) || value < 0 || value > max) reject(parameter, `Must be between 0 and ${max}`);
53
+ return value;
54
+ }
55
+ /** Narrow a projection to the table's public columns. */
56
+ function decodeSelect(table, value, parameter) {
57
+ if (!Array.isArray(value) || value.length === 0) reject(parameter, "Expected a non-empty array of public column names");
58
+ for (const name of value) if (typeof name !== "string" || !table.selectable.has(name)) reject(parameter, "Names an unknown or private column");
59
+ return value;
60
+ }
61
+ /** Order only by columns the table can sort on, in a declared direction. */
62
+ function decodeOrder(table, value, parameter) {
63
+ if (!isPlainObject(value)) reject(parameter, "Expected an object of column directions");
64
+ const entries = Object.entries(value);
65
+ if (entries.length === 0 || entries.length > MAX_ORDER_FIELDS) reject(parameter, `Expected 1 to ${MAX_ORDER_FIELDS} columns`);
66
+ const order = {};
67
+ for (const [column, direction] of entries) {
68
+ if (!table.queryable.has(column)) reject(parameter, "Names an unknown or unorderable column");
69
+ if (direction !== "asc" && direction !== "desc") reject(parameter, "Expected a direction of asc or desc");
70
+ order[column] = direction;
71
+ }
72
+ return order;
73
+ }
74
+ /** Move one filter operand onto its column's canonical runtime value. */
75
+ function decodeOperand(column, raw, parameter) {
76
+ const value = column.decode(raw);
77
+ if (value === void 0) reject(parameter, "Operand does not match its column type");
78
+ return value;
79
+ }
80
+ /**
81
+ * Decode the predicate on one column against the operator matrix its kind
82
+ * allows, counting conditions so a filter cannot grow without bound.
83
+ */
84
+ function decodeCondition(column, value, parameter, state) {
85
+ const countCondition = () => {
86
+ state.conditions += 1;
87
+ if (state.conditions > MAX_WHERE_CONDITIONS) reject(parameter, `Expected at most ${MAX_WHERE_CONDITIONS} conditions`);
88
+ };
89
+ if (value === null) reject(parameter, "Match a null column with is: null");
90
+ if (!isPlainObject(value)) {
91
+ if (Array.isArray(value)) reject(parameter, "Expected a scalar or an operator object");
92
+ countCondition();
93
+ return decodeOperand(column, value, parameter);
94
+ }
95
+ const nullable = !column.meta.notNull;
96
+ const supported = filterOperatorsForKind(column.meta.kind);
97
+ const operators = {};
98
+ const entries = Object.entries(value);
99
+ if (entries.length === 0) reject(parameter, "Filter cannot be empty");
100
+ for (const [operator, operand] of entries) {
101
+ countCondition();
102
+ if (operator === "is") {
103
+ if (operand !== null || !nullable) reject(parameter, "The is operator accepts only null on a nullable column");
104
+ operators.is = null;
105
+ continue;
106
+ }
107
+ if (!isFilterOperator(operator) || !supported.includes(operator)) reject(parameter, "Operator is not supported for this column");
108
+ if (operator !== "in") {
109
+ operators[operator] = decodeOperand(column, operand, parameter);
110
+ continue;
111
+ }
112
+ if (!Array.isArray(operand) || operand.length > IN_CAP) reject(parameter, `Expected an array of at most ${IN_CAP} values`);
113
+ operators.in = operand.map((item) => {
114
+ if (item === null) reject(parameter, "The in operator does not accept null");
115
+ return decodeOperand(column, item, parameter);
116
+ });
117
+ }
118
+ return operators;
119
+ }
120
+ /** Decode one filter level, recursing through bounded `and`/`or` groups. */
121
+ function decodeWhere(table, node, depth, parameter, state) {
122
+ if (depth > MAX_WHERE_DEPTH) reject(parameter, `Expected at most ${MAX_WHERE_DEPTH} nesting levels`);
123
+ if (!isPlainObject(node)) reject(parameter, "Expected an object");
124
+ const clause = {};
125
+ for (const [key, value] of Object.entries(node)) {
126
+ if (key === "and" || key === "or") {
127
+ if (!Array.isArray(value) || value.length === 0 || value.length > MAX_GROUP_ITEMS) reject(parameter, `Expected 1 to ${MAX_GROUP_ITEMS} group members`);
128
+ clause[key] = value.map((member) => decodeWhere(table, member, depth + 1, parameter, state));
129
+ continue;
130
+ }
131
+ const column = table.queryable.has(key) ? table.columns.get(key) : void 0;
132
+ if (!column) reject(parameter, "Names an unknown or unfilterable column");
133
+ clause[key] = decodeCondition(column, value, parameter, state);
134
+ }
135
+ return clause;
136
+ }
137
+ /**
138
+ * Decode one relation's options against the target table, not the parent, and
139
+ * give every to-many edge a limit so an unqualified include stays bounded.
140
+ */
141
+ function decodeIncludeOptions(target, value, depth, toMany, parameter) {
142
+ if (value === true) return toMany ? { limit: DEFAULT_LIMIT } : true;
143
+ if (!isPlainObject(value)) reject(parameter, "Expected true or an options object");
144
+ const options = {};
145
+ for (const [key, inner] of Object.entries(value)) switch (key) {
146
+ case "select":
147
+ options.select = decodeSelect(target, inner, parameter);
148
+ break;
149
+ case "where":
150
+ options.where = decodeWhere(target, inner, 1, parameter, { conditions: 0 });
151
+ break;
152
+ case "order":
153
+ options.order = decodeOrder(target, inner, parameter);
154
+ break;
155
+ case "limit":
156
+ if (!toMany) reject(parameter, "Only to-many relations accept a limit");
157
+ if (typeof inner !== "number" || !Number.isSafeInteger(inner) || inner < 0 || inner > MAX_LIMIT) reject(parameter, `Expected a limit between 0 and ${MAX_LIMIT}`);
158
+ options.limit = inner;
159
+ break;
160
+ case "include":
161
+ options.include = decodeInclude(target, inner, depth + 1, parameter);
162
+ break;
163
+ default: reject(parameter, "Unsupported relation option");
164
+ }
165
+ if (toMany && options.limit === void 0) options.limit = DEFAULT_LIMIT;
166
+ return options;
167
+ }
168
+ /** Decode one include level; a relation to an unexposed table has no edge. */
169
+ function decodeInclude(table, value, depth, parameter) {
170
+ if (depth > MAX_INCLUDE_DEPTH) reject(parameter, `Expected at most ${MAX_INCLUDE_DEPTH} relation edges`);
171
+ if (!isPlainObject(value)) reject(parameter, "Expected an object of relation names");
172
+ const entries = Object.entries(value);
173
+ if (entries.length > MAX_INCLUDES) reject(parameter, `Expected at most ${MAX_INCLUDES} relations per level`);
174
+ const include = {};
175
+ for (const [name, options] of entries) {
176
+ const relation = table.relations.get(name);
177
+ if (!relation) reject(parameter, "Names an unknown or unexposed relation");
178
+ include[name] = decodeIncludeOptions(relation.target, options, depth, relation.cardinality === "toMany", parameter);
179
+ }
180
+ return include;
181
+ }
182
+ /**
183
+ * Rows one include tree materializes per parent row, counting relation nodes
184
+ * on the way. Every term is a non-negative integer bounded by `MAX_LIMIT` and
185
+ * both running totals are checked after each addition, so neither can overflow.
186
+ */
187
+ function measureInclude(table, include, budget) {
188
+ let rows = 1;
189
+ if (!include) return rows;
190
+ for (const [name, value] of Object.entries(include)) {
191
+ const relation = table.relations.get(name);
192
+ if (!relation) continue;
193
+ budget.nodes += 1;
194
+ if (budget.nodes > MAX_INCLUDE_NODES) reject("include", `Expected at most ${MAX_INCLUDE_NODES} relations`);
195
+ const options = value === true ? void 0 : value;
196
+ const child = measureInclude(relation.target, options?.include, budget);
197
+ rows += relation.cardinality === "toMany" ? (options?.limit ?? DEFAULT_LIMIT) * child : child;
198
+ if (rows > MAX_MATERIALIZED_NODES) reject("include", "Relation fan-out exceeds the read budget");
199
+ }
200
+ return rows;
201
+ }
202
+ /** Reject an include tree whose cost is only visible once it is assembled. */
203
+ function assertReadBudget(table, include, rootRows) {
204
+ if (rootRows * measureInclude(table, include, { nodes: 0 }) > MAX_MATERIALIZED_NODES) reject("include", "Relation fan-out exceeds the read budget");
205
+ }
206
+ /** Decode the projection parameters both reads share. */
207
+ function decodeProjection(table, params) {
208
+ const rawSelect = params.get("select");
209
+ const rawInclude = params.get("include");
210
+ return {
211
+ select: rawSelect === void 0 ? void 0 : decodeSelect(table, parseJson(rawSelect, "select"), "select"),
212
+ include: rawInclude === void 0 ? void 0 : decodeInclude(table, parseJson(rawInclude, "include"), 1, "include")
213
+ };
214
+ }
215
+ /** Decode `GET /:table` and reject it before any database work is scheduled. */
216
+ function decodeListQuery(table, rawQuery) {
217
+ const params = parseParams(rawQuery, LIST_PARAMS);
218
+ const rawWhere = params.get("where");
219
+ const rawOrder = params.get("order");
220
+ const where = rawWhere === void 0 ? void 0 : decodeWhere(table, parseJson(rawWhere, "where"), 1, "where", { conditions: 0 });
221
+ const order = rawOrder === void 0 ? void 0 : decodeOrder(table, parseJson(rawOrder, "order"), "order");
222
+ const { select, include } = decodeProjection(table, params);
223
+ const limit = decodeIntParam(params.get("limit"), "limit", DEFAULT_LIMIT, MAX_LIMIT);
224
+ const offset = decodeIntParam(params.get("offset"), "offset", 0, MAX_OFFSET);
225
+ assertReadBudget(table, include, limit);
226
+ return {
227
+ where,
228
+ order,
229
+ select,
230
+ include,
231
+ limit,
232
+ offset
233
+ };
234
+ }
235
+ /** Decode `GET /:table/:id`, which supports projection and includes only. */
236
+ function decodeDetailQuery(table, rawQuery) {
237
+ const decoded = decodeProjection(table, parseParams(rawQuery, DETAIL_PARAMS));
238
+ assertReadBudget(table, decoded.include, 1);
239
+ return decoded;
240
+ }
241
+
242
+ //#endregion
243
+ export { decodeDetailQuery, decodeListQuery };
244
+ //# sourceMappingURL=query.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"query.js","names":[],"sources":["../../../../src/plugins/database/crud/query.ts"],"sourcesContent":["import {\n DEFAULT_LIMIT,\n IN_CAP,\n isFilterOperator,\n MAX_INCLUDE_DEPTH,\n MAX_INCLUDE_NODES,\n MAX_INCLUDES,\n MAX_LIMIT,\n} from \"../../../database/contract\";\nimport { invalidDatabaseInput } from \"../../../database/errors\";\nimport type {\n IncludeOptions,\n IncludeSpec,\n OrderDirection,\n OrderSpec,\n ScalarValue,\n WhereClause,\n WhereValue,\n} from \"../../../database/runtime\";\nimport { filterOperatorsForKind } from \"../../../database/schema-builder/types\";\nimport {\n MAX_GROUP_ITEMS,\n MAX_MATERIALIZED_NODES,\n MAX_OFFSET,\n MAX_ORDER_FIELDS,\n MAX_QUERY_BYTES,\n MAX_WHERE_CONDITIONS,\n MAX_WHERE_DEPTH,\n} from \"../defaults\";\nimport type { CompiledColumn } from \"./codecs\";\nimport type { CrudTable } from \"./contract\";\n\n/** One decoded, budget-checked page request. */\ninterface DecodedListQuery {\n readonly where?: WhereClause;\n readonly order?: OrderSpec;\n readonly select?: string[];\n readonly include?: IncludeSpec;\n readonly limit: number;\n readonly offset: number;\n}\n\n/** One decoded, budget-checked single-row request. */\ninterface DecodedDetailQuery {\n readonly select?: string[];\n readonly include?: IncludeSpec;\n}\n\nconst LIST_PARAMS = new Set([\n \"where\",\n \"order\",\n \"select\",\n \"include\",\n \"limit\",\n \"offset\",\n]);\nconst DETAIL_PARAMS = new Set([\"select\", \"include\"]);\nconst CANONICAL_INT = /^-?(0|[1-9]\\d*)$/;\n\n/**\n * Reject one query parameter. `parameter` is always a fixed name and `reason`\n * a fixed sentence, so a rejection can never echo caller-supplied text back.\n */\nfunction reject(parameter: string, reason: string): never {\n throw invalidDatabaseInput([parameter], reason);\n}\n\n/** A decoded JSON object; arrays and null are separate wire shapes here. */\nfunction isPlainObject(value: unknown): value is Record<string, unknown> {\n return typeof value === \"object\" && value !== null && !Array.isArray(value);\n}\n\n/** Read known, single-occurrence parameters from the raw query string. */\nfunction parseParams(\n rawQuery: string,\n allowed: ReadonlySet<string>,\n): Map<string, string> {\n if (Buffer.byteLength(rawQuery, \"utf8\") > MAX_QUERY_BYTES) {\n reject(\"query\", \"Query string exceeds the maximum size\");\n }\n const params = new Map<string, string>();\n for (const [key, value] of new URLSearchParams(rawQuery)) {\n if (!allowed.has(key) || params.has(key)) {\n reject(\"query\", \"Unknown or repeated query parameter\");\n }\n params.set(key, value);\n }\n return params;\n}\n\n/** Parse one structured parameter without leaking the parser's diagnostics. */\nfunction parseJson(raw: string, parameter: string): unknown {\n try {\n return JSON.parse(raw);\n } catch {\n reject(parameter, \"Expected JSON\");\n }\n}\n\n/** Accept one bounded integer, rejecting the forms `Number` would coerce. */\nfunction decodeIntParam(\n raw: string | undefined,\n parameter: string,\n fallback: number,\n max: number,\n): number {\n if (raw === undefined) return fallback;\n if (!CANONICAL_INT.test(raw)) {\n reject(parameter, \"Expected a canonical decimal integer\");\n }\n const value = Number(raw);\n if (!Number.isSafeInteger(value) || value < 0 || value > max) {\n reject(parameter, `Must be between 0 and ${max}`);\n }\n return value;\n}\n\n/** Narrow a projection to the table's public columns. */\nfunction decodeSelect(\n table: CrudTable,\n value: unknown,\n parameter: string,\n): string[] {\n if (!Array.isArray(value) || value.length === 0) {\n reject(parameter, \"Expected a non-empty array of public column names\");\n }\n for (const name of value) {\n if (typeof name !== \"string\" || !table.selectable.has(name)) {\n reject(parameter, \"Names an unknown or private column\");\n }\n }\n return value as string[];\n}\n\n/** Order only by columns the table can sort on, in a declared direction. */\nfunction decodeOrder(\n table: CrudTable,\n value: unknown,\n parameter: string,\n): OrderSpec {\n if (!isPlainObject(value)) {\n reject(parameter, \"Expected an object of column directions\");\n }\n const entries = Object.entries(value);\n if (entries.length === 0 || entries.length > MAX_ORDER_FIELDS) {\n reject(parameter, `Expected 1 to ${MAX_ORDER_FIELDS} columns`);\n }\n const order: Record<string, OrderDirection> = {};\n for (const [column, direction] of entries) {\n if (!table.queryable.has(column)) {\n reject(parameter, \"Names an unknown or unorderable column\");\n }\n if (direction !== \"asc\" && direction !== \"desc\") {\n reject(parameter, \"Expected a direction of asc or desc\");\n }\n order[column] = direction;\n }\n return order;\n}\n\n/** Move one filter operand onto its column's canonical runtime value. */\nfunction decodeOperand(\n column: CompiledColumn,\n raw: unknown,\n parameter: string,\n): ScalarValue {\n const value = column.decode(raw);\n if (value === undefined)\n reject(parameter, \"Operand does not match its column type\");\n return value;\n}\n\n/**\n * Decode the predicate on one column against the operator matrix its kind\n * allows, counting conditions so a filter cannot grow without bound.\n */\nfunction decodeCondition(\n column: CompiledColumn,\n value: unknown,\n parameter: string,\n state: { conditions: number },\n): WhereValue {\n const countCondition = () => {\n state.conditions += 1;\n if (state.conditions > MAX_WHERE_CONDITIONS) {\n reject(parameter, `Expected at most ${MAX_WHERE_CONDITIONS} conditions`);\n }\n };\n\n if (value === null) {\n // SQL three-valued matching stays explicit; a bare null is ambiguous.\n reject(parameter, \"Match a null column with is: null\");\n }\n if (!isPlainObject(value)) {\n if (Array.isArray(value)) {\n reject(parameter, \"Expected a scalar or an operator object\");\n }\n countCondition();\n return decodeOperand(column, value, parameter);\n }\n\n const nullable = !column.meta.notNull;\n const supported = filterOperatorsForKind(column.meta.kind);\n const operators: Record<string, ScalarValue | ScalarValue[]> = {};\n const entries = Object.entries(value);\n if (entries.length === 0) reject(parameter, \"Filter cannot be empty\");\n\n for (const [operator, operand] of entries) {\n countCondition();\n if (operator === \"is\") {\n if (operand !== null || !nullable) {\n reject(\n parameter,\n \"The is operator accepts only null on a nullable column\",\n );\n }\n operators.is = null;\n continue;\n }\n if (!isFilterOperator(operator) || !supported.includes(operator)) {\n reject(parameter, \"Operator is not supported for this column\");\n }\n if (operator !== \"in\") {\n operators[operator] = decodeOperand(column, operand, parameter);\n continue;\n }\n if (!Array.isArray(operand) || operand.length > IN_CAP) {\n reject(parameter, `Expected an array of at most ${IN_CAP} values`);\n }\n operators.in = operand.map((item) => {\n if (item === null)\n reject(parameter, \"The in operator does not accept null\");\n return decodeOperand(column, item, parameter);\n });\n }\n return operators as WhereValue;\n}\n\n/** Decode one filter level, recursing through bounded `and`/`or` groups. */\nfunction decodeWhere(\n table: CrudTable,\n node: unknown,\n depth: number,\n parameter: string,\n state: { conditions: number },\n): WhereClause {\n if (depth > MAX_WHERE_DEPTH) {\n reject(parameter, `Expected at most ${MAX_WHERE_DEPTH} nesting levels`);\n }\n if (!isPlainObject(node)) reject(parameter, \"Expected an object\");\n\n const clause: Record<string, WhereValue | WhereClause[]> = {};\n for (const [key, value] of Object.entries(node)) {\n if (key === \"and\" || key === \"or\") {\n if (\n !Array.isArray(value) ||\n value.length === 0 ||\n value.length > MAX_GROUP_ITEMS\n ) {\n reject(parameter, `Expected 1 to ${MAX_GROUP_ITEMS} group members`);\n }\n clause[key] = value.map((member) =>\n decodeWhere(table, member, depth + 1, parameter, state),\n );\n continue;\n }\n // Relations are absent from `queryable`, so relation predicates fail here.\n const column = table.queryable.has(key)\n ? table.columns.get(key)\n : undefined;\n if (!column) reject(parameter, \"Names an unknown or unfilterable column\");\n clause[key] = decodeCondition(column, value, parameter, state);\n }\n return clause;\n}\n\n/**\n * Decode one relation's options against the target table, not the parent, and\n * give every to-many edge a limit so an unqualified include stays bounded.\n */\nfunction decodeIncludeOptions(\n target: CrudTable,\n value: unknown,\n depth: number,\n toMany: boolean,\n parameter: string,\n): boolean | IncludeOptions {\n if (value === true) return toMany ? { limit: DEFAULT_LIMIT } : true;\n if (!isPlainObject(value)) {\n reject(parameter, \"Expected true or an options object\");\n }\n\n const options: {\n select?: string[];\n where?: WhereClause;\n order?: OrderSpec;\n limit?: number;\n include?: IncludeSpec;\n } = {};\n for (const [key, inner] of Object.entries(value)) {\n switch (key) {\n case \"select\":\n options.select = decodeSelect(target, inner, parameter);\n break;\n case \"where\":\n options.where = decodeWhere(target, inner, 1, parameter, {\n conditions: 0,\n });\n break;\n case \"order\":\n options.order = decodeOrder(target, inner, parameter);\n break;\n case \"limit\":\n if (!toMany) reject(parameter, \"Only to-many relations accept a limit\");\n if (\n typeof inner !== \"number\" ||\n !Number.isSafeInteger(inner) ||\n inner < 0 ||\n inner > MAX_LIMIT\n ) {\n reject(parameter, `Expected a limit between 0 and ${MAX_LIMIT}`);\n }\n options.limit = inner;\n break;\n case \"include\":\n options.include = decodeInclude(target, inner, depth + 1, parameter);\n break;\n default:\n reject(parameter, \"Unsupported relation option\");\n }\n }\n if (toMany && options.limit === undefined) options.limit = DEFAULT_LIMIT;\n return options;\n}\n\n/** Decode one include level; a relation to an unexposed table has no edge. */\nfunction decodeInclude(\n table: CrudTable,\n value: unknown,\n depth: number,\n parameter: string,\n): IncludeSpec {\n if (depth > MAX_INCLUDE_DEPTH) {\n reject(parameter, `Expected at most ${MAX_INCLUDE_DEPTH} relation edges`);\n }\n if (!isPlainObject(value)) {\n reject(parameter, \"Expected an object of relation names\");\n }\n const entries = Object.entries(value);\n if (entries.length > MAX_INCLUDES) {\n reject(parameter, `Expected at most ${MAX_INCLUDES} relations per level`);\n }\n\n const include: Record<string, boolean | IncludeOptions> = {};\n for (const [name, options] of entries) {\n const relation = table.relations.get(name);\n if (!relation) reject(parameter, \"Names an unknown or unexposed relation\");\n include[name] = decodeIncludeOptions(\n relation.target,\n options,\n depth,\n relation.cardinality === \"toMany\",\n parameter,\n );\n }\n return include;\n}\n\n/**\n * Rows one include tree materializes per parent row, counting relation nodes\n * on the way. Every term is a non-negative integer bounded by `MAX_LIMIT` and\n * both running totals are checked after each addition, so neither can overflow.\n */\nfunction measureInclude(\n table: CrudTable,\n include: IncludeSpec | undefined,\n budget: { nodes: number },\n): number {\n let rows = 1;\n if (!include) return rows;\n for (const [name, value] of Object.entries(include)) {\n const relation = table.relations.get(name);\n if (!relation) continue;\n budget.nodes += 1;\n if (budget.nodes > MAX_INCLUDE_NODES) {\n reject(\"include\", `Expected at most ${MAX_INCLUDE_NODES} relations`);\n }\n const options = value === true ? undefined : (value as IncludeOptions);\n const child = measureInclude(relation.target, options?.include, budget);\n rows +=\n relation.cardinality === \"toMany\"\n ? (options?.limit ?? DEFAULT_LIMIT) * child\n : child;\n if (rows > MAX_MATERIALIZED_NODES) {\n reject(\"include\", \"Relation fan-out exceeds the read budget\");\n }\n }\n return rows;\n}\n\n/** Reject an include tree whose cost is only visible once it is assembled. */\nfunction assertReadBudget(\n table: CrudTable,\n include: IncludeSpec | undefined,\n rootRows: number,\n): void {\n const rows = measureInclude(table, include, { nodes: 0 });\n if (rootRows * rows > MAX_MATERIALIZED_NODES) {\n reject(\"include\", \"Relation fan-out exceeds the read budget\");\n }\n}\n\n/** Decode the projection parameters both reads share. */\nfunction decodeProjection(\n table: CrudTable,\n params: ReadonlyMap<string, string>,\n): DecodedDetailQuery {\n const rawSelect = params.get(\"select\");\n const rawInclude = params.get(\"include\");\n return {\n select:\n rawSelect === undefined\n ? undefined\n : decodeSelect(table, parseJson(rawSelect, \"select\"), \"select\"),\n include:\n rawInclude === undefined\n ? undefined\n : decodeInclude(table, parseJson(rawInclude, \"include\"), 1, \"include\"),\n };\n}\n\n/** Decode `GET /:table` and reject it before any database work is scheduled. */\nexport function decodeListQuery(\n table: CrudTable,\n rawQuery: string,\n): DecodedListQuery {\n const params = parseParams(rawQuery, LIST_PARAMS);\n const rawWhere = params.get(\"where\");\n const rawOrder = params.get(\"order\");\n\n const where =\n rawWhere === undefined\n ? undefined\n : decodeWhere(table, parseJson(rawWhere, \"where\"), 1, \"where\", {\n conditions: 0,\n });\n const order =\n rawOrder === undefined\n ? undefined\n : decodeOrder(table, parseJson(rawOrder, \"order\"), \"order\");\n const { select, include } = decodeProjection(table, params);\n const limit = decodeIntParam(\n params.get(\"limit\"),\n \"limit\",\n DEFAULT_LIMIT,\n MAX_LIMIT,\n );\n const offset = decodeIntParam(params.get(\"offset\"), \"offset\", 0, MAX_OFFSET);\n\n assertReadBudget(table, include, limit);\n return { where, order, select, include, limit, offset };\n}\n\n/** Decode `GET /:table/:id`, which supports projection and includes only. */\nexport function decodeDetailQuery(\n table: CrudTable,\n rawQuery: string,\n): DecodedDetailQuery {\n const decoded = decodeProjection(table, parseParams(rawQuery, DETAIL_PARAMS));\n assertReadBudget(table, decoded.include, 1);\n return decoded;\n}\n"],"mappings":"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"}
@@ -0,0 +1,124 @@
1
+ import { DatabasePluginError, classifyDatabaseError, invalidDatabaseInput } from "../../../database/errors.js";
2
+ import { MAX_RESPONSE_BYTES } from "../defaults.js";
3
+ import { decodeDetailQuery, decodeListQuery } from "./query.js";
4
+
5
+ //#region src/plugins/database/crud/routes.ts
6
+ /** Low-cardinality span outcome for one failed generated read. */
7
+ function readRouteOutcome(error) {
8
+ const { statusCode } = classifyDatabaseError(error, "read");
9
+ if (statusCode === 404) return "not_found";
10
+ return statusCode < 500 ? "rejected" : "failed";
11
+ }
12
+ /** Express normalizes `req.query`; the decoders need the untouched string. */
13
+ function rawQuery(req) {
14
+ const url = req.originalUrl ?? req.url;
15
+ const start = url.indexOf("?");
16
+ return start === -1 ? "" : url.slice(start + 1);
17
+ }
18
+ /**
19
+ * Append the primary key so equal sort keys cannot reshuffle between pages.
20
+ * A keyless table has no unique tie-breaker to append, so it must order itself
21
+ * and its pages stay stable only while no concurrent write reorders them.
22
+ */
23
+ function stableOrder(primaryKey, order) {
24
+ if (primaryKey) return {
25
+ ...order,
26
+ [primaryKey]: order?.[primaryKey] ?? "asc"
27
+ };
28
+ if (!order) throw invalidDatabaseInput(["order"], "A table without a primary key requires an explicit order");
29
+ return order;
30
+ }
31
+ function serializeRow(deps, operation, row) {
32
+ const projected = deps.table.projectPublicRow(row);
33
+ if (!deps.serialize) return projected;
34
+ const shaped = deps.serialize(projected, {
35
+ entity: deps.table.name,
36
+ operation
37
+ });
38
+ return deps.table.sanitizeSerializedRow(shaped);
39
+ }
40
+ /**
41
+ * Row data is never cacheable by a shared proxy or a browser: the same URL can
42
+ * answer differently once the underlying table or the caller's rights change.
43
+ */
44
+ function writeJson(res, status, payload) {
45
+ res.status(status);
46
+ res.type("application/json");
47
+ res.setHeader("Cache-Control", "no-store");
48
+ res.send(payload);
49
+ }
50
+ /** Measure the encoded body before sending so no partial response escapes. */
51
+ function sendJson(res, body) {
52
+ const payload = JSON.stringify(body);
53
+ if (Buffer.byteLength(payload, "utf8") > MAX_RESPONSE_BYTES) throw new DatabasePluginError("PAYLOAD_TOO_LARGE", "read");
54
+ writeJson(res, 200, payload);
55
+ }
56
+ /**
57
+ * Encode the list envelope one row at a time, charging each encoded row
58
+ * against the byte budget before the next row is shaped, so one response
59
+ * never costs more than the budget in memory.
60
+ */
61
+ function encodeListPayload(deps, rows, limit, offset) {
62
+ const prefix = "{\"items\":[";
63
+ const suffix = `],"limit":${limit},"offset":${offset}}`;
64
+ let bytes = Buffer.byteLength(prefix, "utf8") + Buffer.byteLength(suffix, "utf8");
65
+ const items = [];
66
+ for (const row of rows) {
67
+ const encoded = JSON.stringify(serializeRow(deps, "list", row));
68
+ bytes += Buffer.byteLength(encoded, "utf8") + (items.length > 0 ? 1 : 0);
69
+ if (bytes > MAX_RESPONSE_BYTES) throw new DatabasePluginError("PAYLOAD_TOO_LARGE", "read");
70
+ items.push(encoded);
71
+ }
72
+ return prefix + items.join(",") + suffix;
73
+ }
74
+ /** Answer with the failure's safe category and the field it concerns. */
75
+ function writeError(res, error) {
76
+ if (res.headersSent) return;
77
+ const safe = classifyDatabaseError(error, "read");
78
+ const body = { error: safe.clientMessage };
79
+ if (safe.details && safe.details.length > 0) body.details = safe.details;
80
+ writeJson(res, safe.statusCode, JSON.stringify(body));
81
+ }
82
+ /** `GET /:table` — one bounded page in the `{ items, limit, offset }` envelope. */
83
+ function createListHandler(deps) {
84
+ const primaryKey = deps.table.primaryKey?.meta.columnName;
85
+ const route = `/${deps.table.name}`;
86
+ return async (req, res) => {
87
+ try {
88
+ await deps.runRouteSpan("list", route, async () => {
89
+ const decoded = decodeListQuery(deps.table, rawQuery(req));
90
+ let query = deps.entity().order(stableOrder(primaryKey, decoded.order)).limit(decoded.limit).offset(decoded.offset);
91
+ if (decoded.where) query = query.where(decoded.where);
92
+ if (decoded.select) query = query.select(decoded.select);
93
+ if (decoded.include) query = query.include(decoded.include);
94
+ writeJson(res, 200, encodeListPayload(deps, await query.toArray(), decoded.limit, decoded.offset));
95
+ });
96
+ } catch (error) {
97
+ writeError(res, error);
98
+ }
99
+ };
100
+ }
101
+ /** `GET /:table/:id` — one public row, or 404 when nothing matches. */
102
+ function createDetailHandler(deps) {
103
+ const route = `/${deps.table.name}/:id`;
104
+ return async (req, res) => {
105
+ try {
106
+ await deps.runRouteSpan("detail", route, async () => {
107
+ const id = deps.table.decodeId(req.params.id);
108
+ const decoded = decodeDetailQuery(deps.table, rawQuery(req));
109
+ let query = deps.entity();
110
+ if (decoded.select) query = query.select(decoded.select);
111
+ if (decoded.include) query = query.include(decoded.include);
112
+ const row = await query.find(id);
113
+ if (row === null) throw new DatabasePluginError("NOT_FOUND", "read");
114
+ sendJson(res, serializeRow(deps, "detail", row));
115
+ });
116
+ } catch (error) {
117
+ writeError(res, error);
118
+ }
119
+ };
120
+ }
121
+
122
+ //#endregion
123
+ export { createDetailHandler, createListHandler, readRouteOutcome };
124
+ //# sourceMappingURL=routes.js.map