@databricks/appkit 0.66.1 → 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 (85) hide show
  1. package/CLAUDE.md +2 -2
  2. package/dist/appkit/package.js +1 -1
  3. package/dist/core/agent/frontmatter.js +23 -0
  4. package/dist/core/agent/frontmatter.js.map +1 -0
  5. package/dist/core/agent/load-agents.d.ts.map +1 -1
  6. package/dist/core/agent/load-agents.js +36 -6
  7. package/dist/core/agent/load-agents.js.map +1 -1
  8. package/dist/core/agent/skills/index.js +7 -0
  9. package/dist/core/agent/skills/load-skills.js +78 -0
  10. package/dist/core/agent/skills/load-skills.js.map +1 -0
  11. package/dist/core/agent/skills/parse-skill.js +69 -0
  12. package/dist/core/agent/skills/parse-skill.js.map +1 -0
  13. package/dist/core/agent/skills/read-resource.js +31 -0
  14. package/dist/core/agent/skills/read-resource.js.map +1 -0
  15. package/dist/core/agent/skills/render.js +33 -0
  16. package/dist/core/agent/skills/render.js.map +1 -0
  17. package/dist/core/agent/skills/resolve-catalog.js +78 -0
  18. package/dist/core/agent/skills/resolve-catalog.js.map +1 -0
  19. package/dist/core/agent/skills/types.d.ts +50 -0
  20. package/dist/core/agent/skills/types.d.ts.map +1 -0
  21. package/dist/core/agent/types.d.ts +47 -0
  22. package/dist/core/agent/types.d.ts.map +1 -1
  23. package/dist/core/agent/types.js.map +1 -1
  24. package/dist/database/contract/index.js +1 -1
  25. package/dist/database/contract/wire.d.ts.map +1 -1
  26. package/dist/database/contract/wire.js +5 -1
  27. package/dist/database/contract/wire.js.map +1 -1
  28. package/dist/database/errors.js +39 -3
  29. package/dist/database/errors.js.map +1 -1
  30. package/dist/database/runtime/data-path.js.map +1 -1
  31. package/dist/database/runtime/engine/drizzle-data-path.js +1 -1
  32. package/dist/database/runtime/engine/drizzle-data-path.js.map +1 -1
  33. package/dist/database/runtime/engine/translate.js +9 -2
  34. package/dist/database/runtime/engine/translate.js.map +1 -1
  35. package/dist/database/schema-builder/define-schema.d.ts +5 -1
  36. package/dist/database/schema-builder/define-schema.d.ts.map +1 -1
  37. package/dist/database/schema-builder/define-schema.js +4 -0
  38. package/dist/database/schema-builder/define-schema.js.map +1 -1
  39. package/dist/database/schema-builder/types.d.ts +7 -2
  40. package/dist/database/schema-builder/types.d.ts.map +1 -1
  41. package/dist/database/schema-builder/types.js.map +1 -1
  42. package/dist/plugins/agents/agents.d.ts +50 -0
  43. package/dist/plugins/agents/agents.d.ts.map +1 -1
  44. package/dist/plugins/agents/agents.js +225 -13
  45. package/dist/plugins/agents/agents.js.map +1 -1
  46. package/dist/plugins/agents/manifest.js +40 -21
  47. package/dist/plugins/agents/schemas.js +2 -1
  48. package/dist/plugins/agents/schemas.js.map +1 -1
  49. package/dist/plugins/database/crud/codecs.js +78 -0
  50. package/dist/plugins/database/crud/codecs.js.map +1 -0
  51. package/dist/plugins/database/crud/contract.js +151 -0
  52. package/dist/plugins/database/crud/contract.js.map +1 -0
  53. package/dist/plugins/database/crud/exposure.js +40 -0
  54. package/dist/plugins/database/crud/exposure.js.map +1 -0
  55. package/dist/plugins/database/crud/query.js +244 -0
  56. package/dist/plugins/database/crud/query.js.map +1 -0
  57. package/dist/plugins/database/crud/routes.js +124 -0
  58. package/dist/plugins/database/crud/routes.js.map +1 -0
  59. package/dist/plugins/database/database.d.ts +6 -0
  60. package/dist/plugins/database/database.d.ts.map +1 -1
  61. package/dist/plugins/database/database.js +58 -4
  62. package/dist/plugins/database/database.js.map +1 -1
  63. package/dist/plugins/database/defaults.js +28 -1
  64. package/dist/plugins/database/defaults.js.map +1 -1
  65. package/dist/plugins/database/entity-client.js +1 -1
  66. package/dist/plugins/database/entity-types.d.ts +13 -4
  67. package/dist/plugins/database/entity-types.d.ts.map +1 -1
  68. package/dist/plugins/database/lifecycle.js +2 -1
  69. package/dist/plugins/database/lifecycle.js.map +1 -1
  70. package/dist/plugins/database/types.d.ts +32 -0
  71. package/dist/plugins/database/types.d.ts.map +1 -1
  72. package/dist/shared/src/schemas/manifest.d.ts +35 -35
  73. package/docs/api/appkit/Function.database.md +3 -3
  74. package/docs/api/appkit/Function.defineSchema.md +14 -6
  75. package/docs/api/appkit/Interface.AgentDefinition.md +11 -0
  76. package/docs/api/appkit/Interface.AgentsPluginConfig.md +35 -0
  77. package/docs/api/appkit/Interface.RegisteredAgent.md +11 -0
  78. package/docs/api/appkit/Interface.Schema.md +10 -2
  79. package/docs/api/appkit/TypeAlias.IDatabaseConfig.md +20 -0
  80. package/docs/api/appkit/TypeAlias.ResolvedToolEntry.md +46 -0
  81. package/docs/api/appkit.md +2 -2
  82. package/docs/plugins/agents.md +68 -1
  83. package/llms.txt +2 -2
  84. package/package.json +1 -1
  85. package/sbom.cdx.json +1 -1
