cvx-kit 0.1.2 → 0.1.4

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 (88) hide show
  1. package/CHANGELOG.md +38 -0
  2. package/README.md +69 -24
  3. package/dist/agent-tools.d.mts +28 -12
  4. package/dist/agent-tools.mjs +1 -1
  5. package/dist/agent-tools.mjs.map +1 -1
  6. package/dist/auth.d.mts +4 -2
  7. package/dist/auth.mjs +1 -1
  8. package/dist/auth.mjs.map +1 -1
  9. package/dist/components/approvals/_generated/api.d.mts +1 -1
  10. package/dist/components/approvals/audit.mjs +1 -1
  11. package/dist/components/approvals/audit.mjs.map +1 -1
  12. package/dist/components/approvals/client.d.mts +1 -1
  13. package/dist/components/approvals/client.mjs +1 -1
  14. package/dist/components/approvals/client.mjs.map +1 -1
  15. package/dist/components/approvals/constants.mjs.map +1 -1
  16. package/dist/components/approvals/decisions.d.mts +6 -7
  17. package/dist/components/approvals/decisions.mjs +1 -1
  18. package/dist/components/approvals/decisions.mjs.map +1 -1
  19. package/dist/components/approvals/functions.mjs +1 -1
  20. package/dist/components/approvals/functions.mjs.map +1 -1
  21. package/dist/components/approvals/health.mjs.map +1 -1
  22. package/dist/components/approvals/requests.mjs.map +1 -1
  23. package/dist/components/approvals/validators.mjs.map +1 -1
  24. package/dist/components/approvals/workflow.d.mts +1 -1
  25. package/dist/components/approvals/workflow.mjs +1 -1
  26. package/dist/components/approvals/workflow.mjs.map +1 -1
  27. package/dist/components/approvals/workflow_steps.d.mts +3 -3
  28. package/dist/components/approvals/workflow_steps.mjs +1 -1
  29. package/dist/components/approvals/workflow_steps.mjs.map +1 -1
  30. package/dist/components/foundation/client.d.mts +27 -28
  31. package/dist/components/foundation/client.mjs +1 -1
  32. package/dist/components/foundation/client.mjs.map +1 -1
  33. package/dist/components/foundation/modules/command/command.d.mts +17 -8
  34. package/dist/components/foundation/modules/command/command.mjs +1 -1
  35. package/dist/components/foundation/modules/command/command.mjs.map +1 -1
  36. package/dist/components/foundation/modules/observability/observability.d.mts +1 -1
  37. package/dist/components/foundation/modules/observability/observability.mjs +1 -1
  38. package/dist/components/foundation/modules/observability/observability.mjs.map +1 -1
  39. package/dist/components/foundation/modules/query/query.d.mts +8 -8
  40. package/dist/components/foundation/modules/query/query.mjs +1 -1
  41. package/dist/components/foundation/modules/query/query.mjs.map +1 -1
  42. package/dist/components/foundation/operation.d.mts +74 -0
  43. package/dist/components/foundation/operation.mjs +2 -0
  44. package/dist/components/foundation/operation.mjs.map +1 -0
  45. package/dist/components/foundation/result.d.mts +1 -1
  46. package/dist/components/foundation/result.mjs.map +1 -1
  47. package/dist/components/foundation/telemetry.mjs +1 -1
  48. package/dist/components/foundation/telemetry.mjs.map +1 -1
  49. package/dist/crud.d.mts +177 -16
  50. package/dist/crud.mjs +1 -1
  51. package/dist/crud.mjs.map +1 -1
  52. package/dist/middleware.d.mts +7 -7
  53. package/dist/middleware.mjs +1 -1
  54. package/dist/middleware.mjs.map +1 -1
  55. package/dist/oxlint.d.mts +9 -0
  56. package/dist/oxlint.mjs +2 -0
  57. package/dist/oxlint.mjs.map +1 -0
  58. package/dist/tenancy.mjs +1 -1
  59. package/dist/tenancy.mjs.map +1 -1
  60. package/dist/triggers.mjs +1 -1
  61. package/dist/triggers.mjs.map +1 -1
  62. package/dist/webhooks.d.mts +17 -106
  63. package/dist/webhooks.mjs.map +1 -1
  64. package/dist/zod-table.d.mts +47 -91
  65. package/dist/zod-table.mjs +1 -1
  66. package/dist/zod-table.mjs.map +1 -1
  67. package/docs/approvals.md +57 -56
  68. package/docs/architecture.md +1 -1
  69. package/docs/auth.md +57 -33
  70. package/docs/commands.md +159 -44
  71. package/docs/conventions.md +40 -40
  72. package/docs/crud.md +5 -5
  73. package/docs/llms.md +117 -71
  74. package/docs/maintainability.md +4 -4
  75. package/docs/migration.md +33 -33
  76. package/docs/tenancy.md +5 -8
  77. package/docs/triggers.md +4 -2
  78. package/docs/upgrading.md +74 -7
  79. package/docs/zod-table.md +70 -45
  80. package/package.json +41 -28
  81. package/scripts/convex-config-bridge.mjs +7 -18
  82. package/scripts/extract-changelog.mjs +1 -1
  83. package/scripts/smoke-packed-components.mjs +35 -75
  84. package/scripts/test-packed-ids.mjs +149 -0
  85. package/scripts/test-packed-oxlint.mjs +121 -0
  86. package/src/test.ts +9 -17
  87. package/docs/plans/2026-08-14-001-feat-v0.1.0-feature-set-plan.md +0 -461
  88. package/docs/plans/2026-08-21-0930-feat-resolve-organization-hook-plan.md +0 -178
@@ -1 +1 @@
1
- {"version":3,"file":"result.mjs","names":[],"sources":["../../../src/components/foundation/result.ts"],"sourcesContent":["import type { TransactionMetrics } from 'convex/server'\n\nexport type TransactionMetricsContext = Readonly<{\n\tmeta: Readonly<{\n\t\tgetTransactionMetrics: () => Promise<TransactionMetrics>\n\t}>\n}>\n\nexport type Result<Value, Failure> =\n\t| Readonly<{ ok: true; value: Value }>\n\t| Readonly<{ ok: false; error: Failure }>\n\nexport type ResultBoundary<Failure> = Readonly<{\n\tok: <Value>(value: Value) => Result<Value, Failure>\n\thandledError: <Value = never>(failure: Failure) => Result<Value, Failure>\n\tdataOf: (error: unknown) => Failure | undefined\n}>\n\nconst metricNames = [\n\t'bytesRead',\n\t'bytesWritten',\n\t'databaseQueries',\n\t'documentsRead',\n\t'documentsWritten',\n\t'functionsScheduled',\n\t'scheduledFunctionArgsBytes',\n] as const satisfies readonly (keyof TransactionMetrics)[]\n\nconst effectMetricNames = [\n\t'bytesWritten',\n\t'documentsWritten',\n\t'functionsScheduled',\n\t'scheduledFunctionArgsBytes',\n] as const satisfies readonly (keyof TransactionMetrics)[]\n\n/** Return handled failures only before effects; otherwise rethrow for rollback. */\nexport async function executeResultBoundary<Value, Failure>(\n\tctx: TransactionMetricsContext,\n\toperation: () => Promise<Value>,\n\tboundary: ResultBoundary<Failure>,\n): Promise<Result<Value, Failure>> {\n\ttry {\n\t\treturn boundary.ok(await operation())\n\t} catch (error) {\n\t\tconst handled = boundary.dataOf(error)\n\t\tif (handled === undefined) throw error\n\t\ttry {\n\t\t\tif (hasEffects(await ctx.meta.getTransactionMetrics())) throw error\n\t\t} catch {\n\t\t\tthrow error\n\t\t}\n\t\treturn boundary.handledError(handled)\n\t}\n}\n\nexport function projectResult<Value, Failure>(\n\tresult: Result<Value, Failure>,\n\tprojectFailure: (failure: Failure) => never,\n): Value {\n\treturn result.ok ? result.value : projectFailure(result.error)\n}\n\nfunction hasEffects(metrics: TransactionMetrics): boolean {\n\tfor (const name of metricNames) {\n\t\tconst metric = metrics[name]\n\t\tif (\n\t\t\t!metric ||\n\t\t\t!Number.isFinite(metric.used) ||\n\t\t\tmetric.used < 0 ||\n\t\t\t!Number.isFinite(metric.remaining) ||\n\t\t\tmetric.remaining < 0\n\t\t) {\n\t\t\tthrow new Error('Unreadable transaction metrics')\n\t\t}\n\t}\n\treturn effectMetricNames.some((name) => metrics[name].used > 0)\n}\n"],"mappings":"AAkBA,MAAM,EAAc,CACnB,YACA,eACA,kBACA,gBACA,mBACA,qBACA,4BACD,EAEM,EAAoB,CACzB,eACA,mBACA,qBACA,4BACD,EAGA,eAAsB,EACrB,EACA,EACA,EACkC,CAClC,GAAI,CACH,OAAO,EAAS,GAAG,MAAM,EAAU,CAAC,CACrC,OAAS,EAAO,CACf,IAAM,EAAU,EAAS,OAAO,CAAK,EACrC,GAAI,IAAY,IAAA,GAAW,MAAM,EACjC,GAAI,CACH,GAAI,EAAW,MAAM,EAAI,KAAK,sBAAsB,CAAC,EAAG,MAAM,CAC/D,MAAQ,CACP,MAAM,CACP,CACA,OAAO,EAAS,aAAa,CAAO,CACrC,CACD,CAEA,SAAgB,EACf,EACA,EACQ,CACR,OAAO,EAAO,GAAK,EAAO,MAAQ,EAAe,EAAO,KAAK,CAC9D,CAEA,SAAS,EAAW,EAAsC,CACzD,IAAK,IAAM,KAAQ,EAAa,CAC/B,IAAM,EAAS,EAAQ,GACvB,GACC,CAAC,GACD,CAAC,OAAO,SAAS,EAAO,IAAI,GAC5B,EAAO,KAAO,GACd,CAAC,OAAO,SAAS,EAAO,SAAS,GACjC,EAAO,UAAY,EAEnB,MAAU,MAAM,gCAAgC,CAElD,CACA,OAAO,EAAkB,KAAM,GAAS,EAAQ,EAAK,CAAC,KAAO,CAAC,CAC/D"}
1
+ {"version":3,"file":"result.mjs","names":[],"sources":["../../../src/components/foundation/result.ts"],"sourcesContent":["import type { TransactionMetrics } from 'convex/server'\n\nexport type TransactionMetricsContext = Readonly<{\n\tmeta: Readonly<{\n\t\tgetTransactionMetrics: () => Promise<TransactionMetrics>\n\t}>\n}>\n\nexport type Result<Value, Failure> =\n\t| Readonly<{ ok: true; value: Value }>\n\t| Readonly<{ ok: false; error: Failure }>\n\nexport type ResultBoundary<Failure> = Readonly<{\n\tok: <Value>(value: Value) => Result<Value, Failure>\n\thandledError: <Value = never>(failure: Failure) => Result<Value, Failure>\n\tdataOf: (cause: unknown) => Failure | undefined\n}>\n\nconst metricNames = [\n\t'bytesRead',\n\t'bytesWritten',\n\t'databaseQueries',\n\t'documentsRead',\n\t'documentsWritten',\n\t'functionsScheduled',\n\t'scheduledFunctionArgsBytes',\n] as const satisfies readonly (keyof TransactionMetrics)[]\n\nconst effectMetricNames = [\n\t'bytesWritten',\n\t'documentsWritten',\n\t'functionsScheduled',\n\t'scheduledFunctionArgsBytes',\n] as const satisfies readonly (keyof TransactionMetrics)[]\n\n/** Return handled failures only before effects; otherwise rethrow for rollback. */\nexport async function executeResultBoundary<Value, Failure>(\n\tctx: TransactionMetricsContext,\n\toperation: () => Promise<Value>,\n\tboundary: ResultBoundary<Failure>,\n): Promise<Result<Value, Failure>> {\n\ttry {\n\t\treturn boundary.ok(await operation())\n\t} catch (error) {\n\t\tconst handled = boundary.dataOf(error)\n\t\tif (handled === undefined) throw error\n\t\ttry {\n\t\t\tif (hasEffects(await ctx.meta.getTransactionMetrics())) throw error\n\t\t} catch {\n\t\t\tthrow error\n\t\t}\n\t\treturn boundary.handledError(handled)\n\t}\n}\n\nexport function projectResult<Value, Failure>(\n\tresult: Result<Value, Failure>,\n\tprojectFailure: (failure: Failure) => never,\n): Value {\n\treturn result.ok ? result.value : projectFailure(result.error)\n}\n\nfunction hasEffects(metrics: TransactionMetrics): boolean {\n\tfor (const name of metricNames) {\n\t\tconst metric = metrics[name]\n\t\tif (\n\t\t\t!metric ||\n\t\t\t!Number.isFinite(metric.used) ||\n\t\t\tmetric.used < 0 ||\n\t\t\t!Number.isFinite(metric.remaining) ||\n\t\t\tmetric.remaining < 0\n\t\t) {\n\t\t\tthrow new Error('Unreadable transaction metrics')\n\t\t}\n\t}\n\treturn effectMetricNames.some((name) => metrics[name].used > 0)\n}\n"],"mappings":"AAkBA,MAAM,EAAc,CACnB,YACA,eACA,kBACA,gBACA,mBACA,qBACA,4BACD,EAEM,EAAoB,CACzB,eACA,mBACA,qBACA,4BACD,EAGA,eAAsB,EACrB,EACA,EACA,EACkC,CAClC,GAAI,CACH,OAAO,EAAS,GAAG,MAAM,EAAU,CAAC,CACrC,OAAS,EAAO,CACf,IAAM,EAAU,EAAS,OAAO,CAAK,EACrC,GAAI,IAAY,IAAA,GAAW,MAAM,EACjC,GAAI,CACH,GAAI,EAAW,MAAM,EAAI,KAAK,sBAAsB,CAAC,EAAG,MAAM,CAC/D,MAAQ,CACP,MAAM,CACP,CACA,OAAO,EAAS,aAAa,CAAO,CACrC,CACD,CAEA,SAAgB,EACf,EACA,EACQ,CACR,OAAO,EAAO,GAAK,EAAO,MAAQ,EAAe,EAAO,KAAK,CAC9D,CAEA,SAAS,EAAW,EAAsC,CACzD,IAAK,IAAM,KAAQ,EAAa,CAC/B,IAAM,EAAS,EAAQ,GACvB,GACC,CAAC,GACD,CAAC,OAAO,SAAS,EAAO,IAAI,GAC5B,EAAO,KAAO,GACd,CAAC,OAAO,SAAS,EAAO,SAAS,GACjC,EAAO,UAAY,EAEnB,MAAU,MAAM,gCAAgC,CAElD,CACA,OAAO,EAAkB,KAAM,GAAS,EAAQ,EAAK,CAAC,KAAO,CAAC,CAC/D"}
@@ -1,2 +1,2 @@
1
- const e=/^[A-Za-z][A-Za-z0-9_.-]{0,159}$/;function t(t,n={}){try{if(!e.test(t))return;let r={event:t};for(let[t,i]of Object.entries(n))!e.test(t)||i===void 0||typeof i==`string`&&!e.test(i)||typeof i==`number`&&!Number.isFinite(i)||(r[t]=i);console.info(JSON.stringify(r))}catch{}}export{t as emitSemanticEvent};
1
+ import{z as e}from"zod";const t=/^[A-Za-z][A-Za-z0-9_.-]{0,159}$/,n=e.union([e.string().regex(t),e.number().finite(),e.boolean()]);function r(e,r={}){try{if(!t.test(e))return;let i=Object.fromEntries(Object.entries(r).filter(([e,r])=>t.test(e)&&n.safeParse(r).success));console.info(JSON.stringify({event:e,...i}))}catch{}}export{r as emitSemanticEvent};
2
2
  //# sourceMappingURL=telemetry.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"telemetry.mjs","names":[],"sources":["../../../src/components/foundation/telemetry.ts"],"sourcesContent":["type SemanticValue = string | number | boolean\n\nconst semanticToken = /^[A-Za-z][A-Za-z0-9_.-]{0,159}$/\n\n/** Emit bounded semantic fields only; free-form payloads and identifiers drop. */\nexport function emitSemanticEvent(\n\tevent: string,\n\tfields: Readonly<Record<string, SemanticValue | undefined>> = {},\n): void {\n\ttry {\n\t\tif (!semanticToken.test(event)) return\n\t\tconst safe: Record<string, SemanticValue> = { event }\n\t\tfor (const [key, value] of Object.entries(fields)) {\n\t\t\tif (!semanticToken.test(key) || value === undefined) continue\n\t\t\tif (typeof value === 'string' && !semanticToken.test(value)) continue\n\t\t\tif (typeof value === 'number' && !Number.isFinite(value)) continue\n\t\t\tsafe[key] = value\n\t\t}\n\t\tconsole.info(JSON.stringify(safe))\n\t} catch {\n\t\t// Telemetry must never change command behavior.\n\t}\n}\n"],"mappings":"AAEA,MAAM,EAAgB,kCAGtB,SAAgB,EACf,EACA,EAA8D,CAAC,EACxD,CACP,GAAI,CACH,GAAI,CAAC,EAAc,KAAK,CAAK,EAAG,OAChC,IAAM,EAAsC,CAAE,OAAM,EACpD,IAAK,GAAM,CAAC,EAAK,KAAU,OAAO,QAAQ,CAAM,EAC3C,CAAC,EAAc,KAAK,CAAG,GAAK,IAAU,IAAA,IACtC,OAAO,GAAU,UAAY,CAAC,EAAc,KAAK,CAAK,GACtD,OAAO,GAAU,UAAY,CAAC,OAAO,SAAS,CAAK,IACvD,EAAK,GAAO,GAEb,QAAQ,KAAK,KAAK,UAAU,CAAI,CAAC,CAClC,MAAQ,CAER,CACD"}