@@ -7,27 +7,46 @@ var manifest_default = {
7
7
  description: "AI agents driven by markdown configs or code, with auto-tool-discovery from registered plugins",
8
8
  resources: {
9
9
  "required": [],
10
- "optional": [{
11
- "type": "serving_endpoint",
12
- "alias": "Default LLM for agents",
13
- "resourceKey": "agents-serving-endpoint",
14
- "description": "Default streaming-capable LLM endpoint for agents that don't pin their own model",
15
- "permission": "CAN_QUERY",
16
- "fields": { "name": {
17
- "env": "DATABRICKS_SERVING_ENDPOINT_NAME",
18
- "description": "Default LLM serving endpoint name"
19
- } }
20
- }, {
21
- "type": "experiment",
22
- "alias": "MLflow experiment for agent traces",
23
- "resourceKey": "agents-mlflow-experiment",
24
- "description": "When bound, agent turns and tool calls are traced to this MLflow experiment via OpenTelemetry. Tracing is a no-op when unset.",
25
- "permission": "CAN_EDIT",
26
- "fields": { "experimentId": {
27
- "env": "MLFLOW_EXPERIMENT_ID",
28
- "description": "MLflow experiment id traces are logged to"
29
- } }
30
- }]
10
+ "optional": [
11
+ {
12
+ "type": "serving_endpoint",
13
+ "alias": "Default LLM for agents",
14
+ "resourceKey": "agents-serving-endpoint",
15
+ "description": "Default streaming-capable LLM endpoint for agents that don't pin their own model",
16
+ "permission": "CAN_QUERY",
17
+ "fields": { "name": {
18
+ "env": "DATABRICKS_SERVING_ENDPOINT_NAME",
19
+ "description": "Default LLM serving endpoint name"
20
+ } }
21
+ },
22
+ {
23
+ "type": "experiment",
24
+ "alias": "MLflow experiment for agent traces",
25
+ "resourceKey": "agents-mlflow-experiment",
26
+ "description": "When bound, agent turns and tool calls are traced to this MLflow experiment via OpenTelemetry. Tracing is a no-op when unset.",
27
+ "permission": "CAN_EDIT",
28
+ "fields": { "experimentId": {
29
+ "env": "MLFLOW_EXPERIMENT_ID",
30
+ "description": "MLflow experiment id traces are logged to"
31
+ } }
32
+ },
33
+ {
34
+ "type": "volume",
35
+ "alias": "Agent skills",
36
+ "resourceKey": "agents-skills",
37
+ "description": "Optional Unity Catalog Volume providing catalog-sourced agent skills (read-only)",
38
+ "permission": "READ_VOLUME",
39
+ "fields": { "path": {
40
+ "env": "DATABRICKS_VOLUME_AGENT_SKILLS",
41
+ "description": "Volume path for agent skills (e.g. /Volumes/catalog/schema/skills)",
42
+ "discovery": {
43
+ "type": "kind",
44
+ "resourceKind": "volume",
45
+ "select": "full_name"
46
+ }
47
+ } }
48
+ }
49
+ ]
31
50
  }
32
51
  };
33
52
 
@@ -24,7 +24,8 @@ const chatRequestSchema = z.object({
24
24
  message: z.string().min(1, "message must not be empty").max(MAX_MESSAGE_CHARS, `message exceeds the ${MAX_MESSAGE_CHARS}-character limit`),
25
25
  threadId: z.string().optional(),
26
26
  agent: z.string().optional(),
27
- mlflowRunId: z.string().max(64).optional()
27
+ mlflowRunId: z.string().max(64).optional(),
28
+ skill: z.string().optional()
28
29
  });
29
30
  const messageItemSchema = z.object({
30
31
  role: z.enum([
@@ -1 +1 @@
1
- {"version":3,"file":"schemas.js","names":[],"sources":["../../../src/plugins/agents/schemas.ts"],"sourcesContent":["import { z } from \"zod\";\n\n/**\n * Static body cap for the `message` field on `POST /chat`. 64 000 characters\n * is well above any legitimate chat turn (~16k tokens at 4 chars/token) and\n * bounds the per-request cost of appending to `InMemoryThreadStore` without\n * requiring per-deployment configuration.\n */\nconst MAX_MESSAGE_CHARS = 64_000;\n\n/** Cap applied to `/invocations` when `input` is a raw string. */\nconst MAX_INVOCATIONS_INPUT_CHARS = 64_000;\n\n/**\n * Cap on the number of items accepted in an `/invocations` `input` array\n * (one element per seeded message). Protects against a single request\n * seeding hundreds of messages into the thread store.\n */\nconst MAX_INVOCATIONS_INPUT_ITEMS = 100;\n\n/** Per-message `content` size cap (string form). */\nconst MAX_INVOCATIONS_ITEM_CHARS = 64_000;\n\n/** Per-message `content` size cap (array form). */\nconst MAX_INVOCATIONS_ITEM_ARRAY_ITEMS = 100;\n\nexport const chatRequestSchema = z.object({\n message: z\n .string()\n .min(1, \"message must not be empty\")\n .max(\n MAX_MESSAGE_CHARS,\n `message exceeds the ${MAX_MESSAGE_CHARS}-character limit`,\n ),\n threadId: z.string().optional(),\n agent: z.string().optional(),\n /**\n * MLflow run id to link this turn's trace to (via `mlflow.sourceRun`), set by\n * the eval runner so each case's trace shows under the evaluation run. Capped\n * to a run-id-shaped length since it reaches trace metadata and logs.\n */\n mlflowRunId: z.string().max(64).optional(),\n});\n\nconst messageItemSchema = z.object({\n role: z.enum([\"user\", \"assistant\", \"system\"]).optional(),\n content: z\n .union([\n z.string().max(MAX_INVOCATIONS_ITEM_CHARS),\n z.array(z.any()).max(MAX_INVOCATIONS_ITEM_ARRAY_ITEMS),\n ])\n .optional(),\n type: z.string().optional(),\n});\n\nexport const invocationsRequestSchema = z.object({\n input: z.union([\n z.string().min(1).max(MAX_INVOCATIONS_INPUT_CHARS),\n z\n .array(messageItemSchema)\n .min(1)\n .max(\n MAX_INVOCATIONS_INPUT_ITEMS,\n `input array exceeds the ${MAX_INVOCATIONS_INPUT_ITEMS}-item limit`,\n ),\n ]),\n model: z.string().optional(),\n /**\n * MLflow run id to link this turn's trace to. Same as `chatRequestSchema`,\n * but the trace id comes back in the response body (`mlflow_trace_id`) since\n * this surface has no `metadata` event.\n */\n mlflowRunId: z.string().max(64).optional(),\n});\n\nexport const approvalRequestSchema = z.object({\n streamId: z.string().min(1, \"streamId is required\"),\n approvalId: z.string().min(1, \"approvalId is required\"),\n decision: z.enum([\"approve\", \"deny\"]),\n});\n\nexport const cancelRequestSchema = z.object({\n streamId: z.string().min(1, \"streamId is required\"),\n});\n"],"mappings":";;;;;;;;;AAQA,MAAM,oBAAoB;;AAG1B,MAAM,8BAA8B;;;;;;AAOpC,MAAM,8BAA8B;;AAGpC,MAAM,6BAA6B;;AAGnC,MAAM,mCAAmC;AAEzC,MAAa,oBAAoB,EAAE,OAAO;CACxC,SAAS,EACN,QAAQ,CACR,IAAI,GAAG,4BAA4B,CACnC,IACC,mBACA,uBAAuB,kBAAkB,kBAC1C;CACH,UAAU,EAAE,QAAQ,CAAC,UAAU;CAC/B,OAAO,EAAE,QAAQ,CAAC,UAAU;CAM5B,aAAa,EAAE,QAAQ,CAAC,IAAI,GAAG,CAAC,UAAU;CAC3C,CAAC;AAEF,MAAM,oBAAoB,EAAE,OAAO;CACjC,MAAM,EAAE,KAAK;EAAC;EAAQ;EAAa;EAAS,CAAC,CAAC,UAAU;CACxD,SAAS,EACN,MAAM,CACL,EAAE,QAAQ,CAAC,IAAI,2BAA2B,EAC1C,EAAE,MAAM,EAAE,KAAK,CAAC,CAAC,IAAI,iCAAiC,CACvD,CAAC,CACD,UAAU;CACb,MAAM,EAAE,QAAQ,CAAC,UAAU;CAC5B,CAAC;AAEF,MAAa,2BAA2B,EAAE,OAAO;CAC/C,OAAO,EAAE,MAAM,CACb,EAAE,QAAQ,CAAC,IAAI,EAAE,CAAC,IAAI,4BAA4B,EAClD,EACG,MAAM,kBAAkB,CACxB,IAAI,EAAE,CACN,IACC,6BACA,2BAA2B,4BAA4B,aACxD,CACJ,CAAC;CACF,OAAO,EAAE,QAAQ,CAAC,UAAU;CAM5B,aAAa,EAAE,QAAQ,CAAC,IAAI,GAAG,CAAC,UAAU;CAC3C,CAAC;AAEF,MAAa,wBAAwB,EAAE,OAAO;CAC5C,UAAU,EAAE,QAAQ,CAAC,IAAI,GAAG,uBAAuB;CACnD,YAAY,EAAE,QAAQ,CAAC,IAAI,GAAG,yBAAyB;CACvD,UAAU,EAAE,KAAK,CAAC,WAAW,OAAO,CAAC;CACtC,CAAC;AAEF,MAAa,sBAAsB,EAAE,OAAO,EAC1C,UAAU,EAAE,QAAQ,CAAC,IAAI,GAAG,uBAAuB,EACpD,CAAC"}
1
+ {"version":3,"file":"schemas.js","names":[],"sources":["../../../src/plugins/agents/schemas.ts"],"sourcesContent":["import { z } from \"zod\";\n\n/**\n * Static body cap for the `message` field on `POST /chat`. 64 000 characters\n * is well above any legitimate chat turn (~16k tokens at 4 chars/token) and\n * bounds the per-request cost of appending to `InMemoryThreadStore` without\n * requiring per-deployment configuration.\n */\nconst MAX_MESSAGE_CHARS = 64_000;\n\n/** Cap applied to `/invocations` when `input` is a raw string. */\nconst MAX_INVOCATIONS_INPUT_CHARS = 64_000;\n\n/**\n * Cap on the number of items accepted in an `/invocations` `input` array\n * (one element per seeded message). Protects against a single request\n * seeding hundreds of messages into the thread store.\n */\nconst MAX_INVOCATIONS_INPUT_ITEMS = 100;\n\n/** Per-message `content` size cap (string form). */\nconst MAX_INVOCATIONS_ITEM_CHARS = 64_000;\n\n/** Per-message `content` size cap (array form). */\nconst MAX_INVOCATIONS_ITEM_ARRAY_ITEMS = 100;\n\nexport const chatRequestSchema = z.object({\n message: z\n .string()\n .min(1, \"message must not be empty\")\n .max(\n MAX_MESSAGE_CHARS,\n `message exceeds the ${MAX_MESSAGE_CHARS}-character limit`,\n ),\n threadId: z.string().optional(),\n agent: z.string().optional(),\n /**\n * MLflow run id to link this turn's trace to (via `mlflow.sourceRun`), set by\n * the eval runner so each case's trace shows under the evaluation run. Capped\n * to a run-id-shaped length since it reaches trace metadata and logs.\n */\n mlflowRunId: z.string().max(64).optional(),\n /**\n * Optional skill to force-load for this turn (deterministic `/skill-name`\n * invocation). Its instructions are injected into the turn's context; the\n * model can still auto-load others via the `load_skill` tool.\n */\n skill: z.string().optional(),\n});\n\nconst messageItemSchema = z.object({\n role: z.enum([\"user\", \"assistant\", \"system\"]).optional(),\n content: z\n .union([\n z.string().max(MAX_INVOCATIONS_ITEM_CHARS),\n z.array(z.any()).max(MAX_INVOCATIONS_ITEM_ARRAY_ITEMS),\n ])\n .optional(),\n type: z.string().optional(),\n});\n\nexport const invocationsRequestSchema = z.object({\n input: z.union([\n z.string().min(1).max(MAX_INVOCATIONS_INPUT_CHARS),\n z\n .array(messageItemSchema)\n .min(1)\n .max(\n MAX_INVOCATIONS_INPUT_ITEMS,\n `input array exceeds the ${MAX_INVOCATIONS_INPUT_ITEMS}-item limit`,\n ),\n ]),\n model: z.string().optional(),\n /**\n * MLflow run id to link this turn's trace to. Same as `chatRequestSchema`,\n * but the trace id comes back in the response body (`mlflow_trace_id`) since\n * this surface has no `metadata` event.\n */\n mlflowRunId: z.string().max(64).optional(),\n});\n\nexport const approvalRequestSchema = z.object({\n streamId: z.string().min(1, \"streamId is required\"),\n approvalId: z.string().min(1, \"approvalId is required\"),\n decision: z.enum([\"approve\", \"deny\"]),\n});\n\nexport const cancelRequestSchema = z.object({\n streamId: z.string().min(1, \"streamId is required\"),\n});\n"],"mappings":";;;;;;;;;AAQA,MAAM,oBAAoB;;AAG1B,MAAM,8BAA8B;;;;;;AAOpC,MAAM,8BAA8B;;AAGpC,MAAM,6BAA6B;;AAGnC,MAAM,mCAAmC;AAEzC,MAAa,oBAAoB,EAAE,OAAO;CACxC,SAAS,EACN,QAAQ,CACR,IAAI,GAAG,4BAA4B,CACnC,IACC,mBACA,uBAAuB,kBAAkB,kBAC1C;CACH,UAAU,EAAE,QAAQ,CAAC,UAAU;CAC/B,OAAO,EAAE,QAAQ,CAAC,UAAU;CAM5B,aAAa,EAAE,QAAQ,CAAC,IAAI,GAAG,CAAC,UAAU;CAM1C,OAAO,EAAE,QAAQ,CAAC,UAAU;CAC7B,CAAC;AAEF,MAAM,oBAAoB,EAAE,OAAO;CACjC,MAAM,EAAE,KAAK;EAAC;EAAQ;EAAa;EAAS,CAAC,CAAC,UAAU;CACxD,SAAS,EACN,MAAM,CACL,EAAE,QAAQ,CAAC,IAAI,2BAA2B,EAC1C,EAAE,MAAM,EAAE,KAAK,CAAC,CAAC,IAAI,iCAAiC,CACvD,CAAC,CACD,UAAU;CACb,MAAM,EAAE,QAAQ,CAAC,UAAU;CAC5B,CAAC;AAEF,MAAa,2BAA2B,EAAE,OAAO;CAC/C,OAAO,EAAE,MAAM,CACb,EAAE,QAAQ,CAAC,IAAI,EAAE,CAAC,IAAI,4BAA4B,EAClD,EACG,MAAM,kBAAkB,CACxB,IAAI,EAAE,CACN,IACC,6BACA,2BAA2B,4BAA4B,aACxD,CACJ,CAAC;CACF,OAAO,EAAE,QAAQ,CAAC,UAAU;CAM5B,aAAa,EAAE,QAAQ,CAAC,IAAI,GAAG,CAAC,UAAU;CAC3C,CAAC;AAEF,MAAa,wBAAwB,EAAE,OAAO;CAC5C,UAAU,EAAE,QAAQ,CAAC,IAAI,GAAG,uBAAuB;CACnD,YAAY,EAAE,QAAQ,CAAC,IAAI,GAAG,yBAAyB;CACvD,UAAU,EAAE,KAAK,CAAC,WAAW,OAAO,CAAC;CACtC,CAAC;AAEF,MAAa,sBAAsB,EAAE,OAAO,EAC1C,UAAU,EAAE,QAAQ,CAAC,IAAI,GAAG,uBAAuB,EACpD,CAAC"}
@@ -0,0 +1,78 @@
1
+ import { columnValueSchema } from "../../../database/schema-builder/validators.js";
2
+ import { DatabasePluginError } from "../../../database/errors.js";
3
+
4
+ //#region src/plugins/database/crud/codecs.ts
5
+ /** Decimal integer with no sign padding, leading zeros, or exponent. */
6
+ const DECIMAL_INT = /^-?(0|[1-9]\d*)$/;
7
+ /**
8
+ * Map a wire value onto the runtime representation the schema validators and
9
+ * Drizzle columns expect. Only path segments and JSON scalars reach this, so
10
+ * the accepted shapes stay exact rather than coercing whatever parses.
11
+ */
12
+ function toRuntimeValue(kind, raw) {
13
+ switch (kind) {
14
+ case "number":
15
+ if (typeof raw === "number") return raw;
16
+ return typeof raw === "string" && DECIMAL_INT.test(raw) ? Number(raw) : void 0;
17
+ case "bigint":
18
+ if (typeof raw === "string" && DECIMAL_INT.test(raw)) return BigInt(raw);
19
+ return typeof raw === "number" && Number.isSafeInteger(raw) ? BigInt(raw) : void 0;
20
+ case "boolean": return typeof raw === "boolean" ? raw : void 0;
21
+ case "string":
22
+ case "uuid":
23
+ case "enum":
24
+ case "date": return typeof raw === "string" ? raw : void 0;
25
+ default: return;
26
+ }
27
+ }
28
+ function encodeValue(meta, value) {
29
+ if (value === null || value === void 0) return null;
30
+ switch (meta.kind) {
31
+ case "bigint":
32
+ if (typeof value === "bigint") return value.toString();
33
+ if (typeof value === "number" && Number.isSafeInteger(value)) return value.toString();
34
+ break;
35
+ case "number":
36
+ if (typeof value === "number" && Number.isFinite(value)) return value;
37
+ break;
38
+ case "boolean":
39
+ if (typeof value === "boolean") return value;
40
+ break;
41
+ case "string":
42
+ case "uuid":
43
+ case "enum":
44
+ case "date":
45
+ if (typeof value === "string") return value;
46
+ break;
47
+ case "json":
48
+ if (isJsonValue(value)) return value;
49
+ break;
50
+ case "unknown": break;
51
+ }
52
+ throw new DatabasePluginError("INTERNAL", "read");
53
+ }
54
+ function isJsonValue(value) {
55
+ if (value === null || typeof value === "string" || typeof value === "boolean") return true;
56
+ if (typeof value === "number") return Number.isFinite(value);
57
+ if (Array.isArray(value)) return true;
58
+ if (typeof value !== "object") return false;
59
+ const prototype = Object.getPrototypeOf(value);
60
+ return prototype === Object.prototype || prototype === null;
61
+ }
62
+ /** Compile the wire codecs for one column from its finalized metadata. */
63
+ function compileColumn(meta) {
64
+ const schema = columnValueSchema(meta);
65
+ return {
66
+ meta,
67
+ decode: (raw) => {
68
+ const candidate = toRuntimeValue(meta.kind, raw);
69
+ if (candidate === void 0) return void 0;
70
+ return schema.safeParse(candidate).success ? candidate : void 0;
71
+ },
72
+ encode: (value) => encodeValue(meta, value)
73
+ };
74
+ }
75
+
76
+ //#endregion
77
+ export { compileColumn };
78
+ //# sourceMappingURL=codecs.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"codecs.js","names":[],"sources":["../../../../src/plugins/database/crud/codecs.ts"],"sourcesContent":["import { DatabasePluginError } from \"../../../database/errors\";\nimport type { ScalarValue } from \"../../../database/runtime\";\nimport type { ColumnMeta } from \"../../../database/schema-builder\";\nimport { columnValueSchema } from \"../../../database/schema-builder/validators\";\n\n/** JSON-representable value; every generated response is built from these. */\nexport type JsonValue =\n | string\n | number\n | boolean\n | null\n | JsonValue[]\n | { [key: string]: JsonValue };\n\n/** Both wire directions for one column, compiled once per exposed table. */\nexport interface CompiledColumn {\n readonly meta: ColumnMeta;\n /** Untrusted wire value to canonical runtime value; `undefined` when invalid. */\n decode(raw: unknown): ScalarValue | undefined;\n /** Trusted runtime value to its deterministic JSON form. */\n encode(value: unknown): JsonValue;\n}\n\n/** Decimal integer with no sign padding, leading zeros, or exponent. */\nconst DECIMAL_INT = /^-?(0|[1-9]\\d*)$/;\n\n/**\n * Map a wire value onto the runtime representation the schema validators and\n * Drizzle columns expect. Only path segments and JSON scalars reach this, so\n * the accepted shapes stay exact rather than coercing whatever parses.\n */\nfunction toRuntimeValue(\n kind: ColumnMeta[\"kind\"],\n raw: unknown,\n): ScalarValue | undefined {\n switch (kind) {\n case \"number\":\n if (typeof raw === \"number\") return raw;\n return typeof raw === \"string\" && DECIMAL_INT.test(raw)\n ? Number(raw)\n : undefined;\n case \"bigint\":\n if (typeof raw === \"string\" && DECIMAL_INT.test(raw)) return BigInt(raw);\n return typeof raw === \"number\" && Number.isSafeInteger(raw)\n ? BigInt(raw)\n : undefined;\n case \"boolean\":\n return typeof raw === \"boolean\" ? raw : undefined;\n case \"string\":\n case \"uuid\":\n case \"enum\":\n case \"date\":\n return typeof raw === \"string\" ? raw : undefined;\n default:\n return undefined;\n }\n}\n\nfunction encodeValue(meta: ColumnMeta, value: unknown): JsonValue {\n if (value === null || value === undefined) return null;\n switch (meta.kind) {\n case \"bigint\":\n // JSON cannot carry a bigint, and a large id must not lose precision.\n if (typeof value === \"bigint\") return value.toString();\n if (typeof value === \"number\" && Number.isSafeInteger(value)) {\n return value.toString();\n }\n break;\n case \"number\":\n if (typeof value === \"number\" && Number.isFinite(value)) return value;\n break;\n case \"boolean\":\n if (typeof value === \"boolean\") return value;\n break;\n case \"string\":\n case \"uuid\":\n case \"enum\":\n case \"date\":\n if (typeof value === \"string\") return value;\n break;\n case \"json\":\n if (isJsonValue(value)) return value;\n break;\n case \"unknown\":\n break;\n }\n // The driver produced a value this column contract cannot describe.\n throw new DatabasePluginError(\"INTERNAL\", \"read\");\n}\n\nfunction isJsonValue(value: unknown): value is JsonValue {\n if (\n value === null ||\n typeof value === \"string\" ||\n typeof value === \"boolean\"\n ) {\n return true;\n }\n if (typeof value === \"number\") return Number.isFinite(value);\n if (Array.isArray(value)) return true;\n if (typeof value !== \"object\") return false;\n const prototype = Object.getPrototypeOf(value);\n return prototype === Object.prototype || prototype === null;\n}\n\n/** Compile the wire codecs for one column from its finalized metadata. */\nexport function compileColumn(meta: ColumnMeta): CompiledColumn {\n const schema = columnValueSchema(meta);\n return {\n meta,\n decode: (raw) => {\n const candidate = toRuntimeValue(meta.kind, raw);\n if (candidate === undefined) return undefined;\n return schema.safeParse(candidate).success ? candidate : undefined;\n },\n encode: (value) => encodeValue(meta, value),\n };\n}\n"],"mappings":";;;;;AAwBA,MAAM,cAAc;;;;;;AAOpB,SAAS,eACP,MACA,KACyB;AACzB,SAAQ,MAAR;EACE,KAAK;AACH,OAAI,OAAO,QAAQ,SAAU,QAAO;AACpC,UAAO,OAAO,QAAQ,YAAY,YAAY,KAAK,IAAI,GACnD,OAAO,IAAI,GACX;EACN,KAAK;AACH,OAAI,OAAO,QAAQ,YAAY,YAAY,KAAK,IAAI,CAAE,QAAO,OAAO,IAAI;AACxE,UAAO,OAAO,QAAQ,YAAY,OAAO,cAAc,IAAI,GACvD,OAAO,IAAI,GACX;EACN,KAAK,UACH,QAAO,OAAO,QAAQ,YAAY,MAAM;EAC1C,KAAK;EACL,KAAK;EACL,KAAK;EACL,KAAK,OACH,QAAO,OAAO,QAAQ,WAAW,MAAM;EACzC,QACE;;;AAIN,SAAS,YAAY,MAAkB,OAA2B;AAChE,KAAI,UAAU,QAAQ,UAAU,OAAW,QAAO;AAClD,SAAQ,KAAK,MAAb;EACE,KAAK;AAEH,OAAI,OAAO,UAAU,SAAU,QAAO,MAAM,UAAU;AACtD,OAAI,OAAO,UAAU,YAAY,OAAO,cAAc,MAAM,CAC1D,QAAO,MAAM,UAAU;AAEzB;EACF,KAAK;AACH,OAAI,OAAO,UAAU,YAAY,OAAO,SAAS,MAAM,CAAE,QAAO;AAChE;EACF,KAAK;AACH,OAAI,OAAO,UAAU,UAAW,QAAO;AACvC;EACF,KAAK;EACL,KAAK;EACL,KAAK;EACL,KAAK;AACH,OAAI,OAAO,UAAU,SAAU,QAAO;AACtC;EACF,KAAK;AACH,OAAI,YAAY,MAAM,CAAE,QAAO;AAC/B;EACF,KAAK,UACH;;AAGJ,OAAM,IAAI,oBAAoB,YAAY,OAAO;;AAGnD,SAAS,YAAY,OAAoC;AACvD,KACE,UAAU,QACV,OAAO,UAAU,YACjB,OAAO,UAAU,UAEjB,QAAO;AAET,KAAI,OAAO,UAAU,SAAU,QAAO,OAAO,SAAS,MAAM;AAC5D,KAAI,MAAM,QAAQ,MAAM,CAAE,QAAO;AACjC,KAAI,OAAO,UAAU,SAAU,QAAO;CACtC,MAAM,YAAY,OAAO,eAAe,MAAM;AAC9C,QAAO,cAAc,OAAO,aAAa,cAAc;;;AAIzD,SAAgB,cAAc,MAAkC;CAC9D,MAAM,SAAS,kBAAkB,KAAK;AACtC,QAAO;EACL;EACA,SAAS,QAAQ;GACf,MAAM,YAAY,eAAe,KAAK,MAAM,IAAI;AAChD,OAAI,cAAc,OAAW,QAAO;AACpC,UAAO,OAAO,UAAU,UAAU,CAAC,UAAU,YAAY;;EAE3D,SAAS,UAAU,YAAY,MAAM,MAAM;EAC5C"}
@@ -0,0 +1,151 @@
1
+ import { filterOperatorsForKind } from "../../../database/schema-builder/types.js";
2
+ import { DatabasePluginError, invalidDatabaseInput } from "../../../database/errors.js";
3
+ import { MAX_SERIALIZED_DEPTH, MAX_SERIALIZED_NODES } from "../defaults.js";
4
+ import { compileColumn } from "./codecs.js";
5
+
6
+ //#region src/plugins/database/crud/contract.ts
7
+ /** A bare object literal; a `Date`, class instance, or `Map` is not JSON. */
8
+ function isPlainObject(value) {
9
+ if (value === null || typeof value !== "object" || Array.isArray(value)) return false;
10
+ const prototype = Object.getPrototypeOf(value);
11
+ return prototype === Object.prototype || prototype === null;
12
+ }
13
+ /** A serializer that breaks its contract is trusted code failing, not input. */
14
+ function serializerFault() {
15
+ throw new DatabasePluginError("INTERNAL", "read");
16
+ }
17
+ /** Charge one value against the output budget before descending into it. */
18
+ function countNode(depth, state) {
19
+ state.nodes += 1;
20
+ if (state.nodes > MAX_SERIALIZED_NODES || depth > MAX_SERIALIZED_DEPTH) serializerFault();
21
+ }
22
+ /** Walk a container while its ancestors are tracked, so a cycle cannot pass. */
23
+ function enterObject(value, state, visit) {
24
+ if (state.ancestors.has(value)) serializerFault();
25
+ state.ancestors.add(value);
26
+ try {
27
+ return visit();
28
+ } finally {
29
+ state.ancestors.delete(value);
30
+ }
31
+ }
32
+ /** Accept a serializer's own added value only where it is already JSON. */
33
+ function sanitizeJson(value, depth, state) {
34
+ countNode(depth, state);
35
+ if (value === null || typeof value === "string" || typeof value === "boolean") return value;
36
+ if (typeof value === "number") {
37
+ if (!Number.isFinite(value)) serializerFault();
38
+ return value;
39
+ }
40
+ if (Array.isArray(value)) return enterObject(value, state, () => value.map((item) => sanitizeJson(item, depth + 1, state)));
41
+ if (!isPlainObject(value)) serializerFault();
42
+ return enterObject(value, state, () => {
43
+ const out = Object.create(null);
44
+ for (const [key, child] of Object.entries(value)) {
45
+ if (child === void 0) continue;
46
+ out[key] = sanitizeJson(child, depth + 1, state);
47
+ }
48
+ return out;
49
+ });
50
+ }
51
+ /** Keep an included row under its own table's policy, one row or many. */
52
+ function sanitizeRelation(target, value, depth, state) {
53
+ if (value === null) return null;
54
+ if (!Array.isArray(value)) return sanitizeRow(target, value, depth, state);
55
+ countNode(depth, state);
56
+ return enterObject(value, state, () => value.map((row) => sanitizeRow(target, row, depth + 1, state)));
57
+ }
58
+ /** Re-apply the private-column policy wherever the output stays contracted. */
59
+ function sanitizeRow(table, row, depth, state) {
60
+ countNode(depth, state);
61
+ if (!isPlainObject(row)) serializerFault();
62
+ return enterObject(row, state, () => {
63
+ const out = {};
64
+ for (const [key, child] of Object.entries(row)) {
65
+ if (child === void 0) continue;
66
+ if (table.columns.get(key)?.meta.isPrivate) continue;
67
+ const relation = table.relations.get(key);
68
+ out[key] = relation ? sanitizeRelation(relation.target, child, depth + 1, state) : sanitizeJson(child, depth + 1, state);
69
+ }
70
+ return out;
71
+ });
72
+ }
73
+ /** Project an included row through its own table; absent to-one reads null. */
74
+ function projectRelation(target, value) {
75
+ if (value === null || value === void 0) return null;
76
+ return Array.isArray(value) ? value.map((row) => target.projectPublicRow(row)) : target.projectPublicRow(value);
77
+ }
78
+ /** Build the public JSON for one driver row, dropping anything uncontracted. */
79
+ function projectRow(table, row) {
80
+ const out = {};
81
+ for (const [key, value] of Object.entries(row)) {
82
+ const column = table.columns.get(key);
83
+ if (column) {
84
+ if (!column.meta.isPrivate) out[key] = column.encode(value);
85
+ continue;
86
+ }
87
+ const relation = table.relations.get(key);
88
+ if (relation) out[key] = projectRelation(relation.target, value);
89
+ }
90
+ return out;
91
+ }
92
+ /** Compile one table's allowlists and codecs from its finalized metadata. */
93
+ function compileTable(table) {
94
+ const columns = /* @__PURE__ */ new Map();
95
+ const selectable = /* @__PURE__ */ new Set();
96
+ const queryable = /* @__PURE__ */ new Set();
97
+ let primaryKey;
98
+ for (const meta of Object.values(table.$columns)) {
99
+ const column = compileColumn(meta);
100
+ columns.set(meta.columnName, column);
101
+ if (meta.primaryKey && !meta.isPrivate) primaryKey = column;
102
+ if (meta.isPrivate) continue;
103
+ selectable.add(meta.columnName);
104
+ if (filterOperatorsForKind(meta.kind).length > 0) queryable.add(meta.columnName);
105
+ }
106
+ const compiled = {
107
+ name: table.$name,
108
+ primaryKey,
109
+ columns,
110
+ selectable,
111
+ queryable,
112
+ relations: /* @__PURE__ */ new Map(),
113
+ decodeId: (raw) => {
114
+ if (!primaryKey) throw new DatabasePluginError("INTERNAL", "read");
115
+ const value = primaryKey.decode(raw);
116
+ if (typeof value !== "string" && typeof value !== "number" && typeof value !== "bigint") throw invalidDatabaseInput(["id"], "Not a valid identifier");
117
+ return value;
118
+ },
119
+ projectPublicRow: (row) => projectRow(compiled, row),
120
+ sanitizeSerializedRow: (row) => sanitizeRow(compiled, row, 0, {
121
+ nodes: 0,
122
+ ancestors: /* @__PURE__ */ new Set()
123
+ })
124
+ };
125
+ return compiled;
126
+ }
127
+ /**
128
+ * Compile the HTTP contract for every exposed table and wire the relations
129
+ * they share. A relation whose target is not exposed stays unreachable, so
130
+ * enabling one table never widens another table's public surface.
131
+ */
132
+ function compileCrudTables(tables) {
133
+ const compiled = /* @__PURE__ */ new Map();
134
+ for (const table of Object.values(tables)) compiled.set(table.$name, compileTable(table));
135
+ for (const table of Object.values(tables)) {
136
+ const entry = compiled.get(table.$name);
137
+ for (const relation of table.$relations) {
138
+ const target = compiled.get(relation.targetTable);
139
+ if (!entry || !target) continue;
140
+ entry.relations.set(relation.name, {
141
+ cardinality: relation.cardinality,
142
+ target
143
+ });
144
+ }
145
+ }
146
+ return compiled;
147
+ }
148
+
149
+ //#endregion
150
+ export { compileCrudTables };
151
+ //# sourceMappingURL=contract.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"contract.js","names":[],"sources":["../../../../src/plugins/database/crud/contract.ts"],"sourcesContent":["import {\n DatabasePluginError,\n invalidDatabaseInput,\n} from \"../../../database/errors\";\nimport type { IdValue, Row } from \"../../../database/runtime\";\nimport type { AppKitTable } from \"../../../database/schema-builder\";\nimport { filterOperatorsForKind } from \"../../../database/schema-builder/types\";\nimport { MAX_SERIALIZED_DEPTH, MAX_SERIALIZED_NODES } from \"../defaults\";\nimport { type CompiledColumn, compileColumn, type JsonValue } from \"./codecs\";\n\n/** One relation edge wired to the contract of its target table. */\nexport interface CrudRelation {\n readonly cardinality: \"toOne\" | \"toMany\";\n readonly target: CrudTable;\n}\n\n/** Private HTTP contract compiled once for one explicitly exposed table. */\nexport interface CrudTable {\n readonly name: string;\n readonly primaryKey?: CompiledColumn;\n readonly columns: ReadonlyMap<string, CompiledColumn>;\n /** Public columns a request may project. */\n readonly selectable: ReadonlySet<string>;\n /** Public columns a request may filter or order by. */\n readonly queryable: ReadonlySet<string>;\n readonly relations: ReadonlyMap<string, CrudRelation>;\n decodeId(raw: string): IdValue;\n projectPublicRow(row: Row): JsonValue;\n sanitizeSerializedRow(row: unknown): JsonValue;\n}\n\ntype MutableCrudTable = Omit<CrudTable, \"relations\"> & {\n readonly relations: Map<string, CrudRelation>;\n};\n\ninterface SanitizeState {\n nodes: number;\n readonly ancestors: Set<object>;\n}\n\n/** A bare object literal; a `Date`, class instance, or `Map` is not JSON. */\nfunction isPlainObject(value: unknown): value is Record<string, unknown> {\n if (value === null || typeof value !== \"object\" || Array.isArray(value)) {\n return false;\n }\n const prototype = Object.getPrototypeOf(value);\n return prototype === Object.prototype || prototype === null;\n}\n\n/** A serializer that breaks its contract is trusted code failing, not input. */\nfunction serializerFault(): never {\n throw new DatabasePluginError(\"INTERNAL\", \"read\");\n}\n\n/** Charge one value against the output budget before descending into it. */\nfunction countNode(depth: number, state: SanitizeState): void {\n state.nodes += 1;\n if (state.nodes > MAX_SERIALIZED_NODES || depth > MAX_SERIALIZED_DEPTH) {\n serializerFault();\n }\n}\n\n/** Walk a container while its ancestors are tracked, so a cycle cannot pass. */\nfunction enterObject<T>(\n value: object,\n state: SanitizeState,\n visit: () => T,\n): T {\n if (state.ancestors.has(value)) serializerFault();\n state.ancestors.add(value);\n try {\n return visit();\n } finally {\n state.ancestors.delete(value);\n }\n}\n\n/** Accept a serializer's own added value only where it is already JSON. */\nfunction sanitizeJson(\n value: unknown,\n depth: number,\n state: SanitizeState,\n): JsonValue {\n countNode(depth, state);\n if (\n value === null ||\n typeof value === \"string\" ||\n typeof value === \"boolean\"\n ) {\n return value;\n }\n if (typeof value === \"number\") {\n if (!Number.isFinite(value)) serializerFault();\n return value;\n }\n if (Array.isArray(value)) {\n return enterObject(value, state, () =>\n value.map((item) => sanitizeJson(item, depth + 1, state)),\n );\n }\n if (!isPlainObject(value)) serializerFault();\n return enterObject(value, state, () => {\n // A null prototype keeps `__proto__` an ordinary key instead of a setter.\n const out: Record<string, JsonValue> = Object.create(null);\n for (const [key, child] of Object.entries(value)) {\n if (child === undefined) continue;\n out[key] = sanitizeJson(child, depth + 1, state);\n }\n return out;\n });\n}\n\n/** Keep an included row under its own table's policy, one row or many. */\nfunction sanitizeRelation(\n target: CrudTable,\n value: unknown,\n depth: number,\n state: SanitizeState,\n): JsonValue {\n if (value === null) return null;\n if (!Array.isArray(value)) return sanitizeRow(target, value, depth, state);\n countNode(depth, state);\n return enterObject(value, state, () =>\n value.map((row) => sanitizeRow(target, row, depth + 1, state)),\n );\n}\n\n/** Re-apply the private-column policy wherever the output stays contracted. */\nfunction sanitizeRow(\n table: CrudTable,\n row: unknown,\n depth: number,\n state: SanitizeState,\n): JsonValue {\n countNode(depth, state);\n if (!isPlainObject(row)) serializerFault();\n return enterObject(row, state, () => {\n const out: Record<string, JsonValue> = {};\n for (const [key, child] of Object.entries(row)) {\n if (child === undefined) continue;\n if (table.columns.get(key)?.meta.isPrivate) continue;\n const relation = table.relations.get(key);\n out[key] = relation\n ? sanitizeRelation(relation.target, child, depth + 1, state)\n : sanitizeJson(child, depth + 1, state);\n }\n return out;\n });\n}\n\n/** Project an included row through its own table; absent to-one reads null. */\nfunction projectRelation(target: CrudTable, value: unknown): JsonValue {\n if (value === null || value === undefined) return null;\n return Array.isArray(value)\n ? value.map((row) => target.projectPublicRow(row as Row))\n : target.projectPublicRow(value as Row);\n}\n\n/** Build the public JSON for one driver row, dropping anything uncontracted. */\nfunction projectRow(table: CrudTable, row: Row): JsonValue {\n const out: Record<string, JsonValue> = {};\n for (const [key, value] of Object.entries(row)) {\n const column = table.columns.get(key);\n if (column) {\n // Whatever the driver returned, only public contracted columns ship.\n if (!column.meta.isPrivate) out[key] = column.encode(value);\n continue;\n }\n const relation = table.relations.get(key);\n if (relation) out[key] = projectRelation(relation.target, value);\n }\n return out;\n}\n\n/** Compile one table's allowlists and codecs from its finalized metadata. */\nfunction compileTable(table: AppKitTable): MutableCrudTable {\n const columns = new Map<string, CompiledColumn>();\n const selectable = new Set<string>();\n const queryable = new Set<string>();\n let primaryKey: CompiledColumn | undefined;\n\n for (const meta of Object.values(table.$columns)) {\n const column = compileColumn(meta);\n columns.set(meta.columnName, column);\n // A private key must not power `GET /:table/:id`: per-id probing would\n // answer 200 or 404 on an identifier the schema hides, so over HTTP the\n // table is keyless — no detail route, and lists must name their own order.\n if (meta.primaryKey && !meta.isPrivate) primaryKey = column;\n if (meta.isPrivate) continue;\n selectable.add(meta.columnName);\n if (filterOperatorsForKind(meta.kind).length > 0) {\n queryable.add(meta.columnName);\n }\n }\n\n const compiled: MutableCrudTable = {\n name: table.$name,\n primaryKey,\n columns,\n selectable,\n queryable,\n relations: new Map(),\n decodeId: (raw) => {\n if (!primaryKey) throw new DatabasePluginError(\"INTERNAL\", \"read\");\n const value = primaryKey.decode(raw);\n if (\n typeof value !== \"string\" &&\n typeof value !== \"number\" &&\n typeof value !== \"bigint\"\n ) {\n throw invalidDatabaseInput([\"id\"], \"Not a valid identifier\");\n }\n return value;\n },\n projectPublicRow: (row) => projectRow(compiled, row),\n sanitizeSerializedRow: (row) =>\n sanitizeRow(compiled, row, 0, { nodes: 0, ancestors: new Set() }),\n };\n return compiled;\n}\n\n/**\n * Compile the HTTP contract for every exposed table and wire the relations\n * they share. A relation whose target is not exposed stays unreachable, so\n * enabling one table never widens another table's public surface.\n */\nexport function compileCrudTables(\n tables: Record<string, AppKitTable>,\n): Map<string, CrudTable> {\n const compiled = new Map<string, MutableCrudTable>();\n for (const table of Object.values(tables)) {\n compiled.set(table.$name, compileTable(table));\n }\n for (const table of Object.values(tables)) {\n const entry = compiled.get(table.$name);\n for (const relation of table.$relations) {\n const target = compiled.get(relation.targetTable);\n if (!entry || !target) continue;\n entry.relations.set(relation.name, {\n cardinality: relation.cardinality,\n target,\n });\n }\n }\n return compiled as Map<string, CrudTable>;\n}\n"],"mappings":"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"}
@@ -0,0 +1,40 @@
1
+ import { databaseSetupFailed } from "../../../database/errors.js";
2
+
3
+ //#region src/plugins/database/crud/exposure.ts
4
+ /** A table name also becomes a URL path segment, so keep it unambiguous. */
5
+ const ROUTABLE_TABLE = /^[A-Za-z][A-Za-z0-9_-]{0,63}$/;
6
+ /** Refuse names that cannot address exactly one table over HTTP. */
7
+ function assertRoutable(names) {
8
+ const lowercased = /* @__PURE__ */ new Set();
9
+ for (const name of names) {
10
+ if (!ROUTABLE_TABLE.test(name) || lowercased.has(name.toLowerCase())) throw databaseSetupFailed();
11
+ lowercased.add(name.toLowerCase());
12
+ }
13
+ }
14
+ /**
15
+ * Resolve the tables whose generated reads are explicitly turned on. The
16
+ * exposure value arrives untyped because a finalized schema widens its table
17
+ * names to `string`, so every name is re-checked against the declared schema.
18
+ */
19
+ function resolveExposedTables(exposure, declared) {
20
+ if (exposure === void 0 || exposure === false) return [];
21
+ if (exposure === true) {
22
+ assertRoutable(declared);
23
+ return [...declared];
24
+ }
25
+ if (typeof exposure !== "object" || exposure === null) throw databaseSetupFailed();
26
+ const requested = exposure.tables;
27
+ if (!Array.isArray(requested)) throw databaseSetupFailed();
28
+ const names = [];
29
+ for (const name of requested) {
30
+ if (typeof name !== "string" || !declared.includes(name)) throw databaseSetupFailed();
31
+ if (names.includes(name)) throw databaseSetupFailed();
32
+ names.push(name);
33
+ }
34
+ assertRoutable(names);
35
+ return names;
36
+ }
37
+
38
+ //#endregion
39
+ export { resolveExposedTables };
40
+ //# sourceMappingURL=exposure.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"exposure.js","names":[],"sources":["../../../../src/plugins/database/crud/exposure.ts"],"sourcesContent":["import { databaseSetupFailed } from \"../../../database/errors\";\n\n/** A table name also becomes a URL path segment, so keep it unambiguous. */\nconst ROUTABLE_TABLE = /^[A-Za-z][A-Za-z0-9_-]{0,63}$/;\n\n/** Refuse names that cannot address exactly one table over HTTP. */\nfunction assertRoutable(names: readonly string[]): void {\n const lowercased = new Set<string>();\n for (const name of names) {\n // Express matches paths case-insensitively, so near-duplicates would alias.\n if (!ROUTABLE_TABLE.test(name) || lowercased.has(name.toLowerCase())) {\n throw databaseSetupFailed();\n }\n lowercased.add(name.toLowerCase());\n }\n}\n\n/**\n * Resolve the tables whose generated reads are explicitly turned on. The\n * exposure value arrives untyped because a finalized schema widens its table\n * names to `string`, so every name is re-checked against the declared schema.\n */\nexport function resolveExposedTables(\n exposure: unknown,\n declared: readonly string[],\n): string[] {\n if (exposure === undefined || exposure === false) return [];\n if (exposure === true) {\n assertRoutable(declared);\n return [...declared];\n }\n if (typeof exposure !== \"object\" || exposure === null) {\n throw databaseSetupFailed();\n }\n const requested = (exposure as { tables?: unknown }).tables;\n if (!Array.isArray(requested)) throw databaseSetupFailed();\n\n const names: string[] = [];\n for (const name of requested) {\n // Unknown and duplicate names are configuration bugs, not empty routes.\n if (typeof name !== \"string\" || !declared.includes(name)) {\n throw databaseSetupFailed();\n }\n if (names.includes(name)) throw databaseSetupFailed();\n names.push(name);\n }\n assertRoutable(names);\n return names;\n}\n"],"mappings":";;;;AAGA,MAAM,iBAAiB;;AAGvB,SAAS,eAAe,OAAgC;CACtD,MAAM,6BAAa,IAAI,KAAa;AACpC,MAAK,MAAM,QAAQ,OAAO;AAExB,MAAI,CAAC,eAAe,KAAK,KAAK,IAAI,WAAW,IAAI,KAAK,aAAa,CAAC,CAClE,OAAM,qBAAqB;AAE7B,aAAW,IAAI,KAAK,aAAa,CAAC;;;;;;;;AAStC,SAAgB,qBACd,UACA,UACU;AACV,KAAI,aAAa,UAAa,aAAa,MAAO,QAAO,EAAE;AAC3D,KAAI,aAAa,MAAM;AACrB,iBAAe,SAAS;AACxB,SAAO,CAAC,GAAG,SAAS;;AAEtB,KAAI,OAAO,aAAa,YAAY,aAAa,KAC/C,OAAM,qBAAqB;CAE7B,MAAM,YAAa,SAAkC;AACrD,KAAI,CAAC,MAAM,QAAQ,UAAU,CAAE,OAAM,qBAAqB;CAE1D,MAAM,QAAkB,EAAE;AAC1B,MAAK,MAAM,QAAQ,WAAW;AAE5B,MAAI,OAAO,SAAS,YAAY,CAAC,SAAS,SAAS,KAAK,CACtD,OAAM,qBAAqB;AAE7B,MAAI,MAAM,SAAS,KAAK,CAAE,OAAM,qBAAqB;AACrD,QAAM,KAAK,KAAK;;AAElB,gBAAe,MAAM;AACrB,QAAO"}