1
+ {"version":3,"file":"telemetry.mjs","names":[],"sources":["../../../src/components/foundation/telemetry.ts"],"sourcesContent":["import { z } from 'zod'\n\ntype SemanticValue = string | number | boolean\n\nconst semanticToken = /^[A-Za-z][A-Za-z0-9_.-]{0,159}$/\nconst semanticValue = z.union([z.string().regex(semanticToken), z.number().finite(), z.boolean()])\n\n/** Emit bounded semantic fields only; free-form payloads and identifiers drop. */\nexport function emitSemanticEvent(\n\tevent: string,\n\tfields: Readonly<Record<string, SemanticValue | undefined>> = {},\n): void {\n\ttry {\n\t\tif (!semanticToken.test(event)) return\n\t\tconst safe = Object.fromEntries(\n\t\t\tObject.entries(fields).filter(\n\t\t\t\t([key, value]) => semanticToken.test(key) && semanticValue.safeParse(value).success,\n\t\t\t),\n\t\t)\n\t\tconsole.info(JSON.stringify({ event, ...safe }))\n\t} catch {\n\t\t// Telemetry must never change command behavior.\n\t}\n}\n"],"mappings":"wBAIA,MAAM,EAAgB,kCAChB,EAAgB,EAAE,MAAM,CAAC,EAAE,OAAO,CAAC,CAAC,MAAM,CAAa,EAAG,EAAE,OAAO,CAAC,CAAC,OAAO,EAAG,EAAE,QAAQ,CAAC,CAAC,EAGjG,SAAgB,EACf,EACA,EAA8D,CAAC,EACxD,CACP,GAAI,CACH,GAAI,CAAC,EAAc,KAAK,CAAK,EAAG,OAChC,IAAM,EAAO,OAAO,YACnB,OAAO,QAAQ,CAAM,CAAC,CAAC,QACrB,CAAC,EAAK,KAAW,EAAc,KAAK,CAAG,GAAK,EAAc,UAAU,CAAK,CAAC,CAAC,OAC7E,CACD,EACA,QAAQ,KAAK,KAAK,UAAU,CAAE,QAAO,GAAG,CAAK,CAAC,CAAC,CAChD,MAAQ,CAER,CACD"}
package/dist/crud.d.mts CHANGED
@@ -1,6 +1,9 @@
1
1
  import { ErrorFactory } from "./errors.mjs";
2
- import { AnyCommandMiddleware, ApplicationCommand, AuditedRegistry, CommandConstructor } from "./components/foundation/client.mjs";
2
+ import { SchemaInput } from "./components/foundation/modules/command/command.mjs";
3
+ import { AnyCommandMiddleware, ApplicationCommand, AuditedOperation, CommandConstructor } from "./components/foundation/client.mjs";
4
+ import { GenericDataModel, GenericMutationCtx } from "convex/server";
3
5
  import { z } from "zod";
6
+ import { Value } from "convex/values";
4
7
  //#region src/crud.d.ts
5
8
  /**
6
9
  * The zodTable surface the factory consumes — structural, so any zodTable /
@@ -8,20 +11,17 @@ import { z } from "zod";
8
11
  */
9
12
  type CrudTable = {
10
13
  tableName: string;
11
- commandInput: z.ZodObject<Record<string, z.ZodType>>;
12
- storage: z.ZodObject<Record<string, z.ZodType>>;
14
+ commandInput: z.ZodObject<Record<string, z.ZodType<Value | undefined>>>;
15
+ storage: z.ZodObject<Record<string, z.ZodType<Value | undefined>>>;
13
16
  };
14
17
  type CrudContext = {
15
- db: {
16
- insert: (table: never, value: never) => Promise<unknown>;
17
- patch: (id: never, value: never) => Promise<unknown>;
18
- };
18
+ db: Pick<GenericMutationCtx<GenericDataModel>['db'], 'insert' | 'patch'>;
19
19
  };
20
20
  type MaybePromise<Value> = Value | Promise<Value>;
21
- type CrudConfig<Context extends CrudContext> = {
21
+ type CrudConfig<Context extends CrudContext, Table extends CrudTable = CrudTable> = {
22
22
  /** The Foundation-bound Command class (destructured from the facade). */
23
23
  Command: CommandConstructor;
24
- table: CrudTable;
24
+ table: Table;
25
25
  /** Audit aggregate type; enforced by the aggregates allowlist. */
26
26
  aggregateType: string;
27
27
  /** Observability/audit classification. Default 'business'. */
@@ -33,7 +33,7 @@ type CrudConfig<Context extends CrudContext> = {
33
33
  * REQUIRED when the table is a tenantTable — forgetting it would insert
34
34
  * untenanted rows; the factory fails fast at construction instead.
35
35
  */
36
- enrich?: (context: Context, command: Record<string, unknown>) => Record<string, unknown> | Promise<Record<string, unknown>>;
36
+ enrich?: (context: Context, command: ReturnType<Table['commandInput']['parse']>) => MaybePromise<Partial<z.input<Table['storage']>>>;
37
37
  /** Per-operation preconditions; run before the handlers. */
38
38
  guards?: {
39
39
  create?: (context: Context, command: never) => MaybePromise<void>;
@@ -55,11 +55,172 @@ type CrudConfig<Context extends CrudContext> = {
55
55
  * used — it bypasses the command pipeline (no audits, no guards, and hard
56
56
  * deletes). See docs/crud.md.
57
57
  */
58
- type CrudCommands<Context extends CrudContext> = {
59
- commands: ApplicationCommand<Context, AuditedRegistry>;
60
- operations: AuditedRegistry;
61
- executeCreate: (context: Context, command: Record<string, unknown>) => Promise<{
62
- id: string;
58
+ type CrudCommands<Context extends CrudContext, Table extends CrudTable = CrudTable> = ReturnType<typeof createCrudCommands<Context, Table>>;
59
+ declare function createCrudCommands<Context extends CrudContext, Table extends CrudTable = CrudTable>(config: CrudConfig<Context, Table>): {
60
+ commands: ApplicationCommand<Context, NamedOperation<`${string}.create`, {
61
+ command: Table["commandInput"];
62
+ result: z.ZodObject<{
63
+ id: import("convex-helpers/server/zod4").Zid<string>;
64
+ }, z.core.$strict>;
65
+ classification: string;
66
+ aggregates: string[];
67
+ middleware: readonly AnyCommandMiddleware[] | undefined;
68
+ guard: ((context: Context, command: never) => MaybePromise<void>) | undefined;
69
+ audit: (resolution: {
70
+ result: z.output<z.ZodObject<{
71
+ id: import("convex-helpers/server/zod4").Zid<string>;
72
+ }, z.core.$strict>>;
73
+ }, context: Context) => {
74
+ operation: `${string}.create`;
75
+ actorId: string;
76
+ aggregate: {
77
+ type: string;
78
+ id: string & {
79
+ __tableName: string;
80
+ };
81
+ };
82
+ };
83
+ }> & NamedOperation<`${string}.update`, {
84
+ command: z.ZodObject<{
85
+ id: import("convex-helpers/server/zod4").Zid<string>;
86
+ data: z.ZodObject<{
87
+ [x: string]: z.ZodOptional<z.ZodType<Value | undefined, unknown, z.core.$ZodTypeInternals<Value | undefined, unknown>>>;
88
+ }, z.core.$strict>;
89
+ }, z.core.$strict>;
90
+ result: z.ZodObject<{
91
+ ok: z.ZodLiteral<true>;
92
+ }, z.core.$strict>;
93
+ classification: string;
94
+ aggregates: string[];
95
+ middleware: readonly AnyCommandMiddleware[] | undefined;
96
+ guard: ((context: Context, command: never) => MaybePromise<void>) | undefined;
97
+ audit: (resolution: {
98
+ command: z.output<z.ZodObject<{
99
+ id: import("convex-helpers/server/zod4").Zid<string>;
100
+ data: z.ZodObject<{
101
+ [x: string]: z.ZodOptional<z.ZodType<Value | undefined, unknown, z.core.$ZodTypeInternals<Value | undefined, unknown>>>;
102
+ }, z.core.$strict>;
103
+ }, z.core.$strict>>;
104
+ }, context: Context) => {
105
+ operation: `${string}.update`;
106
+ actorId: string;
107
+ aggregate: {
108
+ type: string;
109
+ id: string & {
110
+ __tableName: string;
111
+ };
112
+ };
113
+ };
114
+ }> & NamedOperation<`${string}.archive`, {
115
+ command: z.ZodObject<{
116
+ id: import("convex-helpers/server/zod4").Zid<string>;
117
+ }, z.core.$strict>;
118
+ result: z.ZodObject<{
119
+ ok: z.ZodLiteral<true>;
120
+ }, z.core.$strict>;
121
+ classification: string;
122
+ aggregates: string[];
123
+ middleware: readonly AnyCommandMiddleware[] | undefined;
124
+ guard: ((context: Context, command: never) => MaybePromise<void>) | undefined;
125
+ audit: (resolution: {
126
+ command: z.output<z.ZodObject<{
127
+ id: import("convex-helpers/server/zod4").Zid<string>;
128
+ }, z.core.$strict>>;
129
+ }, context: Context) => {
130
+ operation: `${string}.archive`;
131
+ actorId: string;
132
+ aggregate: {
133
+ type: string;
134
+ id: string & {
135
+ __tableName: string;
136
+ };
137
+ };
138
+ };
139
+ }>>;
140
+ operations: NamedOperation<`${string}.create`, {
141
+ command: Table["commandInput"];
142
+ result: z.ZodObject<{
143
+ id: import("convex-helpers/server/zod4").Zid<string>;
144
+ }, z.core.$strict>;
145
+ classification: string;
146
+ aggregates: string[];
147
+ middleware: readonly AnyCommandMiddleware[] | undefined;
148
+ guard: ((context: Context, command: never) => MaybePromise<void>) | undefined;
149
+ audit: (resolution: {
150
+ result: z.output<z.ZodObject<{
151
+ id: import("convex-helpers/server/zod4").Zid<string>;
152
+ }, z.core.$strict>>;
153
+ }, context: Context) => {
154
+ operation: `${string}.create`;
155
+ actorId: string;
156
+ aggregate: {
157
+ type: string;
158
+ id: string & {
159
+ __tableName: string;
160
+ };
161
+ };
162
+ };
163
+ }> & NamedOperation<`${string}.update`, {
164
+ command: z.ZodObject<{
165
+ id: import("convex-helpers/server/zod4").Zid<string>;
166
+ data: z.ZodObject<{
167
+ [x: string]: z.ZodOptional<z.ZodType<Value | undefined, unknown, z.core.$ZodTypeInternals<Value | undefined, unknown>>>;
168
+ }, z.core.$strict>;
169
+ }, z.core.$strict>;
170
+ result: z.ZodObject<{
171
+ ok: z.ZodLiteral<true>;
172
+ }, z.core.$strict>;
173
+ classification: string;
174
+ aggregates: string[];
175
+ middleware: readonly AnyCommandMiddleware[] | undefined;
176
+ guard: ((context: Context, command: never) => MaybePromise<void>) | undefined;
177
+ audit: (resolution: {
178
+ command: z.output<z.ZodObject<{
179
+ id: import("convex-helpers/server/zod4").Zid<string>;
180
+ data: z.ZodObject<{
181
+ [x: string]: z.ZodOptional<z.ZodType<Value | undefined, unknown, z.core.$ZodTypeInternals<Value | undefined, unknown>>>;
182
+ }, z.core.$strict>;
183
+ }, z.core.$strict>>;
184
+ }, context: Context) => {
185
+ operation: `${string}.update`;
186
+ actorId: string;
187
+ aggregate: {
188
+ type: string;
189
+ id: string & {
190
+ __tableName: string;
191
+ };
192
+ };
193
+ };
194
+ }> & NamedOperation<`${string}.archive`, {
195
+ command: z.ZodObject<{
196
+ id: import("convex-helpers/server/zod4").Zid<string>;
197
+ }, z.core.$strict>;
198
+ result: z.ZodObject<{
199
+ ok: z.ZodLiteral<true>;
200
+ }, z.core.$strict>;
201
+ classification: string;
202
+ aggregates: string[];
203
+ middleware: readonly AnyCommandMiddleware[] | undefined;
204
+ guard: ((context: Context, command: never) => MaybePromise<void>) | undefined;
205
+ audit: (resolution: {
206
+ command: z.output<z.ZodObject<{
207
+ id: import("convex-helpers/server/zod4").Zid<string>;
208
+ }, z.core.$strict>>;
209
+ }, context: Context) => {
210
+ operation: `${string}.archive`;
211
+ actorId: string;
212
+ aggregate: {
213
+ type: string;
214
+ id: string & {
215
+ __tableName: string;
216
+ };
217
+ };
218
+ };
219
+ }>;
220
+ executeCreate: (context: Context, command: SchemaInput<Table["commandInput"]>) => Promise<{
221
+ id: string & {
222
+ __tableName: string;
223
+ };
63
224
  }>;
64
225
  executeUpdate: (context: Context, command: {
65
226
  id: string;
@@ -73,7 +234,7 @@ type CrudCommands<Context extends CrudContext> = {
73
234
  ok: true;
74
235
  }>;
75
236
  };
76
- declare function createCrudCommands<Context extends CrudContext>(config: CrudConfig<Context>): CrudCommands<Context>;
237
+ type NamedOperation<Name extends string, Definition extends AuditedOperation> = { [Key in Name]: Definition; };
77
238
  //#endregion
78
239
  export { CrudCommands, CrudConfig, createCrudCommands };
79
240
  //# sourceMappingURL=crud.d.mts.map
package/dist/crud.mjs CHANGED
@@ -1,2 +1,2 @@
1
- import{defaultErrors as e}from"./errors.mjs";import{zid as t}from"convex-helpers/server/zod4";import{z as n}from"zod";function r(r){let i=r.errors??e,a=r.table.tableName,o=r.classification??`business`;if(`tenant`in r.table.storage.shape&&!r.enrich)return i.throw({code:`CRUD_ENRICH_REQUIRED`,message:`Table "${a}" is a tenantTable: createCrudCommands requires an enrich callback that stamps ctx.tenant`});let s=t(a),c=n.object({id:s}).strict(),l=n.object({ok:n.literal(!0)}).strict(),u=n.object({id:s,data:r.table.commandInput.partial().strict()}).strict(),d=n.object({id:s}).strict(),f={[`${a}.create`]:{command:r.table.commandInput,result:c,classification:o,aggregates:[r.aggregateType],...r.middleware?{middleware:r.middleware}:{},...r.guards?.create?{guard:r.guards.create}:{},audit:(e,t)=>({operation:`${a}.create`,actorId:r.actor(t),aggregate:{type:r.aggregateType,id:e.result.id}})},[`${a}.update`]:{command:u,result:l,classification:o,aggregates:[r.aggregateType],...r.middleware?{middleware:r.middleware}:{},...r.guards?.update?{guard:r.guards.update}:{},audit:(e,t)=>({operation:`${a}.update`,actorId:r.actor(t),aggregate:{type:r.aggregateType,id:e.command.id}})},[`${a}.archive`]:{command:d,result:l,classification:o,aggregates:[r.aggregateType],...r.middleware?{middleware:r.middleware}:{},...r.guards?.archive?{guard:r.guards.archive}:{},audit:(e,t)=>({operation:`${a}.archive`,actorId:r.actor(t),aggregate:{type:r.aggregateType,id:e.command.id}})}},p=new r.Command(f);return{commands:p,operations:f,executeCreate:p.exec({operation:`${a}.create`,handler:(async(e,t)=>{let n=r.enrich?await r.enrich(e,t):{};return{id:await e.db.insert(a,{...t,...n})}})}),executeUpdate:p.exec({operation:`${a}.update`,handler:(async(e,t)=>(await e.db.patch(t.id,t.data),{ok:!0}))}),executeArchive:p.exec({operation:`${a}.archive`,handler:(async(e,t)=>(await e.db.patch(t.id,{archivedAt:Date.now()}),{ok:!0}))})}}export{r as createCrudCommands};
1
+ import{defaultErrors as e}from"./errors.mjs";import{zid as t}from"convex-helpers/server/zod4";import{z as n}from"zod";function r(r){let a=r.errors??e,o=r.table.tableName,s=r.classification??`business`;if(`tenant`in r.table.storage.shape&&!r.enrich)return a.throw({code:`CRUD_ENRICH_REQUIRED`,message:`Table "${o}" is a tenantTable: createCrudCommands requires an enrich callback that stamps ctx.tenant`});let c=r.table.commandInput,l=t(o),u=n.object({id:l}).strict(),d=n.object({ok:n.literal(!0)}).strict(),f=n.object({id:l,data:c.partial().strict()}).strict(),p=n.object({id:l}).strict(),m=`${o}.create`,h=`${o}.update`,g=`${o}.archive`,_=Object.assign({},i(m,{command:c,result:u,classification:s,aggregates:[r.aggregateType],middleware:r.middleware,guard:r.guards?.create,audit:(e,t)=>({operation:m,actorId:r.actor(t),aggregate:{type:r.aggregateType,id:e.result.id}})}),i(h,{command:f,result:d,classification:s,aggregates:[r.aggregateType],middleware:r.middleware,guard:r.guards?.update,audit:(e,t)=>({operation:h,actorId:r.actor(t),aggregate:{type:r.aggregateType,id:e.command.id}})}),i(g,{command:p,result:d,classification:s,aggregates:[r.aggregateType],middleware:r.middleware,guard:r.guards?.archive,audit:(e,t)=>({operation:g,actorId:r.actor(t),aggregate:{type:r.aggregateType,id:e.command.id}})})),v=new r.Command(_);return{commands:v,operations:_,executeCreate:v.exec({operation:m,handler:async(e,t)=>{let n=r.enrich?await r.enrich(e,t):{},i={};for(let[e,r]of Object.entries({...t,...n}))r!==void 0&&(i[e]=r);return{id:await e.db.insert(o,i)}}}),executeUpdate:v.exec({operation:h,handler:async(e,t)=>(await e.db.patch(t.id,t.data),{ok:!0})}),executeArchive:v.exec({operation:g,handler:async(e,t)=>(await e.db.patch(t.id,{archivedAt:Date.now()}),{ok:!0})})}}function i(e,t){return{[e]:t}}export{r as createCrudCommands};
2
2
  //# sourceMappingURL=crud.mjs.map
package/dist/crud.mjs.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"crud.mjs","names":[],"sources":["../src/crud.ts"],"sourcesContent":["import { zid } from 'convex-helpers/server/zod4'\nimport { z } from 'zod'\nimport type {\n\tAnyCommandMiddleware,\n\tApplicationCommand,\n\tAuditedRegistry,\n\tCommandConstructor,\n} from './components/foundation/client'\nimport { defaultErrors, type ErrorFactory } from './errors'\n\n/**\n * The zodTable surface the factory consumes — structural, so any zodTable /\n * tenantTable return satisfies it.\n */\ntype CrudTable = {\n\ttableName: string\n\t// biome-ignore format: structural zod surface\n\tcommandInput: z.ZodObject<Record<string, z.ZodType>>\n\tstorage: z.ZodObject<Record<string, z.ZodType>>\n}\n\ntype CrudContext = {\n\tdb: {\n\t\tinsert: (table: never, value: never) => Promise<unknown>\n\t\tpatch: (id: never, value: never) => Promise<unknown>\n\t}\n}\n\ntype MaybePromise<Value> = Value | Promise<Value>\n\nexport type CrudConfig<Context extends CrudContext> = {\n\t/** The Foundation-bound Command class (destructured from the facade). */\n\tCommand: CommandConstructor\n\ttable: CrudTable\n\t/** Audit aggregate type; enforced by the aggregates allowlist. */\n\taggregateType: string\n\t/** Observability/audit classification. Default 'business'. */\n\tclassification?: string\n\t/** Derives the audited actor id from the ctx (usually ctx.actor.userId). */\n\tactor: (context: Context) => string\n\t/**\n\t * Stamps server-owned fields onto creates (tenant, denormalized ids...).\n\t * REQUIRED when the table is a tenantTable — forgetting it would insert\n\t * untenanted rows; the factory fails fast at construction instead.\n\t */\n\tenrich?: (\n\t\tcontext: Context,\n\t\tcommand: Record<string, unknown>,\n\t) => Record<string, unknown> | Promise<Record<string, unknown>>\n\t/** Per-operation preconditions; run before the handlers. */\n\tguards?: {\n\t\tcreate?: (context: Context, command: never) => MaybePromise<void>\n\t\tupdate?: (context: Context, command: never) => MaybePromise<void>\n\t\tarchive?: (context: Context, command: never) => MaybePromise<void>\n\t}\n\t/** Per-operation middleware applied to all three generated operations. */\n\tmiddleware?: readonly AnyCommandMiddleware[]\n\terrors?: ErrorFactory\n}\n\n/**\n * Generates the standard create/update/archive operations from one zodTable\n * declaration, fully inside the audited command pipeline: strict command\n * inputs from `commandFields`, mandatory audit with the declared aggregate\n * type, soft delete only (archive sets `archivedAt` — no hard-delete\n * operation is generated, per kit doctrine).\n *\n * convex-helpers ships a same-named `crud` helper; it was deliberately not\n * used — it bypasses the command pipeline (no audits, no guards, and hard\n * deletes). See docs/crud.md.\n */\nexport type CrudCommands<Context extends CrudContext> = {\n\tcommands: ApplicationCommand<Context, AuditedRegistry>\n\toperations: AuditedRegistry\n\texecuteCreate: (\n\t\tcontext: Context,\n\t\tcommand: Record<string, unknown>,\n\t) => Promise<{ id: string }>\n\texecuteUpdate: (\n\t\tcontext: Context,\n\t\tcommand: { id: string; data: Record<string, unknown> },\n\t) => Promise<{ ok: true }>\n\texecuteArchive: (\n\t\tcontext: Context,\n\t\tcommand: { id: string },\n\t) => Promise<{ ok: true }>\n}\n\nexport function createCrudCommands<Context extends CrudContext>(\n\tconfig: CrudConfig<Context>,\n): CrudCommands<Context> {\n\tconst errors = config.errors ?? defaultErrors\n\tconst name = config.table.tableName\n\tconst classification = config.classification ?? 'business'\n\tconst isTenantTable = 'tenant' in config.table.storage.shape\n\tif (isTenantTable && !config.enrich) {\n\t\treturn errors.throw({\n\t\t\tcode: 'CRUD_ENRICH_REQUIRED',\n\t\t\tmessage: `Table \"${name}\" is a tenantTable: createCrudCommands requires an enrich callback that stamps ctx.tenant`,\n\t\t})\n\t}\n\n\tconst id = zid(name)\n\tconst createResult = z.object({ id }).strict()\n\tconst okResult = z.object({ ok: z.literal(true) }).strict()\n\tconst updateInput = z\n\t\t.object({ id, data: config.table.commandInput.partial().strict() })\n\t\t.strict()\n\tconst archiveInput = z.object({ id }).strict()\n\n\tconst operations = {\n\t\t[`${name}.create`]: {\n\t\t\tcommand: config.table.commandInput,\n\t\t\tresult: createResult,\n\t\t\tclassification,\n\t\t\taggregates: [config.aggregateType],\n\t\t\t...(config.middleware ? { middleware: config.middleware } : {}),\n\t\t\t...(config.guards?.create ? { guard: config.guards.create } : {}),\n\t\t\taudit: (\n\t\t\t\tresolution: { command: unknown; result: { id: string } },\n\t\t\t\tcontext: Context,\n\t\t\t) => ({\n\t\t\t\toperation: `${name}.create`,\n\t\t\t\tactorId: config.actor(context),\n\t\t\t\taggregate: { type: config.aggregateType, id: resolution.result.id },\n\t\t\t}),\n\t\t},\n\t\t[`${name}.update`]: {\n\t\t\tcommand: updateInput,\n\t\t\tresult: okResult,\n\t\t\tclassification,\n\t\t\taggregates: [config.aggregateType],\n\t\t\t...(config.middleware ? { middleware: config.middleware } : {}),\n\t\t\t...(config.guards?.update ? { guard: config.guards.update } : {}),\n\t\t\taudit: (\n\t\t\t\tresolution: { command: { id: string }; result: unknown },\n\t\t\t\tcontext: Context,\n\t\t\t) => ({\n\t\t\t\toperation: `${name}.update`,\n\t\t\t\tactorId: config.actor(context),\n\t\t\t\taggregate: { type: config.aggregateType, id: resolution.command.id },\n\t\t\t}),\n\t\t},\n\t\t[`${name}.archive`]: {\n\t\t\tcommand: archiveInput,\n\t\t\tresult: okResult,\n\t\t\tclassification,\n\t\t\taggregates: [config.aggregateType],\n\t\t\t...(config.middleware ? { middleware: config.middleware } : {}),\n\t\t\t...(config.guards?.archive ? { guard: config.guards.archive } : {}),\n\t\t\taudit: (\n\t\t\t\tresolution: { command: { id: string }; result: unknown },\n\t\t\t\tcontext: Context,\n\t\t\t) => ({\n\t\t\t\toperation: `${name}.archive`,\n\t\t\t\tactorId: config.actor(context),\n\t\t\t\taggregate: { type: config.aggregateType, id: resolution.command.id },\n\t\t\t}),\n\t\t},\n\t} as never\n\n\tconst commands = new config.Command<Context, AuditedRegistry>(operations)\n\n\tconst executeCreate = commands.exec({\n\t\toperation: `${name}.create` as never,\n\t\thandler: (async (context: Context, command: Record<string, unknown>) => {\n\t\t\tconst enrichment = config.enrich\n\t\t\t\t? await config.enrich(context, command)\n\t\t\t\t: {}\n\t\t\tconst inserted = await context.db.insert(\n\t\t\t\tname as never,\n\t\t\t\t{ ...command, ...enrichment } as never,\n\t\t\t)\n\t\t\treturn { id: inserted }\n\t\t}) as never,\n\t})\n\n\tconst executeUpdate = commands.exec({\n\t\toperation: `${name}.update` as never,\n\t\thandler: (async (\n\t\t\tcontext: Context,\n\t\t\tcommand: { id: string; data: Record<string, unknown> },\n\t\t) => {\n\t\t\tawait context.db.patch(command.id as never, command.data as never)\n\t\t\treturn { ok: true }\n\t\t}) as never,\n\t})\n\n\tconst executeArchive = commands.exec({\n\t\toperation: `${name}.archive` as never,\n\t\thandler: (async (context: Context, command: { id: string }) => {\n\t\t\tawait context.db.patch(\n\t\t\t\tcommand.id as never,\n\t\t\t\t{ archivedAt: Date.now() } as never,\n\t\t\t)\n\t\t\treturn { ok: true }\n\t\t}) as never,\n\t})\n\n\treturn {\n\t\tcommands,\n\t\toperations: operations as AuditedRegistry,\n\t\texecuteCreate,\n\t\texecuteUpdate,\n\t\texecuteArchive,\n\t} as unknown as CrudCommands<Context>\n}\n"],"mappings":"sHAwFA,SAAgB,EACf,EACwB,CACxB,IAAM,EAAS,EAAO,QAAU,EAC1B,EAAO,EAAO,MAAM,UACpB,EAAiB,EAAO,gBAAkB,WAEhD,GADsB,WAAY,EAAO,MAAM,QAAQ,OAClC,CAAC,EAAO,OAC5B,OAAO,EAAO,MAAM,CACnB,KAAM,uBACN,QAAS,UAAU,EAAK,0FACzB,CAAC,EAGF,IAAM,EAAK,EAAI,CAAI,EACb,EAAe,EAAE,OAAO,CAAE,IAAG,CAAC,CAAC,CAAC,OAAO,EACvC,EAAW,EAAE,OAAO,CAAE,GAAI,EAAE,QAAQ,EAAI,CAAE,CAAC,CAAC,CAAC,OAAO,EACpD,EAAc,EAClB,OAAO,CAAE,KAAI,KAAM,EAAO,MAAM,aAAa,QAAQ,CAAC,CAAC,OAAO,CAAE,CAAC,CAAC,CAClE,OAAO,EACH,EAAe,EAAE,OAAO,CAAE,IAAG,CAAC,CAAC,CAAC,OAAO,EAEvC,EAAa,EACjB,GAAG,EAAK,UAAW,CACnB,QAAS,EAAO,MAAM,aACtB,OAAQ,EACR,iBACA,WAAY,CAAC,EAAO,aAAa,EACjC,GAAI,EAAO,WAAa,CAAE,WAAY,EAAO,UAAW,EAAI,CAAC,EAC7D,GAAI,EAAO,QAAQ,OAAS,CAAE,MAAO,EAAO,OAAO,MAAO,EAAI,CAAC,EAC/D,OACC,EACA,KACK,CACL,UAAW,GAAG,EAAK,SACnB,QAAS,EAAO,MAAM,CAAO,EAC7B,UAAW,CAAE,KAAM,EAAO,cAAe,GAAI,EAAW,OAAO,EAAG,CACnE,EACD,GACC,GAAG,EAAK,UAAW,CACnB,QAAS,EACT,OAAQ,EACR,iBACA,WAAY,CAAC,EAAO,aAAa,EACjC,GAAI,EAAO,WAAa,CAAE,WAAY,EAAO,UAAW,EAAI,CAAC,EAC7D,GAAI,EAAO,QAAQ,OAAS,CAAE,MAAO,EAAO,OAAO,MAAO,EAAI,CAAC,EAC/D,OACC,EACA,KACK,CACL,UAAW,GAAG,EAAK,SACnB,QAAS,EAAO,MAAM,CAAO,EAC7B,UAAW,CAAE,KAAM,EAAO,cAAe,GAAI,EAAW,QAAQ,EAAG,CACpE,EACD,GACC,GAAG,EAAK,WAAY,CACpB,QAAS,EACT,OAAQ,EACR,iBACA,WAAY,CAAC,EAAO,aAAa,EACjC,GAAI,EAAO,WAAa,CAAE,WAAY,EAAO,UAAW,EAAI,CAAC,EAC7D,GAAI,EAAO,QAAQ,QAAU,CAAE,MAAO,EAAO,OAAO,OAAQ,EAAI,CAAC,EACjE,OACC,EACA,KACK,CACL,UAAW,GAAG,EAAK,UACnB,QAAS,EAAO,MAAM,CAAO,EAC7B,UAAW,CAAE,KAAM,EAAO,cAAe,GAAI,EAAW,QAAQ,EAAG,CACpE,EACD,CACD,EAEM,EAAW,IAAI,EAAO,QAAkC,CAAU,EAsCxE,MAAO,CACN,WACY,aACZ,cAvCqB,EAAS,KAAK,CACnC,UAAW,GAAG,EAAK,SACnB,SAAU,MAAO,EAAkB,IAAqC,CACvE,IAAM,EAAa,EAAO,OACvB,MAAM,EAAO,OAAO,EAAS,CAAO,EACpC,CAAC,EAKJ,MAAO,CAAE,GAAI,MAJU,EAAQ,GAAG,OACjC,EACA,CAAE,GAAG,EAAS,GAAG,CAAW,CAC7B,CACsB,CACvB,EACD,CA2Ba,EACZ,cA1BqB,EAAS,KAAK,CACnC,UAAW,GAAG,EAAK,SACnB,SAAU,MACT,EACA,KAEA,MAAM,EAAQ,GAAG,MAAM,EAAQ,GAAa,EAAQ,IAAa,EAC1D,CAAE,GAAI,EAAK,GAEpB,CAiBa,EACZ,eAhBsB,EAAS,KAAK,CACpC,UAAW,GAAG,EAAK,UACnB,SAAU,MAAO,EAAkB,KAClC,MAAM,EAAQ,GAAG,MAChB,EAAQ,GACR,CAAE,WAAY,KAAK,IAAI,CAAE,CAC1B,EACO,CAAE,GAAI,EAAK,GAEpB,CAOc,CACd,CACD"}
1
+ {"version":3,"file":"crud.mjs","names":[],"sources":["../src/crud.ts"],"sourcesContent":["import { zid } from 'convex-helpers/server/zod4'\nimport { z } from 'zod'\nimport type { Value } from 'convex/values'\nimport type { GenericDataModel, GenericMutationCtx } from 'convex/server'\nimport type {\n\tAnyCommandMiddleware,\n\tApplicationCommand,\n\tAuditedOperation,\n\tCommandConstructor,\n} from './components/foundation/client'\nimport { defaultErrors, type ErrorFactory } from './errors'\n\n/**\n * The zodTable surface the factory consumes — structural, so any zodTable /\n * tenantTable return satisfies it.\n */\ntype CrudTable = {\n\ttableName: string\n\tcommandInput: z.ZodObject<Record<string, z.ZodType<Value | undefined>>>\n\tstorage: z.ZodObject<Record<string, z.ZodType<Value | undefined>>>\n}\n\ntype CrudContext = { db: Pick<GenericMutationCtx<GenericDataModel>['db'], 'insert' | 'patch'> }\n\ntype MaybePromise<Value> = Value | Promise<Value>\n\nexport type CrudConfig<Context extends CrudContext, Table extends CrudTable = CrudTable> = {\n\t/** The Foundation-bound Command class (destructured from the facade). */\n\tCommand: CommandConstructor\n\ttable: Table\n\t/** Audit aggregate type; enforced by the aggregates allowlist. */\n\taggregateType: string\n\t/** Observability/audit classification. Default 'business'. */\n\tclassification?: string\n\t/** Derives the audited actor id from the ctx (usually ctx.actor.userId). */\n\tactor: (context: Context) => string\n\t/**\n\t * Stamps server-owned fields onto creates (tenant, denormalized ids...).\n\t * REQUIRED when the table is a tenantTable — forgetting it would insert\n\t * untenanted rows; the factory fails fast at construction instead.\n\t */\n\tenrich?: (\n\t\tcontext: Context,\n\t\tcommand: ReturnType<Table['commandInput']['parse']>,\n\t) => MaybePromise<Partial<z.input<Table['storage']>>>\n\t/** Per-operation preconditions; run before the handlers. */\n\tguards?: {\n\t\tcreate?: (context: Context, command: never) => MaybePromise<void>\n\t\tupdate?: (context: Context, command: never) => MaybePromise<void>\n\t\tarchive?: (context: Context, command: never) => MaybePromise<void>\n\t}\n\t/** Per-operation middleware applied to all three generated operations. */\n\tmiddleware?: readonly AnyCommandMiddleware[]\n\terrors?: ErrorFactory\n}\n\n/**\n * Generates the standard create/update/archive operations from one zodTable\n * declaration, fully inside the audited command pipeline: strict command\n * inputs from `commandFields`, mandatory audit with the declared aggregate\n * type, soft delete only (archive sets `archivedAt` — no hard-delete\n * operation is generated, per kit doctrine).\n *\n * convex-helpers ships a same-named `crud` helper; it was deliberately not\n * used — it bypasses the command pipeline (no audits, no guards, and hard\n * deletes). See docs/crud.md.\n */\nexport type CrudCommands<\n\tContext extends CrudContext,\n\tTable extends CrudTable = CrudTable,\n> = ReturnType<typeof createCrudCommands<Context, Table>>\n\nexport function createCrudCommands<\n\tContext extends CrudContext,\n\tTable extends CrudTable = CrudTable,\n>(config: CrudConfig<Context, Table>) {\n\tconst errors = config.errors ?? defaultErrors\n\tconst name = config.table.tableName\n\tconst classification = config.classification ?? 'business'\n\tconst isTenantTable = 'tenant' in config.table.storage.shape\n\tif (isTenantTable && !config.enrich) {\n\t\treturn errors.throw({\n\t\t\tcode: 'CRUD_ENRICH_REQUIRED',\n\t\t\tmessage: `Table \"${name}\" is a tenantTable: createCrudCommands requires an enrich callback that stamps ctx.tenant`,\n\t\t})\n\t}\n\n\tconst commandSchema: Table['commandInput'] = config.table.commandInput\n\tconst id = zid(name)\n\tconst createResult = z.object({ id }).strict()\n\tconst okResult = z.object({ ok: z.literal(true) }).strict()\n\tconst updateInput = z.object({ id, data: commandSchema.partial().strict() }).strict()\n\tconst archiveInput = z.object({ id }).strict()\n\n\tconst createName = `${name}.create` as const\n\tconst updateName = `${name}.update` as const\n\tconst archiveName = `${name}.archive` as const\n\tconst operations = Object.assign(\n\t\t{},\n\t\tnamedOperation(createName, {\n\t\t\tcommand: commandSchema,\n\t\t\tresult: createResult,\n\t\t\tclassification,\n\t\t\taggregates: [config.aggregateType],\n\t\t\tmiddleware: config.middleware,\n\t\t\tguard: config.guards?.create,\n\t\t\taudit: (resolution: { result: z.output<typeof createResult> }, context: Context) => ({\n\t\t\t\toperation: createName,\n\t\t\t\tactorId: config.actor(context),\n\t\t\t\taggregate: { type: config.aggregateType, id: resolution.result.id },\n\t\t\t}),\n\t\t}),\n\t\tnamedOperation(updateName, {\n\t\t\tcommand: updateInput,\n\t\t\tresult: okResult,\n\t\t\tclassification,\n\t\t\taggregates: [config.aggregateType],\n\t\t\tmiddleware: config.middleware,\n\t\t\tguard: config.guards?.update,\n\t\t\taudit: (resolution: { command: z.output<typeof updateInput> }, context: Context) => ({\n\t\t\t\toperation: updateName,\n\t\t\t\tactorId: config.actor(context),\n\t\t\t\taggregate: { type: config.aggregateType, id: resolution.command.id },\n\t\t\t}),\n\t\t}),\n\t\tnamedOperation(archiveName, {\n\t\t\tcommand: archiveInput,\n\t\t\tresult: okResult,\n\t\t\tclassification,\n\t\t\taggregates: [config.aggregateType],\n\t\t\tmiddleware: config.middleware,\n\t\t\tguard: config.guards?.archive,\n\t\t\taudit: (resolution: { command: z.output<typeof archiveInput> }, context: Context) => ({\n\t\t\t\toperation: archiveName,\n\t\t\t\tactorId: config.actor(context),\n\t\t\t\taggregate: { type: config.aggregateType, id: resolution.command.id },\n\t\t\t}),\n\t\t}),\n\t)\n\tconst commands: ApplicationCommand<Context, typeof operations> = new config.Command<\n\t\tContext,\n\t\ttypeof operations\n\t>(operations)\n\tconst executeCreate = commands.exec({\n\t\toperation: createName,\n\t\thandler: async (context, command) => {\n\t\t\tconst enrichment = config.enrich ? await config.enrich(context, command) : {}\n\t\t\tconst values: Record<string, Value> = {}\n\t\t\tfor (const [field, value] of Object.entries({ ...command, ...enrichment })) {\n\t\t\t\tif (value !== undefined) values[field] = value\n\t\t\t}\n\t\t\tconst inserted = await context.db.insert(name, values)\n\t\t\treturn { id: inserted }\n\t\t},\n\t})\n\tconst executeUpdate = commands.exec({\n\t\toperation: updateName,\n\t\thandler: async (context, command) => {\n\t\t\tawait context.db.patch(command.id, command.data)\n\t\t\treturn { ok: true }\n\t\t},\n\t})\n\tconst executeArchive = commands.exec({\n\t\toperation: archiveName,\n\t\thandler: async (context, command) => {\n\t\t\tawait context.db.patch(command.id, { archivedAt: Date.now() })\n\t\t\treturn { ok: true }\n\t\t},\n\t})\n\n\treturn {\n\t\tcommands,\n\t\toperations,\n\t\texecuteCreate,\n\t\texecuteUpdate,\n\t\texecuteArchive,\n\t}\n}\n\nfunction namedOperation<Name extends string, Definition extends AuditedOperation>(\n\tname: Name,\n\tdefinition: Definition,\n) {\n\t// SAFETY: this object contains exactly the computed name and its corresponding definition.\n\treturn { [name]: definition } as NamedOperation<Name, Definition>\n}\n\ntype NamedOperation<Name extends string, Definition extends AuditedOperation> = {\n\t[Key in Name]: Definition\n}\n"],"mappings":"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"}
@@ -24,23 +24,23 @@ type RateLimiterLike = {
24
24
  * The returned middleware is shape-compatible with both AnyCommandMiddleware
25
25
  * and AnyQueryMiddleware.
26
26
  */
27
- declare function rateLimit(config: {
27
+ declare function rateLimit<Context = never>(config: {
28
28
  limiter: RateLimiterLike;
29
29
  name: string;
30
30
  /** Defaults to ctx.tenant. */
31
- key?: (context: never) => string;
31
+ key?: (context: Context) => string;
32
32
  /** Observe a rejection before the throw (metrics, headers). */
33
33
  onLimit?: (status: {
34
34
  ok: boolean;
35
35
  retryAfter?: number;
36
- }, context: never) => void | Promise<void>;
36
+ }, context: Context) => void | Promise<void>;
37
37
  errors?: ErrorFactory;
38
- }): (input: {
38
+ }): <Result>(input: {
39
39
  context: never;
40
40
  next: (options?: {
41
- context?: unknown;
42
- }) => Promise<unknown>;
43
- }) => Promise<unknown>;
41
+ context?: object;
42
+ }) => Promise<Result>;
43
+ }) => Promise<Result>;
44
44
  //#endregion
45
45
  export { RateLimiterLike, rateLimit };
46
46
  //# sourceMappingURL=middleware.d.mts.map
@@ -1,2 +1,2 @@
1
- import{defaultErrors as e}from"./errors.mjs";function t(t){let n=t.errors??e;return async e=>{let r=t.key?t.key(e.context):e.context.tenant;if(r===void 0)return n.throw({code:`RATE_LIMIT_KEY_MISSING`,message:`rateLimit("${t.name}") has no key: ctx.tenant is undefined and no key fn was configured`});let i=await t.limiter.limit(e.context,t.name,{key:r});return i.ok?e.next():(await t.onLimit?.(i,e.context),n.throw({code:`RATE_LIMITED`,message:`Rate limit "${t.name}" exceeded${i.retryAfter===void 0?``:`; retry after ${Math.ceil(i.retryAfter)}ms`}`}))}}export{t as rateLimit};
1
+ import{defaultErrors as e}from"./errors.mjs";import{z as t}from"zod";function n(t){let n=t.errors??e;return async e=>{let i=t.key?t.key(e.context):r.parse(e.context).tenant;if(i===void 0)return n.throw({code:`RATE_LIMIT_KEY_MISSING`,message:`rateLimit("${t.name}") has no key: ctx.tenant is undefined and no key fn was configured`});let a=await t.limiter.limit(e.context,t.name,{key:i});return a.ok?e.next():(await t.onLimit?.(a,e.context),n.throw({code:`RATE_LIMITED`,message:`Rate limit "${t.name}" exceeded${a.retryAfter===void 0?``:`; retry after ${Math.ceil(a.retryAfter)}ms`}`}))}}const r=t.object({tenant:t.string().optional()});export{n as rateLimit};
2
2
  //# sourceMappingURL=middleware.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"middleware.mjs","names":[],"sources":["../src/middleware.ts"],"sourcesContent":["import { defaultErrors, type ErrorFactory } from './errors'\n\n/**\n * Structural surface of @convex-dev/rate-limiter's RateLimiter (checked\n * against 0.3.x; re-verify at install). The host mounts the component and\n * constructs the limiter; the kit only ever receives the instance —\n * injection keeps the kit dependency-free.\n */\nexport type RateLimiterLike = {\n\tlimit: (\n\t\tctx: never,\n\t\tname: string,\n\t\toptions?: { key?: string },\n\t) => Promise<{ ok: boolean; retryAfter?: number }>\n}\n\n/**\n * The first packaged middleware: rate limiting on the Command/Query\n * middleware seams. Runs before guards and the handler; a rejected call\n * throws RATE_LIMITED (audit and handler never run). Keyed by ctx.tenant by\n * default — when no key fn is given and ctx.tenant is undefined, this is a\n * configuration error, not a silent shared bucket.\n *\n * The returned middleware is shape-compatible with both AnyCommandMiddleware\n * and AnyQueryMiddleware.\n */\nexport function rateLimit(config: {\n\tlimiter: RateLimiterLike\n\tname: string\n\t/** Defaults to ctx.tenant. */\n\tkey?: (context: never) => string\n\t/** Observe a rejection before the throw (metrics, headers). */\n\tonLimit?: (\n\t\tstatus: { ok: boolean; retryAfter?: number },\n\t\tcontext: never,\n\t) => void | Promise<void>\n\terrors?: ErrorFactory\n}) {\n\tconst errors = config.errors ?? defaultErrors\n\treturn async (input: {\n\t\tcontext: never\n\t\tnext: (options?: { context?: unknown }) => Promise<unknown>\n\t}): Promise<unknown> => {\n\t\tconst key = config.key\n\t\t\t? config.key(input.context)\n\t\t\t: (input.context as { tenant?: string }).tenant\n\t\tif (key === undefined) {\n\t\t\treturn errors.throw({\n\t\t\t\tcode: 'RATE_LIMIT_KEY_MISSING',\n\t\t\t\tmessage: `rateLimit(\"${config.name}\") has no key: ctx.tenant is undefined and no key fn was configured`,\n\t\t\t})\n\t\t}\n\t\tconst status = await config.limiter.limit(input.context, config.name, {\n\t\t\tkey,\n\t\t})\n\t\tif (!status.ok) {\n\t\t\tawait config.onLimit?.(status, input.context)\n\t\t\treturn errors.throw({\n\t\t\t\tcode: 'RATE_LIMITED',\n\t\t\t\tmessage: `Rate limit \"${config.name}\" exceeded${\n\t\t\t\t\tstatus.retryAfter === undefined\n\t\t\t\t\t\t? ''\n\t\t\t\t\t\t: `; retry after ${Math.ceil(status.retryAfter)}ms`\n\t\t\t\t}`,\n\t\t\t})\n\t\t}\n\t\treturn input.next()\n\t}\n}\n"],"mappings":"6CA0BA,SAAgB,EAAU,EAWvB,CACF,IAAM,EAAS,EAAO,QAAU,EAChC,OAAO,KAAO,IAGU,CACvB,IAAM,EAAM,EAAO,IAChB,EAAO,IAAI,EAAM,OAAO,EACvB,EAAM,QAAgC,OAC1C,GAAI,IAAQ,IAAA,GACX,OAAO,EAAO,MAAM,CACnB,KAAM,yBACN,QAAS,cAAc,EAAO,KAAK,oEACpC,CAAC,EAEF,IAAM,EAAS,MAAM,EAAO,QAAQ,MAAM,EAAM,QAAS,EAAO,KAAM,CACrE,KACD,CAAC,EAYD,OAXK,EAAO,GAWL,EAAM,KAAK,GAVjB,MAAM,EAAO,UAAU,EAAQ,EAAM,OAAO,EACrC,EAAO,MAAM,CACnB,KAAM,eACN,QAAS,eAAe,EAAO,KAAK,YACnC,EAAO,aAAe,IAAA,GACnB,GACA,iBAAiB,KAAK,KAAK,EAAO,UAAU,EAAE,KAEnD,CAAC,EAGH,CACD"}
1
+ {"version":3,"file":"middleware.mjs","names":[],"sources":["../src/middleware.ts"],"sourcesContent":["import { z } from 'zod'\nimport { defaultErrors, type ErrorFactory } from './errors'\n\n/**\n * Structural surface of @convex-dev/rate-limiter's RateLimiter (checked\n * against 0.3.x; re-verify at install). The host mounts the component and\n * constructs the limiter; the kit only ever receives the instance —\n * injection keeps the kit dependency-free.\n */\nexport type RateLimiterLike = {\n\tlimit: (\n\t\tctx: never,\n\t\tname: string,\n\t\toptions?: { key?: string },\n\t) => Promise<{ ok: boolean; retryAfter?: number }>\n}\n\n/**\n * The first packaged middleware: rate limiting on the Command/Query\n * middleware seams. Runs before guards and the handler; a rejected call\n * throws RATE_LIMITED (audit and handler never run). Keyed by ctx.tenant by\n * default — when no key fn is given and ctx.tenant is undefined, this is a\n * configuration error, not a silent shared bucket.\n *\n * The returned middleware is shape-compatible with both AnyCommandMiddleware\n * and AnyQueryMiddleware.\n */\nexport function rateLimit<Context = never>(config: {\n\tlimiter: RateLimiterLike\n\tname: string\n\t/** Defaults to ctx.tenant. */\n\tkey?: (context: Context) => string\n\t/** Observe a rejection before the throw (metrics, headers). */\n\tonLimit?: (status: { ok: boolean; retryAfter?: number }, context: Context) => void | Promise<void>\n\terrors?: ErrorFactory\n}) {\n\tconst errors = config.errors ?? defaultErrors\n\treturn async <Result>(input: {\n\t\tcontext: never\n\t\tnext: (options?: { context?: object }) => Promise<Result>\n\t}): Promise<Result> => {\n\t\tconst key = config.key ? config.key(input.context) : tenantContext.parse(input.context).tenant\n\t\tif (key === undefined) {\n\t\t\treturn errors.throw({\n\t\t\t\tcode: 'RATE_LIMIT_KEY_MISSING',\n\t\t\t\tmessage: `rateLimit(\"${config.name}\") has no key: ctx.tenant is undefined and no key fn was configured`,\n\t\t\t})\n\t\t}\n\t\tconst status = await config.limiter.limit(input.context, config.name, {\n\t\t\tkey,\n\t\t})\n\t\tif (!status.ok) {\n\t\t\tawait config.onLimit?.(status, input.context)\n\t\t\treturn errors.throw({\n\t\t\t\tcode: 'RATE_LIMITED',\n\t\t\t\tmessage: `Rate limit \"${config.name}\" exceeded${\n\t\t\t\t\tstatus.retryAfter === undefined ? '' : `; retry after ${Math.ceil(status.retryAfter)}ms`\n\t\t\t\t}`,\n\t\t\t})\n\t\t}\n\t\treturn input.next()\n\t}\n}\n\nconst tenantContext = z.object({ tenant: z.string().optional() })\n"],"mappings":"qEA2BA,SAAgB,EAA2B,EAQxC,CACF,IAAM,EAAS,EAAO,QAAU,EAChC,OAAO,KAAe,IAGC,CACtB,IAAM,EAAM,EAAO,IAAM,EAAO,IAAI,EAAM,OAAO,EAAI,EAAc,MAAM,EAAM,OAAO,CAAC,CAAC,OACxF,GAAI,IAAQ,IAAA,GACX,OAAO,EAAO,MAAM,CACnB,KAAM,yBACN,QAAS,cAAc,EAAO,KAAK,oEACpC,CAAC,EAEF,IAAM,EAAS,MAAM,EAAO,QAAQ,MAAM,EAAM,QAAS,EAAO,KAAM,CACrE,KACD,CAAC,EAUD,OATK,EAAO,GASL,EAAM,KAAK,GARjB,MAAM,EAAO,UAAU,EAAQ,EAAM,OAAO,EACrC,EAAO,MAAM,CACnB,KAAM,eACN,QAAS,eAAe,EAAO,KAAK,YACnC,EAAO,aAAe,IAAA,GAAY,GAAK,iBAAiB,KAAK,KAAK,EAAO,UAAU,EAAE,KAEvF,CAAC,EAGH,CACD,CAEA,MAAM,EAAgB,EAAE,OAAO,CAAE,OAAQ,EAAE,OAAO,CAAC,CAAC,SAAS,CAAE,CAAC"}
@@ -0,0 +1,9 @@
1
+ import { Plugin, Rule } from "@oxlint/plugins";
2
+ //#region src/oxlint.d.ts
3
+ type RuleName = 'component-boundaries' | 'no-component-env' | 'schema-file-boundaries' | 'no-internal-reexports' | 'public-api-first' | 'named-private-helpers';
4
+ /** Library rules derived from src/docs/conventions.md and architecture.md. */
5
+ declare const rules: Record<RuleName, Rule>;
6
+ declare const plugin: Plugin;
7
+ //#endregion
8
+ export { RuleName, plugin as default, rules };
9
+ //# sourceMappingURL=oxlint.d.mts.map
@@ -0,0 +1,2 @@
1
+ import{z as e}from"zod";import{basename as t,dirname as n,relative as r,resolve as i,sep as a}from"node:path";const o=e.object({entryPoints:e.array(e.string()).default([`src/index.ts`])}),s=e.object({packageName:e.string().min(1).optional()}),c={"component-boundaries":{meta:{...u(`Keep portable components isolated and consume their client facades.`,{boundary:`Components may import only their own files and npm dependencies; inject host policy through the client API.`,facade:`Import a component through its client.ts facade, not its private implementation.`}),schema:[{type:`object`,properties:{packageName:{type:`string`,minLength:1}},additionalProperties:!1}]},create(e){let o=d(e.filename),{packageName:c}=s.parse(e.options[0]??{});function l(s,l){let u=f(l);if(!u)return;if(!u.startsWith(`.`)){o&&c&&(u===c||u.startsWith(`${c}/`))&&e.report({node:s,messageId:`boundary`});return}let p=i(n(e.filename),u);o&&(r(o,p).startsWith(`..${a}`)||p===n(o))?e.report({node:s,messageId:`boundary`}):!o&&d(p)&&!/^client(?:\.[cm]?[jt]s)?$/.test(t(p))&&e.report({node:s,messageId:`facade`})}return{ImportDeclaration(e){l(e,e.source)},ExportNamedDeclaration(e){l(e,e.source)},ExportAllDeclaration(e){l(e,e.source)},ImportExpression(e){l(e,e.source)},TSImportType(e){l(e,e.source)},CallExpression(t){t.callee.type===`Identifier`&&t.callee.name===`require`&&!m(e,t.callee)?.defs.length&&l(t,t.arguments[0])}}}},"no-component-env":{meta:u(`Pass environment configuration into portable components.`,{env:`Portable components must receive configuration through arguments; do not read process.env.`}),create(e){if(!d(e.filename))return{};function t(t){if(t.type!==`Identifier`)return!1;let n=m(e,t);return t.name===`process`&&!n?.defs.length||n?.defs.some(e=>e.type===`ImportBinding`&&e.parent?.type===`ImportDeclaration`&&[`node:process`,`process`].includes(e.parent.source.value)&&[`ImportDefaultSpecifier`,`ImportNamespaceSpecifier`].includes(e.node.type))}return{ImportDeclaration(t){if([`node:process`,`process`].includes(t.source.value))for(let n of t.specifiers)n.type===`ImportSpecifier`&&g(n.imported)===`env`&&e.report({node:n,messageId:`env`})},MemberExpression(n){if(p(n)!==`env`)return;let r=n.object,i=r.type===`MemberExpression`&&p(r)===`process`&&r.object.type===`Identifier`&&r.object.name===`globalThis`&&!m(e,r.object)?.defs.length;(t(r)||i)&&e.report({node:n,messageId:`env`})},VariableDeclarator(n){if(n.init&&t(n.init)&&n.id.type===`ObjectPattern`)for(let t of n.id.properties)t.type===`Property`&&(t.computed?f(t.key):g(t.key))===`env`&&e.report({node:t,messageId:`env`})}}}},"schema-file-boundaries":{meta:u(`Keep component storage definitions in schema.ts or table.ts.`,{schema:`{{builder}} belongs in {{file}}, not a function or client module.`}),create(e){return d(e.filename)?{CallExpression(n){let r=n.callee,i=r.type===`Identifier`?r:r.type===`MemberExpression`?r.object:null;if(i?.type!==`Identifier`)return;let a=m(e,i)?.defs.find(e=>e.type===`ImportBinding`&&e.parent?.type===`ImportDeclaration`&&e.parent.source.value===`convex/server`);if(!a)return;let o=a.node.type===`ImportSpecifier`&&r.type===`Identifier`?g(a.node.imported):a.node.type===`ImportNamespaceSpecifier`?p(r):null,s=t(e.filename);o===`defineSchema`&&s!==`schema.ts`?e.report({node:n,messageId:`schema`,data:{builder:o,file:`schema.ts`}}):o===`defineTable`&&![`schema.ts`,`table.ts`].includes(s)&&e.report({node:n,messageId:`schema`,data:{builder:o,file:`schema.ts or table.ts`}})}}:{}}},"no-internal-reexports":{meta:{...u(`Keep re-exports at package entry points and component client facades.`,{reexport:`Import from the owning file directly; only configured entry points and component client.ts facades may re-export.`}),schema:[{type:`object`,properties:{entryPoints:{type:`array`,items:{type:`string`}}},additionalProperties:!1}]},create(e){let t=e.filename.split(a).join(`/`),{entryPoints:n}=o.parse(e.options[0]??{});if(n.some(t=>i(e.cwd,t)===i(e.filename))||/\/src\/components\/[^/]+\/client\.ts$/.test(t))return{};function r(t){if(t.source){e.report({node:t,messageId:`reexport`});return}for(let n of t.type===`ExportNamedDeclaration`?t.specifiers:[])m(e,n.local)?.defs.some(e=>e.type===`ImportBinding`)&&e.report({node:n,messageId:`reexport`})}return{ExportNamedDeclaration:r,ExportAllDeclaration:r,ExportDefaultDeclaration(t){t.declaration.type===`Identifier`&&m(e,t.declaration)?.defs.some(e=>e.type===`ImportBinding`)&&e.report({node:t,messageId:`reexport`})}}}},"public-api-first":{meta:u(`Place private helper implementations after the exported API.`,{order:`Move this private helper below the exported API so the file reads from public purpose to implementation.`}),create(e){return{Program(t){let n=h(t),r=t.body.findLastIndex(e=>e.type.startsWith(`Export`));for(let i of t.body.slice(0,r<0?0:r))i.type===`FunctionDeclaration`&&i.id&&!n.has(i.id.name)&&e.report({node:i,messageId:`order`})}}}},"named-private-helpers":{meta:u(`Use named declarations for top-level private helper functions.`,{helper:`Declare this private helper with function name(...) at the bottom of the file.`}),create(e){return{Program(t){let n=h(t);for(let r of t.body)if(r.type===`VariableDeclaration`)for(let t of r.declarations)t.id.type===`Identifier`&&!n.has(t.id.name)&&(t.init?.type===`ArrowFunctionExpression`||t.init?.type===`FunctionExpression`)&&e.report({node:t,messageId:`helper`})}}}}},l={meta:{name:`cvx`},rules:c};function u(e,t){return{type:`problem`,docs:{description:e},schema:[],messages:t}}function d(e){let t=e.split(a).join(`/`).match(/^(.*\/src\/components\/[^/]+)(?:\/|$)/);return t?i(t[1]):null}function f(e){return e?.type===`Literal`&&`value`in e&&String(e.value)===e.value?String(e.value):e?.type===`TemplateLiteral`&&e.expressions.length===0?e.quasis[0].value.cooked:null}function p(e){return e.type===`MemberExpression`?e.computed?f(e.property):e.property.name:null}function m(e,t){if(t.type!==`Identifier`)return null;let n=e.sourceCode.getScope(t);for(;n;){let e=n.set.get(t.name);if(e)return e;n=n.upper}return null}function h(e){let t=new Set;for(let n of e.body)if(n.type===`ExportNamedDeclaration`&&!n.source)for(let e of n.specifiers)t.add(g(e.local)??``);else n.type===`ExportDefaultDeclaration`&&n.declaration.type===`Identifier`&&t.add(n.declaration.name);return t}function g(e){return e.type===`Identifier`?e.name:f(e)}export{l as default,c as rules};
2
+ //# sourceMappingURL=oxlint.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"oxlint.mjs","names":[],"sources":["../src/oxlint.ts"],"sourcesContent":["import { basename, dirname, relative, resolve, sep } from 'node:path'\nimport type { Context, ESTree, Plugin, Rule, RuleMeta, Scope } from '@oxlint/plugins'\nimport { z } from 'zod'\n\nconst entryPointOptions = z.object({ entryPoints: z.array(z.string()).default(['src/index.ts']) })\nconst componentOptions = z.object({ packageName: z.string().min(1).optional() })\n\nexport type RuleName =\n\t| 'component-boundaries'\n\t| 'no-component-env'\n\t| 'schema-file-boundaries'\n\t| 'no-internal-reexports'\n\t| 'public-api-first'\n\t| 'named-private-helpers'\n\n/** Library rules derived from src/docs/conventions.md and architecture.md. */\nexport const rules: Record<RuleName, Rule> = {\n\t'component-boundaries': {\n\t\tmeta: {\n\t\t\t...metadata('Keep portable components isolated and consume their client facades.', {\n\t\t\t\tboundary:\n\t\t\t\t\t'Components may import only their own files and npm dependencies; inject host policy through the client API.',\n\t\t\t\tfacade: 'Import a component through its client.ts facade, not its private implementation.',\n\t\t\t}),\n\t\t\tschema: [\n\t\t\t\t{\n\t\t\t\t\ttype: 'object',\n\t\t\t\t\tproperties: { packageName: { type: 'string', minLength: 1 } },\n\t\t\t\t\tadditionalProperties: false,\n\t\t\t\t},\n\t\t\t],\n\t\t},\n\t\tcreate(context) {\n\t\t\tconst owner = componentRoot(context.filename)\n\t\t\tconst { packageName } = componentOptions.parse(context.options[0] ?? {})\n\t\t\tfunction check(node: ESTree.Node, source: ESTree.Node | null | undefined) {\n\t\t\t\tconst specifier = staticString(source)\n\t\t\t\tif (!specifier) return\n\t\t\t\tif (!specifier.startsWith('.')) {\n\t\t\t\t\tif (\n\t\t\t\t\t\towner &&\n\t\t\t\t\t\tpackageName &&\n\t\t\t\t\t\t(specifier === packageName || specifier.startsWith(`${packageName}/`))\n\t\t\t\t\t) {\n\t\t\t\t\t\tcontext.report({ node, messageId: 'boundary' })\n\t\t\t\t\t}\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t\tconst target = resolve(dirname(context.filename), specifier)\n\t\t\t\tif (\n\t\t\t\t\towner &&\n\t\t\t\t\t(relative(owner, target).startsWith(`..${sep}`) || target === dirname(owner))\n\t\t\t\t) {\n\t\t\t\t\tcontext.report({ node, messageId: 'boundary' })\n\t\t\t\t} else if (\n\t\t\t\t\t!owner &&\n\t\t\t\t\tcomponentRoot(target) &&\n\t\t\t\t\t!/^client(?:\\.[cm]?[jt]s)?$/.test(basename(target))\n\t\t\t\t) {\n\t\t\t\t\tcontext.report({ node, messageId: 'facade' })\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn {\n\t\t\t\tImportDeclaration(node) {\n\t\t\t\t\tcheck(node, node.source)\n\t\t\t\t},\n\t\t\t\tExportNamedDeclaration(node) {\n\t\t\t\t\tcheck(node, node.source)\n\t\t\t\t},\n\t\t\t\tExportAllDeclaration(node) {\n\t\t\t\t\tcheck(node, node.source)\n\t\t\t\t},\n\t\t\t\tImportExpression(node) {\n\t\t\t\t\tcheck(node, node.source)\n\t\t\t\t},\n\t\t\t\tTSImportType(node) {\n\t\t\t\t\tcheck(node, node.source)\n\t\t\t\t},\n\t\t\t\tCallExpression(node) {\n\t\t\t\t\tif (\n\t\t\t\t\t\tnode.callee.type === 'Identifier' &&\n\t\t\t\t\t\tnode.callee.name === 'require' &&\n\t\t\t\t\t\t!binding(context, node.callee)?.defs.length\n\t\t\t\t\t) {\n\t\t\t\t\t\tcheck(node, node.arguments[0])\n\t\t\t\t\t}\n\t\t\t\t},\n\t\t\t}\n\t\t},\n\t},\n\t'no-component-env': {\n\t\tmeta: metadata('Pass environment configuration into portable components.', {\n\t\t\tenv: 'Portable components must receive configuration through arguments; do not read process.env.',\n\t\t}),\n\t\tcreate(context) {\n\t\t\tif (!componentRoot(context.filename)) return {}\n\t\t\tfunction isProcess(node: ESTree.Node) {\n\t\t\t\tif (node.type !== 'Identifier') return false\n\t\t\t\tconst variable = binding(context, node)\n\t\t\t\tif (node.name === 'process' && !variable?.defs.length) return true\n\t\t\t\treturn variable?.defs.some(\n\t\t\t\t\t(def) =>\n\t\t\t\t\t\tdef.type === 'ImportBinding' &&\n\t\t\t\t\t\tdef.parent?.type === 'ImportDeclaration' &&\n\t\t\t\t\t\t['node:process', 'process'].includes(def.parent.source.value) &&\n\t\t\t\t\t\t['ImportDefaultSpecifier', 'ImportNamespaceSpecifier'].includes(def.node.type),\n\t\t\t\t)\n\t\t\t}\n\t\t\treturn {\n\t\t\t\tImportDeclaration(node) {\n\t\t\t\t\tif (['node:process', 'process'].includes(node.source.value)) {\n\t\t\t\t\t\tfor (const specifier of node.specifiers) {\n\t\t\t\t\t\t\tif (\n\t\t\t\t\t\t\t\tspecifier.type === 'ImportSpecifier' &&\n\t\t\t\t\t\t\t\texportName(specifier.imported) === 'env'\n\t\t\t\t\t\t\t) {\n\t\t\t\t\t\t\t\tcontext.report({ node: specifier, messageId: 'env' })\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t},\n\t\t\t\tMemberExpression(node) {\n\t\t\t\t\tif (propertyName(node) !== 'env') return\n\t\t\t\t\tconst object = node.object\n\t\t\t\t\tconst globalProcess =\n\t\t\t\t\t\tobject.type === 'MemberExpression' &&\n\t\t\t\t\t\tpropertyName(object) === 'process' &&\n\t\t\t\t\t\tobject.object.type === 'Identifier' &&\n\t\t\t\t\t\tobject.object.name === 'globalThis' &&\n\t\t\t\t\t\t!binding(context, object.object)?.defs.length\n\t\t\t\t\tif (isProcess(object) || globalProcess) context.report({ node, messageId: 'env' })\n\t\t\t\t},\n\t\t\t\tVariableDeclarator(node) {\n\t\t\t\t\tif (node.init && isProcess(node.init) && node.id.type === 'ObjectPattern') {\n\t\t\t\t\t\tfor (const property of node.id.properties) {\n\t\t\t\t\t\t\tif (\n\t\t\t\t\t\t\t\tproperty.type === 'Property' &&\n\t\t\t\t\t\t\t\t(property.computed ? staticString(property.key) : exportName(property.key)) ===\n\t\t\t\t\t\t\t\t\t'env'\n\t\t\t\t\t\t\t) {\n\t\t\t\t\t\t\t\tcontext.report({ node: property, messageId: 'env' })\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t},\n\t\t\t}\n\t\t},\n\t},\n\t'schema-file-boundaries': {\n\t\tmeta: metadata('Keep component storage definitions in schema.ts or table.ts.', {\n\t\t\tschema: '{{builder}} belongs in {{file}}, not a function or client module.',\n\t\t}),\n\t\tcreate(context) {\n\t\t\tif (!componentRoot(context.filename)) return {}\n\t\t\treturn {\n\t\t\t\tCallExpression(node) {\n\t\t\t\t\tconst callee = node.callee\n\t\t\t\t\tconst identifier =\n\t\t\t\t\t\tcallee.type === 'Identifier'\n\t\t\t\t\t\t\t? callee\n\t\t\t\t\t\t\t: callee.type === 'MemberExpression'\n\t\t\t\t\t\t\t\t? callee.object\n\t\t\t\t\t\t\t\t: null\n\t\t\t\t\tif (identifier?.type !== 'Identifier') return\n\t\t\t\t\tconst definition = binding(context, identifier)?.defs.find(\n\t\t\t\t\t\t(def) =>\n\t\t\t\t\t\t\tdef.type === 'ImportBinding' &&\n\t\t\t\t\t\t\tdef.parent?.type === 'ImportDeclaration' &&\n\t\t\t\t\t\t\tdef.parent.source.value === 'convex/server',\n\t\t\t\t\t)\n\t\t\t\t\tif (!definition) return\n\t\t\t\t\tconst builder =\n\t\t\t\t\t\tdefinition.node.type === 'ImportSpecifier' && callee.type === 'Identifier'\n\t\t\t\t\t\t\t? exportName(definition.node.imported)\n\t\t\t\t\t\t\t: definition.node.type === 'ImportNamespaceSpecifier'\n\t\t\t\t\t\t\t\t? propertyName(callee)\n\t\t\t\t\t\t\t\t: null\n\t\t\t\t\tconst file = basename(context.filename)\n\t\t\t\t\tif (builder === 'defineSchema' && file !== 'schema.ts') {\n\t\t\t\t\t\tcontext.report({ node, messageId: 'schema', data: { builder, file: 'schema.ts' } })\n\t\t\t\t\t} else if (builder === 'defineTable' && !['schema.ts', 'table.ts'].includes(file)) {\n\t\t\t\t\t\tcontext.report({\n\t\t\t\t\t\t\tnode,\n\t\t\t\t\t\t\tmessageId: 'schema',\n\t\t\t\t\t\t\tdata: { builder, file: 'schema.ts or table.ts' },\n\t\t\t\t\t\t})\n\t\t\t\t\t}\n\t\t\t\t},\n\t\t\t}\n\t\t},\n\t},\n\t'no-internal-reexports': {\n\t\tmeta: {\n\t\t\t...metadata('Keep re-exports at package entry points and component client facades.', {\n\t\t\t\treexport:\n\t\t\t\t\t'Import from the owning file directly; only configured entry points and component client.ts facades may re-export.',\n\t\t\t}),\n\t\t\tschema: [\n\t\t\t\t{\n\t\t\t\t\ttype: 'object',\n\t\t\t\t\tproperties: { entryPoints: { type: 'array', items: { type: 'string' } } },\n\t\t\t\t\tadditionalProperties: false,\n\t\t\t\t},\n\t\t\t],\n\t\t},\n\t\tcreate(context) {\n\t\t\tconst file = context.filename.split(sep).join('/')\n\t\t\tconst { entryPoints } = entryPointOptions.parse(context.options[0] ?? {})\n\t\t\tconst isEntryPoint = entryPoints.some(\n\t\t\t\t(entry) => resolve(context.cwd, entry) === resolve(context.filename),\n\t\t\t)\n\t\t\tif (isEntryPoint || /\\/src\\/components\\/[^/]+\\/client\\.ts$/.test(file)) return {}\n\t\t\tfunction check(node: ESTree.ExportNamedDeclaration | ESTree.ExportAllDeclaration) {\n\t\t\t\tif (node.source) {\n\t\t\t\t\tcontext.report({ node, messageId: 'reexport' })\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t\tfor (const specifier of node.type === 'ExportNamedDeclaration' ? node.specifiers : []) {\n\t\t\t\t\tif (binding(context, specifier.local)?.defs.some((def) => def.type === 'ImportBinding')) {\n\t\t\t\t\t\tcontext.report({ node: specifier, messageId: 'reexport' })\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn {\n\t\t\t\tExportNamedDeclaration: check,\n\t\t\t\tExportAllDeclaration: check,\n\t\t\t\tExportDefaultDeclaration(node) {\n\t\t\t\t\tif (\n\t\t\t\t\t\tnode.declaration.type === 'Identifier' &&\n\t\t\t\t\t\tbinding(context, node.declaration)?.defs.some((def) => def.type === 'ImportBinding')\n\t\t\t\t\t) {\n\t\t\t\t\t\tcontext.report({ node, messageId: 'reexport' })\n\t\t\t\t\t}\n\t\t\t\t},\n\t\t\t}\n\t\t},\n\t},\n\t'public-api-first': {\n\t\tmeta: metadata('Place private helper implementations after the exported API.', {\n\t\t\torder:\n\t\t\t\t'Move this private helper below the exported API so the file reads from public purpose to implementation.',\n\t\t}),\n\t\tcreate(context) {\n\t\t\treturn {\n\t\t\t\tProgram(node) {\n\t\t\t\t\tconst exports = exportedNames(node)\n\t\t\t\t\tconst lastExport = node.body.findLastIndex((statement) =>\n\t\t\t\t\t\tstatement.type.startsWith('Export'),\n\t\t\t\t\t)\n\t\t\t\t\tfor (const statement of node.body.slice(0, lastExport < 0 ? 0 : lastExport)) {\n\t\t\t\t\t\tif (\n\t\t\t\t\t\t\tstatement.type === 'FunctionDeclaration' &&\n\t\t\t\t\t\t\tstatement.id &&\n\t\t\t\t\t\t\t!exports.has(statement.id.name)\n\t\t\t\t\t\t) {\n\t\t\t\t\t\t\tcontext.report({ node: statement, messageId: 'order' })\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t},\n\t\t\t}\n\t\t},\n\t},\n\t'named-private-helpers': {\n\t\tmeta: metadata('Use named declarations for top-level private helper functions.', {\n\t\t\thelper: 'Declare this private helper with function name(...) at the bottom of the file.',\n\t\t}),\n\t\tcreate(context) {\n\t\t\treturn {\n\t\t\t\tProgram(node) {\n\t\t\t\t\tconst exports = exportedNames(node)\n\t\t\t\t\tfor (const statement of node.body) {\n\t\t\t\t\t\tif (statement.type !== 'VariableDeclaration') continue\n\t\t\t\t\t\tfor (const declaration of statement.declarations) {\n\t\t\t\t\t\t\tif (\n\t\t\t\t\t\t\t\tdeclaration.id.type === 'Identifier' &&\n\t\t\t\t\t\t\t\t!exports.has(declaration.id.name) &&\n\t\t\t\t\t\t\t\t(declaration.init?.type === 'ArrowFunctionExpression' ||\n\t\t\t\t\t\t\t\t\tdeclaration.init?.type === 'FunctionExpression')\n\t\t\t\t\t\t\t) {\n\t\t\t\t\t\t\t\tcontext.report({ node: declaration, messageId: 'helper' })\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t},\n\t\t\t}\n\t\t},\n\t},\n}\n\nconst plugin: Plugin = { meta: { name: 'cvx' }, rules }\nexport default plugin\n\nfunction metadata(description: string, messages: Record<string, string>): RuleMeta {\n\treturn { type: 'problem', docs: { description }, schema: [], messages }\n}\n\nfunction componentRoot(filename: string) {\n\tconst match = filename\n\t\t.split(sep)\n\t\t.join('/')\n\t\t.match(/^(.*\\/src\\/components\\/[^/]+)(?:\\/|$)/)\n\treturn match ? resolve(match[1]) : null\n}\n\nfunction staticString(node: ESTree.Node | null | undefined) {\n\tif (node?.type === 'Literal' && 'value' in node && String(node.value) === node.value)\n\t\treturn String(node.value)\n\tif (node?.type === 'TemplateLiteral' && node.expressions.length === 0)\n\t\treturn node.quasis[0].value.cooked\n\treturn null\n}\n\nfunction propertyName(node: ESTree.Node): string | null {\n\tif (node.type !== 'MemberExpression') return null\n\treturn node.computed ? staticString(node.property) : node.property.name\n}\n\nfunction binding(context: Context, node: ESTree.Node) {\n\tif (node.type !== 'Identifier') return null\n\tlet scope: Scope | null = context.sourceCode.getScope(node)\n\twhile (scope) {\n\t\tconst variable = scope.set.get(node.name)\n\t\tif (variable) return variable\n\t\tscope = scope.upper\n\t}\n\treturn null\n}\n\nfunction exportedNames(program: ESTree.Program) {\n\tconst names = new Set<string>()\n\tfor (const statement of program.body) {\n\t\tif (statement.type === 'ExportNamedDeclaration' && !statement.source) {\n\t\t\tfor (const specifier of statement.specifiers) names.add(exportName(specifier.local) ?? '')\n\t\t} else if (\n\t\t\tstatement.type === 'ExportDefaultDeclaration' &&\n\t\t\tstatement.declaration.type === 'Identifier'\n\t\t) {\n\t\t\tnames.add(statement.declaration.name)\n\t\t}\n\t}\n\treturn names\n}\n\nfunction exportName(node: ESTree.Node): string | null {\n\treturn node.type === 'Identifier' ? node.name : staticString(node)\n}\n"],"mappings":"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"}
package/dist/tenancy.mjs CHANGED
@@ -1,2 +1,2 @@
1
- import{defaultErrors as e}from"./errors.mjs";import{wrapDatabaseReader as t,wrapDatabaseWriter as n}from"convex-helpers/server/rowLevelSecurity";const r=`tenant`;function i(e,t){let n=async(t,n)=>n[r]===e;return Object.fromEntries(t.map(e=>[e,{read:n,insert:n,modify:n}]))}function a(...e){let t=new Set(e.flatMap(e=>Object.keys(e))),n={};for(let r of t){let t={};for(let n of[`read`,`insert`,`modify`]){let i=e.map(e=>e[r]).map(e=>e?.[n]).filter(e=>typeof e==`function`);i.length!==0&&(t[n]=async(e,t)=>{for(let n of i)if(!await n(e,t))return!1;return!0})}n[r]=t}return n}async function o(t,n,r=e){let i=await n();return!i||i.tenant!==t?r.throw({code:`REFERENCE_NOT_FOUND`,message:`The referenced row is unavailable`}):i}function s(t,n,r=e){return n.tenant===t?n:r.throw({code:`CROSS_TENANT_REFERENCE`,message:`The document belongs to another tenant`})}export{r as TENANT_FIELD,s as assertTenantOwned,a as composeRules,i as createTenantRules,o as requireTenantReference,t as wrapDatabaseReader,n as wrapDatabaseWriter};
1
+ import{defaultErrors as e}from"./errors.mjs";import{wrapDatabaseReader as t,wrapDatabaseWriter as n}from"convex-helpers/server/rowLevelSecurity";const r=`tenant`;function i(e,t){let n={};for(let i of t)n[i]={read:async(t,n)=>n[r]===e,insert:async(t,n)=>n[r]===e,modify:async(t,n)=>n[r]===e};return n}function a(...e){let t=new Set(e.flatMap(e=>Object.keys(e))),n={};for(let r of t){let t=e.flatMap(e=>e[r]?.read??[]),i=e.flatMap(e=>e[r]?.insert??[]),a=e.flatMap(e=>e[r]?.modify??[]),o={};t.length&&(o.read=async(e,n)=>{for(let r of t)if(!await r(e,n))return!1;return!0}),i.length&&(o.insert=async(e,t)=>{for(let n of i)if(!await n(e,t))return!1;return!0}),a.length&&(o.modify=async(e,t)=>{for(let n of a)if(!await n(e,t))return!1;return!0}),n[r]=o}return n}async function o(t,n,r=e){let i=await n();return!i||i.tenant!==t?r.throw({code:`REFERENCE_NOT_FOUND`,message:`The referenced row is unavailable`}):i}function s(t,n,r=e){return n.tenant===t?n:r.throw({code:`CROSS_TENANT_REFERENCE`,message:`The document belongs to another tenant`})}export{r as TENANT_FIELD,s as assertTenantOwned,a as composeRules,i as createTenantRules,o as requireTenantReference,t as wrapDatabaseReader,n as wrapDatabaseWriter};
2
2
  //# sourceMappingURL=tenancy.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"tenancy.mjs","names":[],"sources":["../src/tenancy.ts"],"sourcesContent":["import {\n\twrapDatabaseReader,\n\twrapDatabaseWriter,\n\ttype RLSConfig,\n\ttype Rules,\n} from 'convex-helpers/server/rowLevelSecurity'\nimport type {\n\tGenericDataModel,\n\tTableNamesInDataModel,\n} from 'convex/server'\nimport { defaultErrors, type ErrorFactory } from './errors'\n\nexport { wrapDatabaseReader, wrapDatabaseWriter }\nexport type { RLSConfig, Rules }\n\n/** The server-owned tenancy field stamped on every tenant table row. */\nexport const TENANT_FIELD = 'tenant' as const\n\ntype TenantRow = { [TENANT_FIELD]?: unknown }\n\n/**\n * Deny-oriented row-level security rules generated mechanically from a table\n * registry: read/insert/modify pass only when the row's tenant matches.\n * Registering a table here is the entire onboarding — there is no second\n * per-table step to forget.\n */\nexport function createTenantRules<DataModel extends GenericDataModel>(\n\ttenant: string,\n\ttables: readonly TableNamesInDataModel<DataModel>[],\n): Rules<unknown, DataModel> {\n\tconst owned = async (_ctx: unknown, doc: TenantRow) =>\n\t\tdoc[TENANT_FIELD] === tenant\n\treturn Object.fromEntries(\n\t\ttables.map((table) => [\n\t\t\ttable,\n\t\t\t{ read: owned, insert: owned, modify: owned },\n\t\t]),\n\t) as unknown as Rules<unknown, DataModel>\n}\n\n/**\n * AND-composes rule sets: a row passes an operation only when every rule set\n * that defines a rule for it passes. Lets tenant isolation and role-level\n * rules coexist on the same wrapped database.\n */\nexport function composeRules<Ctx, DataModel extends GenericDataModel>(\n\t...sets: readonly Rules<Ctx, DataModel>[]\n): Rules<Ctx, DataModel> {\n\tconst tables = new Set(sets.flatMap((set) => Object.keys(set)))\n\tconst composed: Record<string, Record<string, unknown>> = {}\n\tfor (const table of tables) {\n\t\tconst operations: Record<string, unknown> = {}\n\t\tfor (const operation of ['read', 'insert', 'modify'] as const) {\n\t\t\tconst rules = sets\n\t\t\t\t.map((set) => (set as Record<string, Record<string, unknown>>)[table])\n\t\t\t\t.map((tableRules) => tableRules?.[operation])\n\t\t\t\t.filter((rule): rule is (ctx: Ctx, doc: never) => Promise<boolean> =>\n\t\t\t\t\ttypeof rule === 'function',\n\t\t\t\t)\n\t\t\tif (rules.length === 0) continue\n\t\t\toperations[operation] = async (ctx: Ctx, doc: never) => {\n\t\t\t\tfor (const rule of rules) {\n\t\t\t\t\tif (!(await rule(ctx, doc))) return false\n\t\t\t\t}\n\t\t\t\treturn true\n\t\t\t}\n\t\t}\n\t\tcomposed[table] = operations\n\t}\n\treturn composed as Rules<Ctx, DataModel>\n}\n\n/**\n * Loads a client-referenced document and verifies tenant ownership without\n * disclosing existence: a missing row and a foreign row fail identically\n * with REFERENCE_NOT_FOUND. Use for every id argument a client supplies.\n */\nexport async function requireTenantReference<\n\tDocument extends { [TENANT_FIELD]?: unknown },\n>(\n\ttenant: string,\n\tload: () => Promise<Document | null>,\n\terrors: ErrorFactory = defaultErrors,\n): Promise<Document> {\n\tconst document = await load()\n\tif (!document || document[TENANT_FIELD] !== tenant) {\n\t\treturn errors.throw({\n\t\t\tcode: 'REFERENCE_NOT_FOUND',\n\t\t\tmessage: 'The referenced row is unavailable',\n\t\t})\n\t}\n\treturn document\n}\n\n/** Asserts a loaded document belongs to the tenant; use on internal paths. */\nexport function assertTenantOwned<\n\tDocument extends { [TENANT_FIELD]?: unknown },\n>(\n\ttenant: string,\n\tdocument: Document,\n\terrors: ErrorFactory = defaultErrors,\n): Document {\n\tif (document[TENANT_FIELD] !== tenant) {\n\t\treturn errors.throw({\n\t\t\tcode: 'CROSS_TENANT_REFERENCE',\n\t\t\tmessage: 'The document belongs to another tenant',\n\t\t})\n\t}\n\treturn document\n}\n"],"mappings":"iJAgBA,MAAa,EAAe,SAU5B,SAAgB,EACf,EACA,EAC4B,CAC5B,IAAM,EAAQ,MAAO,EAAe,IACnC,EAAI,KAAkB,EACvB,OAAO,OAAO,YACb,EAAO,IAAK,GAAU,CACrB,EACA,CAAE,KAAM,EAAO,OAAQ,EAAO,OAAQ,CAAM,CAC7C,CAAC,CACF,CACD,CAOA,SAAgB,EACf,GAAG,EACqB,CACxB,IAAM,EAAS,IAAI,IAAI,EAAK,QAAS,GAAQ,OAAO,KAAK,CAAG,CAAC,CAAC,EACxD,EAAoD,CAAC,EAC3D,IAAK,IAAM,KAAS,EAAQ,CAC3B,IAAM,EAAsC,CAAC,EAC7C,IAAK,IAAM,IAAa,CAAC,OAAQ,SAAU,QAAQ,EAAY,CAC9D,IAAM,EAAQ,EACZ,IAAK,GAAS,EAAgD,EAAM,CAAC,CACrE,IAAK,GAAe,IAAa,EAAU,CAAC,CAC5C,OAAQ,GACR,OAAO,GAAS,UACjB,EACG,EAAM,SAAW,IACrB,EAAW,GAAa,MAAO,EAAU,IAAe,CACvD,IAAK,IAAM,KAAQ,EAClB,GAAI,CAAE,MAAM,EAAK,EAAK,CAAG,EAAI,MAAO,GAErC,MAAO,EACR,EACD,CACA,EAAS,GAAS,CACnB,CACA,OAAO,CACR,CAOA,eAAsB,EAGrB,EACA,EACA,EAAuB,EACH,CACpB,IAAM,EAAW,MAAM,EAAK,EAO5B,MANI,CAAC,GAAY,EAAA,SAA2B,EACpC,EAAO,MAAM,CACnB,KAAM,sBACN,QAAS,mCACV,CAAC,EAEK,CACR,CAGA,SAAgB,EAGf,EACA,EACA,EAAuB,EACZ,CAOX,OANI,EAAA,SAA2B,EAMxB,EALC,EAAO,MAAM,CACnB,KAAM,yBACN,QAAS,wCACV,CAAC,CAGH"}
1
+ {"version":3,"file":"tenancy.mjs","names":[],"sources":["../src/tenancy.ts"],"sourcesContent":["import {\n\twrapDatabaseReader,\n\twrapDatabaseWriter,\n\ttype RLSConfig,\n\ttype Rules,\n} from 'convex-helpers/server/rowLevelSecurity'\nimport type { GenericDataModel, TableNamesInDataModel } from 'convex/server'\nimport { defaultErrors, type ErrorFactory } from './errors'\n\nexport { wrapDatabaseReader, wrapDatabaseWriter }\nexport type { RLSConfig, Rules }\n\n/** The server-owned tenancy field stamped on every tenant table row. */\nexport const TENANT_FIELD = 'tenant' as const\n\n/**\n * Deny-oriented row-level security rules generated mechanically from a table\n * registry: read/insert/modify pass only when the row's tenant matches.\n * Registering a table here is the entire onboarding — there is no second\n * per-table step to forget.\n */\nexport function createTenantRules<DataModel extends GenericDataModel>(\n\ttenant: string,\n\ttables: readonly TableNamesInDataModel<DataModel>[],\n): Rules<unknown, DataModel> {\n\tconst rules: Rules<unknown, DataModel> = {}\n\tfor (const table of tables) {\n\t\trules[table] = {\n\t\t\tread: async (_context, document) => document[TENANT_FIELD] === tenant,\n\t\t\tinsert: async (_context, document) => document[TENANT_FIELD] === tenant,\n\t\t\tmodify: async (_context, document) => document[TENANT_FIELD] === tenant,\n\t\t}\n\t}\n\treturn rules\n}\n\n/**\n * AND-composes rule sets: a row passes an operation only when every rule set\n * that defines a rule for it passes. Lets tenant isolation and role-level\n * rules coexist on the same wrapped database.\n */\nexport function composeRules<Ctx, DataModel extends GenericDataModel>(\n\t...sets: readonly Rules<Ctx, DataModel>[]\n): Rules<Ctx, DataModel> {\n\t// SAFETY: Rules keys are table names from DataModel; values come only from the supplied rule sets.\n\tconst tables = new Set(sets.flatMap((set) => Object.keys(set))) as Set<\n\t\tTableNamesInDataModel<DataModel>\n\t>\n\tconst composed: Rules<Ctx, DataModel> = {}\n\tfor (const table of tables) {\n\t\tconst reads = sets.flatMap((set) => set[table]?.read ?? [])\n\t\tconst inserts = sets.flatMap((set) => set[table]?.insert ?? [])\n\t\tconst modifies = sets.flatMap((set) => set[table]?.modify ?? [])\n\t\tconst rules: NonNullable<Rules<Ctx, DataModel>[typeof table]> = {}\n\t\tif (reads.length)\n\t\t\trules.read = async (context, document) => {\n\t\t\t\tfor (const rule of reads) if (!(await rule(context, document))) return false\n\t\t\t\treturn true\n\t\t\t}\n\t\tif (inserts.length)\n\t\t\trules.insert = async (context, document) => {\n\t\t\t\tfor (const rule of inserts) if (!(await rule(context, document))) return false\n\t\t\t\treturn true\n\t\t\t}\n\t\tif (modifies.length)\n\t\t\trules.modify = async (context, document) => {\n\t\t\t\tfor (const rule of modifies) if (!(await rule(context, document))) return false\n\t\t\t\treturn true\n\t\t\t}\n\t\tcomposed[table] = rules\n\t}\n\treturn composed\n}\n\n/**\n * Loads a client-referenced document and verifies tenant ownership without\n * disclosing existence: a missing row and a foreign row fail identically\n * with REFERENCE_NOT_FOUND. Use for every id argument a client supplies.\n */\nexport async function requireTenantReference<Document extends { [TENANT_FIELD]?: unknown }>(\n\ttenant: string,\n\tload: () => Promise<Document | null>,\n\terrors: ErrorFactory = defaultErrors,\n): Promise<Document> {\n\tconst document = await load()\n\tif (!document || document[TENANT_FIELD] !== tenant) {\n\t\treturn errors.throw({\n\t\t\tcode: 'REFERENCE_NOT_FOUND',\n\t\t\tmessage: 'The referenced row is unavailable',\n\t\t})\n\t}\n\treturn document\n}\n\n/** Asserts a loaded document belongs to the tenant; use on internal paths. */\nexport function assertTenantOwned<Document extends { [TENANT_FIELD]?: unknown }>(\n\ttenant: string,\n\tdocument: Document,\n\terrors: ErrorFactory = defaultErrors,\n): Document {\n\tif (document[TENANT_FIELD] !== tenant) {\n\t\treturn errors.throw({\n\t\t\tcode: 'CROSS_TENANT_REFERENCE',\n\t\t\tmessage: 'The document belongs to another tenant',\n\t\t})\n\t}\n\treturn document\n}\n"],"mappings":"iJAaA,MAAa,EAAe,SAQ5B,SAAgB,EACf,EACA,EAC4B,CAC5B,IAAM,EAAmC,CAAC,EAC1C,IAAK,IAAM,KAAS,EACnB,EAAM,GAAS,CACd,KAAM,MAAO,EAAU,IAAa,EAAS,KAAkB,EAC/D,OAAQ,MAAO,EAAU,IAAa,EAAS,KAAkB,EACjE,OAAQ,MAAO,EAAU,IAAa,EAAS,KAAkB,CAClE,EAED,OAAO,CACR,CAOA,SAAgB,EACf,GAAG,EACqB,CAExB,IAAM,EAAS,IAAI,IAAI,EAAK,QAAS,GAAQ,OAAO,KAAK,CAAG,CAAC,CAAC,EAGxD,EAAkC,CAAC,EACzC,IAAK,IAAM,KAAS,EAAQ,CAC3B,IAAM,EAAQ,EAAK,QAAS,GAAQ,EAAI,EAAM,EAAE,MAAQ,CAAC,CAAC,EACpD,EAAU,EAAK,QAAS,GAAQ,EAAI,EAAM,EAAE,QAAU,CAAC,CAAC,EACxD,EAAW,EAAK,QAAS,GAAQ,EAAI,EAAM,EAAE,QAAU,CAAC,CAAC,EACzD,EAA0D,CAAC,EAC7D,EAAM,SACT,EAAM,KAAO,MAAO,EAAS,IAAa,CACzC,IAAK,IAAM,KAAQ,EAAO,GAAI,CAAE,MAAM,EAAK,EAAS,CAAQ,EAAI,MAAO,GACvE,MAAO,EACR,GACG,EAAQ,SACX,EAAM,OAAS,MAAO,EAAS,IAAa,CAC3C,IAAK,IAAM,KAAQ,EAAS,GAAI,CAAE,MAAM,EAAK,EAAS,CAAQ,EAAI,MAAO,GACzE,MAAO,EACR,GACG,EAAS,SACZ,EAAM,OAAS,MAAO,EAAS,IAAa,CAC3C,IAAK,IAAM,KAAQ,EAAU,GAAI,CAAE,MAAM,EAAK,EAAS,CAAQ,EAAI,MAAO,GAC1E,MAAO,EACR,GACD,EAAS,GAAS,CACnB,CACA,OAAO,CACR,CAOA,eAAsB,EACrB,EACA,EACA,EAAuB,EACH,CACpB,IAAM,EAAW,MAAM,EAAK,EAO5B,MANI,CAAC,GAAY,EAAA,SAA2B,EACpC,EAAO,MAAM,CACnB,KAAM,sBACN,QAAS,mCACV,CAAC,EAEK,CACR,CAGA,SAAgB,EACf,EACA,EACA,EAAuB,EACZ,CAOX,OANI,EAAA,SAA2B,EAMxB,EALC,EAAO,MAAM,CACnB,KAAM,yBACN,QAAS,wCACV,CAAC,CAGH"}
package/dist/triggers.mjs CHANGED
@@ -1,2 +1,2 @@
1
- import{Triggers as e}from"convex-helpers/server/triggers";function t(){return new e}function n(e,...t){for(let n of t)e.register(n,async(e,t)=>{if(t.operation!==`insert`)throw Error(`${n} is append-only`)})}function r(e,...t){for(let n of t)e.register(n,async(e,t)=>{if(t.operation===`delete`)throw Error(`${n} rows cannot be deleted`)})}function i(e,...t){for(let n of t)e.register(n,async(e,t)=>{if(t.operation===`insert`){let e=t.newDoc;if(typeof e.tenant!=`string`||e.tenant.length===0)throw Error(`${n} rows require a tenant`);return}if(t.operation===`update`){let e=t.oldDoc,r=t.newDoc;if(e.tenant!==r.tenant)throw Error(`${n} tenant ownership cannot be reassigned`)}})}function a(e,...t){for(let n of t)e.register(n,async(e,t)=>{if(t.operation===`delete`)return;let n=Date.now();if(t.operation===`insert`){t.newDoc.createdAt===void 0&&await e.innerDb.patch(t.id,{createdAt:n,updatedAt:n});return}let r=t.oldDoc;t.newDoc.updatedAt===r.updatedAt&&await e.innerDb.patch(t.id,{updatedAt:n})})}export{e as Triggers,n as appendOnly,t as createTriggers,r as noDelete,i as tenantOwnership,a as timestamps};
1
+ import{z as e}from"zod";import{Triggers as t}from"convex-helpers/server/triggers";function n(){return new t}function r(e,...t){for(let n of t)e.register(n,async(e,t)=>{if(t.operation!==`insert`)throw Error(`${n} is append-only`)})}function i(e,...t){for(let n of t)e.register(n,async(e,t)=>{if(t.operation===`delete`)throw Error(`${n} rows cannot be deleted`)})}function a(e,...t){for(let n of t)e.register(n,async(e,t)=>{if(t.operation===`insert`){if(!s.safeParse(t.newDoc).success)throw Error(`${n} rows require a tenant`);return}if(t.operation===`update`){let e=t.oldDoc,r=t.newDoc;if(e.tenant!==r.tenant)throw Error(`${n} tenant ownership cannot be reassigned`)}})}function o(e,...t){for(let n of t)e.register(n,async(e,t)=>{if(t.operation===`delete`)return;let n=Date.now();if(t.operation===`insert`){t.newDoc.createdAt===void 0&&await e.innerDb.patch(t.id,{createdAt:n,updatedAt:n});return}let r=t.oldDoc;t.newDoc.updatedAt===r.updatedAt&&await e.innerDb.patch(t.id,{updatedAt:n})})}const s=e.object({tenant:e.string().min(1)});export{t as Triggers,r as appendOnly,n as createTriggers,i as noDelete,a as tenantOwnership,o as timestamps};
2
2
  //# sourceMappingURL=triggers.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"triggers.mjs","names":[],"sources":["../src/triggers.ts"],"sourcesContent":["import {\n\tTriggers,\n\ttype Change,\n\ttype Trigger,\n} from 'convex-helpers/server/triggers'\nimport type {\n\tGenericDataModel,\n\tTableNamesInDataModel,\n} from 'convex/server'\n\nexport { Triggers }\nexport type { Change, Trigger }\n\n/**\n * One application-wide mutation interceptor registry. Pass it to\n * createAuthFunctions({ triggers }) so every authMutation/roleMutation/\n * systemMutation runs writes through it — trigger enforcement becomes\n * structural, not a convention.\n */\nexport function createTriggers<\n\tDataModel extends GenericDataModel,\n>(): Triggers<DataModel> {\n\treturn new Triggers<DataModel>()\n}\n\n/**\n * Marks tables as append-only evidence tables: inserts pass, every update or\n * delete throws. The ontology pattern for votes/history/audit-style tables.\n */\nexport function appendOnly<DataModel extends GenericDataModel>(\n\ttriggers: Triggers<DataModel>,\n\t...tables: readonly TableNamesInDataModel<DataModel>[]\n) {\n\tfor (const table of tables) {\n\t\ttriggers.register(table, async (_ctx, change) => {\n\t\t\tif (change.operation !== 'insert') {\n\t\t\t\tthrow new Error(`${table} is append-only`)\n\t\t\t}\n\t\t})\n\t}\n}\n\n/** Forbids hard deletes on the given tables (soft-delete discipline). */\nexport function noDelete<DataModel extends GenericDataModel>(\n\ttriggers: Triggers<DataModel>,\n\t...tables: readonly TableNamesInDataModel<DataModel>[]\n) {\n\tfor (const table of tables) {\n\t\ttriggers.register(table, async (_ctx, change) => {\n\t\t\tif (change.operation === 'delete') {\n\t\t\t\tthrow new Error(`${table} rows cannot be deleted`)\n\t\t\t}\n\t\t})\n\t}\n}\n\n/**\n * Post-image tenant invariant for tenantTable rows. Row-level security\n * authorizes a modification against the PRE-image, so ownership must also be\n * enforced against the post-image: inserts must carry a tenant, and updates\n * may never reassign it. Register for every tenant table (same registry that\n * drives createTenantRules).\n */\nexport function tenantOwnership<DataModel extends GenericDataModel>(\n\ttriggers: Triggers<DataModel>,\n\t...tables: readonly TableNamesInDataModel<DataModel>[]\n) {\n\tfor (const table of tables) {\n\t\ttriggers.register(table, async (_ctx, change) => {\n\t\t\tif (change.operation === 'insert') {\n\t\t\t\tconst document = change.newDoc as { tenant?: unknown }\n\t\t\t\tif (\n\t\t\t\t\ttypeof document.tenant !== 'string' ||\n\t\t\t\t\tdocument.tenant.length === 0\n\t\t\t\t) {\n\t\t\t\t\tthrow new Error(`${table} rows require a tenant`)\n\t\t\t\t}\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif (change.operation === 'update') {\n\t\t\t\tconst previous = change.oldDoc as { tenant?: unknown }\n\t\t\t\tconst current = change.newDoc as { tenant?: unknown }\n\t\t\t\tif (previous.tenant !== current.tenant) {\n\t\t\t\t\tthrow new Error(`${table} tenant ownership cannot be reassigned`)\n\t\t\t\t}\n\t\t\t}\n\t\t})\n\t}\n}\n\n/**\n * Maintains zod-table's opinionated lifecycle timestamps: createdAt and\n * updatedAt on insert, updatedAt on every update. archivedAt stays under\n * application control. Register once per table; writes made through the\n * auth constructors keep these fields correct automatically.\n */\nexport function timestamps<DataModel extends GenericDataModel>(\n\ttriggers: Triggers<DataModel>,\n\t...tables: readonly TableNamesInDataModel<DataModel>[]\n) {\n\tfor (const table of tables) {\n\t\ttriggers.register(table, async (ctx, change) => {\n\t\t\tif (change.operation === 'delete') return\n\t\t\tconst now = Date.now()\n\t\t\tif (change.operation === 'insert') {\n\t\t\t\tconst document = change.newDoc as { createdAt?: number }\n\t\t\t\tif (document.createdAt === undefined) {\n\t\t\t\t\tawait ctx.innerDb.patch(change.id, {\n\t\t\t\t\t\tcreatedAt: now,\n\t\t\t\t\t\tupdatedAt: now,\n\t\t\t\t\t} as never)\n\t\t\t\t}\n\t\t\t\treturn\n\t\t\t}\n\t\t\tconst previous = change.oldDoc as { updatedAt?: number }\n\t\t\tconst current = change.newDoc as { updatedAt?: number }\n\t\t\t// A write that already moved updatedAt (including our own patch)\n\t\t\t// is left alone — this is the recursion guard.\n\t\t\tif (current.updatedAt === previous.updatedAt) {\n\t\t\t\tawait ctx.innerDb.patch(change.id, { updatedAt: now } as never)\n\t\t\t}\n\t\t})\n\t}\n}\n"],"mappings":"0DAmBA,SAAgB,GAES,CACxB,OAAO,IAAI,CACZ,CAMA,SAAgB,EACf,EACA,GAAG,EACF,CACD,IAAK,IAAM,KAAS,EACnB,EAAS,SAAS,EAAO,MAAO,EAAM,IAAW,CAChD,GAAI,EAAO,YAAc,SACxB,MAAU,MAAM,GAAG,EAAM,gBAAgB,CAE3C,CAAC,CAEH,CAGA,SAAgB,EACf,EACA,GAAG,EACF,CACD,IAAK,IAAM,KAAS,EACnB,EAAS,SAAS,EAAO,MAAO,EAAM,IAAW,CAChD,GAAI,EAAO,YAAc,SACxB,MAAU,MAAM,GAAG,EAAM,wBAAwB,CAEnD,CAAC,CAEH,CASA,SAAgB,EACf,EACA,GAAG,EACF,CACD,IAAK,IAAM,KAAS,EACnB,EAAS,SAAS,EAAO,MAAO,EAAM,IAAW,CAChD,GAAI,EAAO,YAAc,SAAU,CAClC,IAAM,EAAW,EAAO,OACxB,GACC,OAAO,EAAS,QAAW,UAC3B,EAAS,OAAO,SAAW,EAE3B,MAAU,MAAM,GAAG,EAAM,uBAAuB,EAEjD,MACD,CACA,GAAI,EAAO,YAAc,SAAU,CAClC,IAAM,EAAW,EAAO,OAClB,EAAU,EAAO,OACvB,GAAI,EAAS,SAAW,EAAQ,OAC/B,MAAU,MAAM,GAAG,EAAM,uCAAuC,CAElE,CACD,CAAC,CAEH,CAQA,SAAgB,EACf,EACA,GAAG,EACF,CACD,IAAK,IAAM,KAAS,EACnB,EAAS,SAAS,EAAO,MAAO,EAAK,IAAW,CAC/C,GAAI,EAAO,YAAc,SAAU,OACnC,IAAM,EAAM,KAAK,IAAI,EACrB,GAAI,EAAO,YAAc,SAAU,CACjB,EAAO,OACX,YAAc,IAAA,IAC1B,MAAM,EAAI,QAAQ,MAAM,EAAO,GAAI,CAClC,UAAW,EACX,UAAW,CACZ,CAAU,EAEX,MACD,CACA,IAAM,EAAW,EAAO,OACR,EAAO,OAGX,YAAc,EAAS,WAClC,MAAM,EAAI,QAAQ,MAAM,EAAO,GAAI,CAAE,UAAW,CAAI,CAAU,CAEhE,CAAC,CAEH"}
1
+ {"version":3,"file":"triggers.mjs","names":[],"sources":["../src/triggers.ts"],"sourcesContent":["import { z } from 'zod'\nimport { Triggers, type Change, type Trigger } from 'convex-helpers/server/triggers'\nimport type { GenericDataModel, TableNamesInDataModel } from 'convex/server'\n\nexport { Triggers }\nexport type { Change, Trigger }\n\n/**\n * One application-wide mutation interceptor registry. Pass it to\n * createAuthFunctions({ triggers }) so every authMutation/roleMutation/\n * systemMutation runs writes through it — trigger enforcement becomes\n * structural, not a convention.\n */\nexport function createTriggers<DataModel extends GenericDataModel>(): Triggers<DataModel> {\n\treturn new Triggers<DataModel>()\n}\n\n/**\n * Marks tables as append-only evidence tables: inserts pass, every update or\n * delete throws. The ontology pattern for votes/history/audit-style tables.\n */\nexport function appendOnly<DataModel extends GenericDataModel>(\n\ttriggers: Triggers<DataModel>,\n\t...tables: readonly TableNamesInDataModel<DataModel>[]\n) {\n\tfor (const table of tables) {\n\t\ttriggers.register(table, async (_ctx, change) => {\n\t\t\tif (change.operation !== 'insert') {\n\t\t\t\tthrow new Error(`${table} is append-only`)\n\t\t\t}\n\t\t})\n\t}\n}\n\n/** Forbids hard deletes on the given tables (soft-delete discipline). */\nexport function noDelete<DataModel extends GenericDataModel>(\n\ttriggers: Triggers<DataModel>,\n\t...tables: readonly TableNamesInDataModel<DataModel>[]\n) {\n\tfor (const table of tables) {\n\t\ttriggers.register(table, async (_ctx, change) => {\n\t\t\tif (change.operation === 'delete') {\n\t\t\t\tthrow new Error(`${table} rows cannot be deleted`)\n\t\t\t}\n\t\t})\n\t}\n}\n\n/**\n * Post-image tenant invariant for tenantTable rows. Row-level security\n * authorizes a modification against the PRE-image, so ownership must also be\n * enforced against the post-image: inserts must carry a tenant, and updates\n * may never reassign it. Register for every tenant table (same registry that\n * drives createTenantRules).\n */\nexport function tenantOwnership<DataModel extends GenericDataModel>(\n\ttriggers: Triggers<DataModel>,\n\t...tables: readonly TableNamesInDataModel<DataModel>[]\n) {\n\tfor (const table of tables) {\n\t\ttriggers.register(table, async (_ctx, change) => {\n\t\t\tif (change.operation === 'insert') {\n\t\t\t\tconst ownership = tenantDocument.safeParse(change.newDoc)\n\t\t\t\tif (!ownership.success) {\n\t\t\t\t\tthrow new Error(`${table} rows require a tenant`)\n\t\t\t\t}\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif (change.operation === 'update') {\n\t\t\t\tconst previous = change.oldDoc\n\t\t\t\tconst current = change.newDoc\n\t\t\t\tif (previous.tenant !== current.tenant) {\n\t\t\t\t\tthrow new Error(`${table} tenant ownership cannot be reassigned`)\n\t\t\t\t}\n\t\t\t}\n\t\t})\n\t}\n}\n\n/**\n * Maintains zod-table's opinionated lifecycle timestamps: createdAt and\n * updatedAt on insert, updatedAt on every update. archivedAt stays under\n * application control. Register once per table; writes made through the\n * auth constructors keep these fields correct automatically.\n */\nexport function timestamps<DataModel extends GenericDataModel>(\n\ttriggers: Triggers<DataModel>,\n\t...tables: readonly TableNamesInDataModel<DataModel>[]\n) {\n\tfor (const table of tables) {\n\t\ttriggers.register(table, async (ctx, change) => {\n\t\t\tif (change.operation === 'delete') return\n\t\t\tconst now = Date.now()\n\t\t\tif (change.operation === 'insert') {\n\t\t\t\tconst document = change.newDoc\n\t\t\t\tif (document.createdAt === undefined) {\n\t\t\t\t\t// SAFETY: registration requires zod-table lifecycle fields; Convex validates this numeric timestamp patch.\n\t\t\t\t\tawait ctx.innerDb.patch(change.id, {\n\t\t\t\t\t\tcreatedAt: now,\n\t\t\t\t\t\tupdatedAt: now,\n\t\t\t\t\t} as never)\n\t\t\t\t}\n\t\t\t\treturn\n\t\t\t}\n\t\t\tconst previous = change.oldDoc\n\t\t\tconst current = change.newDoc\n\t\t\t// A write that already moved updatedAt (including our own patch)\n\t\t\t// is left alone — this is the recursion guard.\n\t\t\tif (current.updatedAt === previous.updatedAt) {\n\t\t\t\t// SAFETY: registration requires zod-table lifecycle fields; Convex validates this numeric timestamp patch.\n\t\t\t\tawait ctx.innerDb.patch(change.id, { updatedAt: now } as never)\n\t\t\t}\n\t\t})\n\t}\n}\n\nconst tenantDocument = z.object({ tenant: z.string().min(1) })\n"],"mappings":"kFAaA,SAAgB,GAA0E,CACzF,OAAO,IAAI,CACZ,CAMA,SAAgB,EACf,EACA,GAAG,EACF,CACD,IAAK,IAAM,KAAS,EACnB,EAAS,SAAS,EAAO,MAAO,EAAM,IAAW,CAChD,GAAI,EAAO,YAAc,SACxB,MAAU,MAAM,GAAG,EAAM,gBAAgB,CAE3C,CAAC,CAEH,CAGA,SAAgB,EACf,EACA,GAAG,EACF,CACD,IAAK,IAAM,KAAS,EACnB,EAAS,SAAS,EAAO,MAAO,EAAM,IAAW,CAChD,GAAI,EAAO,YAAc,SACxB,MAAU,MAAM,GAAG,EAAM,wBAAwB,CAEnD,CAAC,CAEH,CASA,SAAgB,EACf,EACA,GAAG,EACF,CACD,IAAK,IAAM,KAAS,EACnB,EAAS,SAAS,EAAO,MAAO,EAAM,IAAW,CAChD,GAAI,EAAO,YAAc,SAAU,CAElC,GAAI,CADc,EAAe,UAAU,EAAO,MACrC,CAAC,CAAC,QACd,MAAU,MAAM,GAAG,EAAM,uBAAuB,EAEjD,MACD,CACA,GAAI,EAAO,YAAc,SAAU,CAClC,IAAM,EAAW,EAAO,OAClB,EAAU,EAAO,OACvB,GAAI,EAAS,SAAW,EAAQ,OAC/B,MAAU,MAAM,GAAG,EAAM,uCAAuC,CAElE,CACD,CAAC,CAEH,CAQA,SAAgB,EACf,EACA,GAAG,EACF,CACD,IAAK,IAAM,KAAS,EACnB,EAAS,SAAS,EAAO,MAAO,EAAK,IAAW,CAC/C,GAAI,EAAO,YAAc,SAAU,OACnC,IAAM,EAAM,KAAK,IAAI,EACrB,GAAI,EAAO,YAAc,SAAU,CACjB,EAAO,OACX,YAAc,IAAA,IAE1B,MAAM,EAAI,QAAQ,MAAM,EAAO,GAAI,CAClC,UAAW,EACX,UAAW,CACZ,CAAU,EAEX,MACD,CACA,IAAM,EAAW,EAAO,OACR,EAAO,OAGX,YAAc,EAAS,WAElC,MAAM,EAAI,QAAQ,MAAM,EAAO,GAAI,CAAE,UAAW,CAAI,CAAU,CAEhE,CAAC,CAEH,CAEA,MAAM,EAAiB,EAAE,OAAO,CAAE,OAAQ,EAAE,OAAO,CAAC,CAAC,IAAI,CAAC,CAAE,CAAC"}