@classytic/repo-core 0.2.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (87) hide show
  1. package/CHANGELOG.md +363 -0
  2. package/README.md +28 -7
  3. package/dist/adapter/index.d.mts +3 -0
  4. package/dist/adapter/index.mjs +2 -0
  5. package/dist/adapter/types.d.mts +222 -0
  6. package/dist/adapter/widen.d.mts +22 -0
  7. package/dist/adapter/widen.mjs +26 -0
  8. package/dist/aggregate/index.d.mts +3 -0
  9. package/dist/aggregate/index.mjs +3 -0
  10. package/dist/aggregate/keyset.d.mts +57 -0
  11. package/dist/aggregate/keyset.mjs +45 -0
  12. package/dist/aggregate/normalize.d.mts +24 -0
  13. package/dist/aggregate/normalize.mjs +28 -0
  14. package/dist/better-auth/index.d.mts +110 -0
  15. package/dist/better-auth/index.mjs +71 -0
  16. package/dist/cache/engine.d.mts +127 -0
  17. package/dist/cache/engine.mjs +235 -0
  18. package/dist/cache/envelope.mjs +32 -0
  19. package/dist/cache/index.d.mts +7 -2
  20. package/dist/cache/index.mjs +6 -2
  21. package/dist/cache/keys.mjs +131 -0
  22. package/dist/cache/memory-adapter.mjs +41 -7
  23. package/dist/cache/options.d.mts +112 -0
  24. package/dist/cache/options.mjs +25 -0
  25. package/dist/cache/plugin/context.d.mts +18 -0
  26. package/dist/cache/plugin/context.mjs +121 -0
  27. package/dist/cache/plugin/index.d.mts +86 -0
  28. package/dist/cache/plugin/index.mjs +78 -0
  29. package/dist/cache/plugin/invalidation-hooks.mjs +35 -0
  30. package/dist/cache/plugin/read-hooks.mjs +96 -0
  31. package/dist/cache/plugin/swr.mjs +20 -0
  32. package/dist/cache/runtime.d.mts +43 -0
  33. package/dist/cache/runtime.mjs +14 -0
  34. package/dist/cache/tag-index.mjs +84 -0
  35. package/dist/cache/timeout-adapter.d.mts +30 -0
  36. package/dist/cache/timeout-adapter.mjs +58 -0
  37. package/dist/cache/types.d.mts +45 -0
  38. package/dist/cache/version-store.mjs +57 -0
  39. package/dist/errors/contract.d.mts +37 -0
  40. package/dist/errors/contract.mjs +75 -0
  41. package/dist/errors/index.d.mts +4 -2
  42. package/dist/errors/index.mjs +4 -1
  43. package/dist/errors/schema.d.mts +101 -0
  44. package/dist/errors/schema.mjs +78 -0
  45. package/dist/errors/types.d.mts +113 -8
  46. package/dist/errors/types.mjs +29 -0
  47. package/dist/filter/match.mjs +38 -2
  48. package/dist/pagination/canonical.d.mts +35 -0
  49. package/dist/pagination/canonical.mjs +26 -0
  50. package/dist/pagination/cursor.mjs +4 -1
  51. package/dist/pagination/index.d.mts +3 -2
  52. package/dist/pagination/index.mjs +2 -1
  53. package/dist/pagination/types.d.mts +57 -3
  54. package/dist/plugins/index.d.mts +2 -0
  55. package/dist/plugins/index.mjs +2 -0
  56. package/dist/plugins/tenant-helpers.d.mts +63 -0
  57. package/dist/plugins/tenant-helpers.mjs +84 -0
  58. package/dist/query-parser/index.d.mts +2 -1
  59. package/dist/query-parser/index.mjs +2 -1
  60. package/dist/query-parser/parse-url.mjs +13 -11
  61. package/dist/query-parser/reserved.d.mts +43 -0
  62. package/dist/query-parser/reserved.mjs +56 -0
  63. package/dist/repository/agg-output.d.mts +63 -0
  64. package/dist/repository/agg-output.mjs +89 -0
  65. package/dist/repository/base.mjs +21 -0
  66. package/dist/repository/index.d.mts +4 -2
  67. package/dist/repository/index.mjs +3 -1
  68. package/dist/repository/options.d.mts +62 -0
  69. package/dist/repository/options.mjs +57 -0
  70. package/dist/repository/types.d.mts +935 -48
  71. package/dist/schema/field-rules.d.mts +60 -9
  72. package/dist/schema/field-rules.mjs +121 -10
  73. package/dist/schema/generator.d.mts +72 -0
  74. package/dist/schema/generator.mjs +16 -0
  75. package/dist/schema/index.d.mts +3 -2
  76. package/dist/schema/index.mjs +3 -2
  77. package/dist/schema/types.d.mts +77 -3
  78. package/dist/tenant/index.d.mts +3 -0
  79. package/dist/tenant/index.mjs +2 -0
  80. package/dist/tenant/resolve.d.mts +27 -0
  81. package/dist/tenant/resolve.mjs +69 -0
  82. package/dist/tenant/types.d.mts +142 -0
  83. package/dist/testing/conformance.mjs +666 -17
  84. package/dist/testing/index.d.mts +2 -2
  85. package/dist/testing/types.d.mts +99 -2
  86. package/package.json +27 -1
  87. package/dist/cache/stable-stringify.d.mts +0 -15
@@ -4,17 +4,28 @@ import { JsonSchema, SchemaBuilderOptions, ValidationResult } from "./types.mjs"
4
4
  /**
5
5
  * Collect the set of fields that must NOT appear in a generated schema.
6
6
  *
7
- * Combines four sources in priority order:
8
- * 1. Always-hidden system fields (`createdAt`, `updatedAt`, `__v`).
9
- * 2. `fieldRules[field].systemManaged` hidden from both create & update.
10
- * 3. For update schemas: `fieldRules[field].immutable` /
11
- * `immutableAfterCreate` → hidden from update only.
12
- * 4. `options.create.omitFields` / `options.update.omitFields` — explicit
13
- * caller-provided omit list for the matching purpose.
7
+ * Three purposes have three different policies:
8
+ *
9
+ * - `'create'` / `'update'` (request-body schemas):
10
+ * 1. Always-hidden system fields (`createdAt`, `updatedAt`, `__v`).
11
+ * 2. `fieldRules[field].systemManaged` → hidden from both.
12
+ * 3. `'update'` only: `fieldRules[field].immutable` /
13
+ * `immutableAfterCreate` hidden from update.
14
+ * 4. `options.create.omitFields` / `options.update.omitFields` —
15
+ * explicit caller-provided omit list for the matching purpose.
16
+ *
17
+ * - `'response'` (response-shape schema):
18
+ * 1. `fieldRules[field].hidden: true` ONLY — passwords, secrets,
19
+ * internal scoring. Server-set fields (`createdAt`, `updatedAt`,
20
+ * `_id`, systemManaged, immutable / readonly) ARE returned to
21
+ * clients and so ARE included in the response shape.
22
+ * 2. `options.response?.omitFields` — explicit caller-provided omit
23
+ * list when the host wants to strip extra fields from responses
24
+ * without marking them `hidden` globally.
14
25
  *
15
26
  * Returns a fresh `Set<string>` so callers can freely mutate.
16
27
  */
17
- declare function collectFieldsToOmit(options: SchemaBuilderOptions, purpose: 'create' | 'update'): Set<string>;
28
+ declare function collectFieldsToOmit(options: SchemaBuilderOptions, purpose: 'create' | 'update' | 'response'): Set<string>;
18
29
  /**
19
30
  * Apply omissions + `optional` overrides to a built JSON Schema in place.
20
31
  *
@@ -58,5 +69,45 @@ declare function isFieldUpdateAllowed(fieldName: string, options?: SchemaBuilder
58
69
  * walking the rules themselves.
59
70
  */
60
71
  declare function validateUpdateBody(body?: Record<string, unknown>, options?: SchemaBuilderOptions): ValidationResult;
72
+ /**
73
+ * Merge constraint-style `fieldRules` into a generated schema bag in place.
74
+ *
75
+ * Operates on the three slots that carry property maps — `createBody`,
76
+ * `updateBody`, `response`. `listQuery` and `params` are skipped (their
77
+ * constraint vocabulary is owned by the kit's query parser).
78
+ *
79
+ * Existing constraints on a property always win — the merge only fills in
80
+ * gaps. Kits that already walk `fieldRules` during base-schema assembly
81
+ * can call this helper for free (the checks are no-ops when constraints
82
+ * already exist).
83
+ *
84
+ * Schema slot type is intentionally loose (`Record<string, unknown> | null
85
+ * | undefined`) so adapters can pass either an `OpenApiSchemas`-shaped bag
86
+ * or a kit-native bag without coercion.
87
+ */
88
+ declare function mergeFieldRuleConstraints(schemas: Record<string, unknown> | null | undefined, schemaOptions?: SchemaBuilderOptions): void;
89
+ /**
90
+ * Widen a JSON Schema property to also accept `null`.
91
+ *
92
+ * Handles the three ways a property can be typed:
93
+ * - `type: 'string'` → `type: ['string', 'null']`
94
+ * - `type: [...]` → append `'null'` if missing
95
+ * - `anyOf: [...]` → append `{ type: 'null' }` branch if missing
96
+ *
97
+ * **Enum interaction:** when the widened prop also carries `enum: [...]`,
98
+ * `null` is appended to the enum list too. AJV's `enum` keyword rejects
99
+ * values not in the list regardless of the widened `type`, so
100
+ * `{ type: ['string','null'], enum: ['a','b'] }` alone would still reject
101
+ * `null`. The fix is `enum: ['a','b', null]`. (The `anyOf` branch dodges
102
+ * this entirely — each branch scopes its own enum.)
103
+ *
104
+ * No-op when the schema already admits null (don't double-wrap) or has
105
+ * no `type` / `anyOf` anchor to widen (e.g. Mixed — already accepts null).
106
+ *
107
+ * Mutates in place — callers already treat the slot schema as owned.
108
+ * Exported so adapters that walk `fieldRules` inline can reuse the same
109
+ * widening logic.
110
+ */
111
+ declare function applyNullable(prop: Record<string, unknown>): void;
61
112
  //#endregion
62
- export { applyFieldRules, collectFieldsToOmit, getImmutableFields, getSystemManagedFields, isFieldUpdateAllowed, validateUpdateBody };
113
+ export { applyFieldRules, applyNullable, collectFieldsToOmit, getImmutableFields, getSystemManagedFields, isFieldUpdateAllowed, mergeFieldRuleConstraints, validateUpdateBody };
@@ -2,23 +2,43 @@
2
2
  /**
3
3
  * Collect the set of fields that must NOT appear in a generated schema.
4
4
  *
5
- * Combines four sources in priority order:
6
- * 1. Always-hidden system fields (`createdAt`, `updatedAt`, `__v`).
7
- * 2. `fieldRules[field].systemManaged` hidden from both create & update.
8
- * 3. For update schemas: `fieldRules[field].immutable` /
9
- * `immutableAfterCreate` → hidden from update only.
10
- * 4. `options.create.omitFields` / `options.update.omitFields` — explicit
11
- * caller-provided omit list for the matching purpose.
5
+ * Three purposes have three different policies:
6
+ *
7
+ * - `'create'` / `'update'` (request-body schemas):
8
+ * 1. Always-hidden system fields (`createdAt`, `updatedAt`, `__v`).
9
+ * 2. `fieldRules[field].systemManaged` → hidden from both.
10
+ * 3. `'update'` only: `fieldRules[field].immutable` /
11
+ * `immutableAfterCreate` hidden from update.
12
+ * 4. `options.create.omitFields` / `options.update.omitFields` —
13
+ * explicit caller-provided omit list for the matching purpose.
14
+ *
15
+ * - `'response'` (response-shape schema):
16
+ * 1. `fieldRules[field].hidden: true` ONLY — passwords, secrets,
17
+ * internal scoring. Server-set fields (`createdAt`, `updatedAt`,
18
+ * `_id`, systemManaged, immutable / readonly) ARE returned to
19
+ * clients and so ARE included in the response shape.
20
+ * 2. `options.response?.omitFields` — explicit caller-provided omit
21
+ * list when the host wants to strip extra fields from responses
22
+ * without marking them `hidden` globally.
12
23
  *
13
24
  * Returns a fresh `Set<string>` so callers can freely mutate.
14
25
  */
15
26
  function collectFieldsToOmit(options, purpose) {
27
+ const rules = options?.fieldRules ?? {};
28
+ const globalExcludes = options?.excludeFields ?? [];
29
+ if (purpose === "response") {
30
+ const result = new Set(globalExcludes);
31
+ for (const [field, rule] of Object.entries(rules)) if (rule.hidden) result.add(field);
32
+ const explicit = options?.response?.omitFields;
33
+ if (explicit) for (const f of explicit) result.add(f);
34
+ return result;
35
+ }
16
36
  const result = new Set([
17
37
  "createdAt",
18
38
  "updatedAt",
19
- "__v"
39
+ "__v",
40
+ ...globalExcludes
20
41
  ]);
21
- const rules = options?.fieldRules ?? {};
22
42
  for (const [field, rule] of Object.entries(rules)) {
23
43
  if (rule.systemManaged) result.add(field);
24
44
  if (purpose === "update" && (rule.immutable || rule.immutableAfterCreate)) result.add(field);
@@ -106,5 +126,96 @@ function validateUpdateBody(body = {}, options = {}) {
106
126
  violations
107
127
  };
108
128
  }
129
+ /**
130
+ * Merge constraint-style `fieldRules` into a generated schema bag in place.
131
+ *
132
+ * Operates on the three slots that carry property maps — `createBody`,
133
+ * `updateBody`, `response`. `listQuery` and `params` are skipped (their
134
+ * constraint vocabulary is owned by the kit's query parser).
135
+ *
136
+ * Existing constraints on a property always win — the merge only fills in
137
+ * gaps. Kits that already walk `fieldRules` during base-schema assembly
138
+ * can call this helper for free (the checks are no-ops when constraints
139
+ * already exist).
140
+ *
141
+ * Schema slot type is intentionally loose (`Record<string, unknown> | null
142
+ * | undefined`) so adapters can pass either an `OpenApiSchemas`-shaped bag
143
+ * or a kit-native bag without coercion.
144
+ */
145
+ function mergeFieldRuleConstraints(schemas, schemaOptions) {
146
+ if (!schemas || typeof schemas !== "object") return;
147
+ const rules = schemaOptions?.fieldRules;
148
+ if (!rules || Object.keys(rules).length === 0) return;
149
+ for (const slot of [
150
+ "createBody",
151
+ "updateBody",
152
+ "response"
153
+ ]) {
154
+ const slotSchema = schemas[slot];
155
+ if (!slotSchema || typeof slotSchema !== "object") continue;
156
+ const properties = slotSchema["properties"];
157
+ if (!properties) continue;
158
+ for (const [field, rule] of Object.entries(rules)) {
159
+ const prop = properties[field];
160
+ if (!prop || typeof prop !== "object") continue;
161
+ if (rule.minLength != null && prop["minLength"] == null) prop["minLength"] = rule.minLength;
162
+ if (rule.maxLength != null && prop["maxLength"] == null) prop["maxLength"] = rule.maxLength;
163
+ if (rule.min != null && prop["minimum"] == null) prop["minimum"] = rule.min;
164
+ if (rule.max != null && prop["maximum"] == null) prop["maximum"] = rule.max;
165
+ if (rule.pattern != null && prop["pattern"] == null) prop["pattern"] = rule.pattern;
166
+ if (rule.enum != null && prop["enum"] == null) prop["enum"] = rule.enum;
167
+ if (rule.description != null && prop["description"] == null) prop["description"] = rule.description;
168
+ if (rule.nullable === true) applyNullable(prop);
169
+ }
170
+ }
171
+ }
172
+ /**
173
+ * Widen a JSON Schema property to also accept `null`.
174
+ *
175
+ * Handles the three ways a property can be typed:
176
+ * - `type: 'string'` → `type: ['string', 'null']`
177
+ * - `type: [...]` → append `'null'` if missing
178
+ * - `anyOf: [...]` → append `{ type: 'null' }` branch if missing
179
+ *
180
+ * **Enum interaction:** when the widened prop also carries `enum: [...]`,
181
+ * `null` is appended to the enum list too. AJV's `enum` keyword rejects
182
+ * values not in the list regardless of the widened `type`, so
183
+ * `{ type: ['string','null'], enum: ['a','b'] }` alone would still reject
184
+ * `null`. The fix is `enum: ['a','b', null]`. (The `anyOf` branch dodges
185
+ * this entirely — each branch scopes its own enum.)
186
+ *
187
+ * No-op when the schema already admits null (don't double-wrap) or has
188
+ * no `type` / `anyOf` anchor to widen (e.g. Mixed — already accepts null).
189
+ *
190
+ * Mutates in place — callers already treat the slot schema as owned.
191
+ * Exported so adapters that walk `fieldRules` inline can reuse the same
192
+ * widening logic.
193
+ */
194
+ function applyNullable(prop) {
195
+ if (Array.isArray(prop["anyOf"])) {
196
+ if (!prop["anyOf"].some((b) => b !== null && typeof b === "object" && (b["type"] === "null" || b["const"] === null))) prop["anyOf"].push({ type: "null" });
197
+ return;
198
+ }
199
+ if (Array.isArray(prop["type"])) {
200
+ if (!prop["type"].includes("null")) prop["type"].push("null");
201
+ widenEnumToIncludeNull(prop);
202
+ return;
203
+ }
204
+ if (typeof prop["type"] === "string") {
205
+ prop["type"] = [prop["type"], "null"];
206
+ widenEnumToIncludeNull(prop);
207
+ return;
208
+ }
209
+ }
210
+ /**
211
+ * Append `null` to `enum` when present. Required because AJV's `enum`
212
+ * keyword is independent of `type` — a value must appear in the enum
213
+ * array verbatim even if the widened type says null is allowed.
214
+ */
215
+ function widenEnumToIncludeNull(prop) {
216
+ if (!Array.isArray(prop["enum"])) return;
217
+ if (prop["enum"].includes(null)) return;
218
+ prop["enum"] = [...prop["enum"], null];
219
+ }
109
220
  //#endregion
110
- export { applyFieldRules, collectFieldsToOmit, getImmutableFields, getSystemManagedFields, isFieldUpdateAllowed, validateUpdateBody };
221
+ export { applyFieldRules, applyNullable, collectFieldsToOmit, getImmutableFields, getSystemManagedFields, isFieldUpdateAllowed, mergeFieldRuleConstraints, validateUpdateBody };
@@ -0,0 +1,72 @@
1
+ import { CrudSchemas, SchemaBuilderOptions } from "./types.mjs";
2
+
3
+ //#region src/schema/generator.d.ts
4
+ /**
5
+ * Resource-level context threaded into the generator at boot. Lets the
6
+ * generator shape output to per-resource config (idField pattern,
7
+ * resource name for OpenAPI titles).
8
+ *
9
+ * All fields optional — generators that ignore the context still produce
10
+ * valid schemas; arc applies safety-net normalization downstream.
11
+ */
12
+ interface SchemaGeneratorContext {
13
+ /**
14
+ * The `idField` configured on the resource. Defaults to `'_id'` for
15
+ * Mongoose-shaped kits, `'id'` for SQL kits. Generators emit the
16
+ * matching `params.properties[idField]` so route-param validation
17
+ * matches the actual lookup field.
18
+ */
19
+ idField?: string;
20
+ /** Resource name (for OpenAPI titles, generator log messages). */
21
+ resourceName?: string;
22
+ }
23
+ /**
24
+ * Canonical generator contract. Functions that produce CRUD JSON schemas
25
+ * for a kit satisfy this shape — `mongokit/buildCrudSchemasFromModel`,
26
+ * `sqlitekit/buildCrudSchemasFromTable`, etc.
27
+ *
28
+ * The return type is intentionally widened to `CrudSchemas | Record<string,
29
+ * unknown>` so kits that emit additional vendor-specific schema fields
30
+ * (`x-ref`, `x-foreign-key`, OpenAPI extensions) flow through without
31
+ * type erosion. Arc's adapter post-processes via `mergeFieldRuleConstraints`
32
+ * so portable `fieldRules` constraints (`minLength`/`maxLength`/`min`/
33
+ * `max`/`pattern`/`enum`/`description`/`nullable`) apply uniformly across
34
+ * kit outputs.
35
+ *
36
+ * @typeParam TModel - The kit's native model / table type. `Model<unknown>`
37
+ * for Mongoose kits, a Drizzle `Table` for SQL kits, etc. Widened to
38
+ * `unknown` by default so adapters that don't care about model typing
39
+ * (or cross-kit utilities) pass any model through.
40
+ *
41
+ * @example mongokit conformance (one-line `satisfies`)
42
+ * ```ts
43
+ * import type { SchemaGenerator } from '@classytic/repo-core/schema';
44
+ *
45
+ * export const buildCrudSchemasFromModel = ((model, options, ctx) => {
46
+ * // ... existing impl
47
+ * }) satisfies SchemaGenerator<Model<unknown>>;
48
+ * ```
49
+ *
50
+ * @example arc adapter typing
51
+ * ```ts
52
+ * import type { SchemaGenerator } from '@classytic/repo-core/schema';
53
+ *
54
+ * interface MongooseAdapterOptions<TDoc> {
55
+ * schemaGenerator?: SchemaGenerator<Model<unknown>>;
56
+ * }
57
+ * ```
58
+ */
59
+ type SchemaGenerator<TModel = unknown> = (model: TModel, options?: SchemaBuilderOptions, context?: SchemaGeneratorContext) => CrudSchemas | Record<string, unknown>;
60
+ /**
61
+ * Runtime predicate — true when `value` matches the generator shape.
62
+ *
63
+ * Conservative: only checks `typeof value === 'function'` and arity.
64
+ * Doesn't invoke the function with a sentinel argument because doing so
65
+ * could trigger expensive schema introspection on a single test call.
66
+ * The structural-typing alignment (`satisfies SchemaGenerator<...>`) is
67
+ * the primary contract enforcement; this guard is for runtime hosts that
68
+ * accept either a generator or a config-bag.
69
+ */
70
+ declare function isSchemaGenerator(value: unknown): value is SchemaGenerator;
71
+ //#endregion
72
+ export { SchemaGenerator, SchemaGeneratorContext, isSchemaGenerator };
@@ -0,0 +1,16 @@
1
+ //#region src/schema/generator.ts
2
+ /**
3
+ * Runtime predicate — true when `value` matches the generator shape.
4
+ *
5
+ * Conservative: only checks `typeof value === 'function'` and arity.
6
+ * Doesn't invoke the function with a sentinel argument because doing so
7
+ * could trigger expensive schema introspection on a single test call.
8
+ * The structural-typing alignment (`satisfies SchemaGenerator<...>`) is
9
+ * the primary contract enforcement; this guard is for runtime hosts that
10
+ * accept either a generator or a config-bag.
11
+ */
12
+ function isSchemaGenerator(value) {
13
+ return typeof value === "function" && value.length >= 1 && value.length <= 3;
14
+ }
15
+ //#endregion
16
+ export { isSchemaGenerator };
@@ -1,3 +1,4 @@
1
1
  import { CrudSchemas, FieldRule, FieldRules, JsonSchema, SchemaBuilderOptions, ValidationResult } from "./types.mjs";
2
- import { applyFieldRules, collectFieldsToOmit, getImmutableFields, getSystemManagedFields, isFieldUpdateAllowed, validateUpdateBody } from "./field-rules.mjs";
3
- export { type CrudSchemas, type FieldRule, type FieldRules, type JsonSchema, type SchemaBuilderOptions, type ValidationResult, applyFieldRules, collectFieldsToOmit, getImmutableFields, getSystemManagedFields, isFieldUpdateAllowed, validateUpdateBody };
2
+ import { applyFieldRules, applyNullable, collectFieldsToOmit, getImmutableFields, getSystemManagedFields, isFieldUpdateAllowed, mergeFieldRuleConstraints, validateUpdateBody } from "./field-rules.mjs";
3
+ import { SchemaGenerator, SchemaGeneratorContext, isSchemaGenerator } from "./generator.mjs";
4
+ export { type CrudSchemas, type FieldRule, type FieldRules, type JsonSchema, type SchemaBuilderOptions, type SchemaGenerator, type SchemaGeneratorContext, type ValidationResult, applyFieldRules, applyNullable, collectFieldsToOmit, getImmutableFields, getSystemManagedFields, isFieldUpdateAllowed, isSchemaGenerator, mergeFieldRuleConstraints, validateUpdateBody };
@@ -1,2 +1,3 @@
1
- import { applyFieldRules, collectFieldsToOmit, getImmutableFields, getSystemManagedFields, isFieldUpdateAllowed, validateUpdateBody } from "./field-rules.mjs";
2
- export { applyFieldRules, collectFieldsToOmit, getImmutableFields, getSystemManagedFields, isFieldUpdateAllowed, validateUpdateBody };
1
+ import { applyFieldRules, applyNullable, collectFieldsToOmit, getImmutableFields, getSystemManagedFields, isFieldUpdateAllowed, mergeFieldRuleConstraints, validateUpdateBody } from "./field-rules.mjs";
2
+ import { isSchemaGenerator } from "./generator.mjs";
3
+ export { applyFieldRules, applyNullable, collectFieldsToOmit, getImmutableFields, getSystemManagedFields, isFieldUpdateAllowed, isSchemaGenerator, mergeFieldRuleConstraints, validateUpdateBody };
@@ -26,6 +26,37 @@ interface FieldRule {
26
26
  systemManaged?: boolean;
27
27
  /** Remove from `required[]` in the generated schema. DB-level constraints unaffected. */
28
28
  optional?: boolean;
29
+ /**
30
+ * Strip the field from the response shape. Use for passwords, secrets,
31
+ * internal scoring — anything the server stores but should never echo.
32
+ *
33
+ * Distinct from `systemManaged` (which only affects request bodies):
34
+ * `hidden` is a *response* concern and lives at the schema-builder
35
+ * boundary so kits, OpenAPI tooling, and arc's response serializer
36
+ * narrow on the same flag.
37
+ */
38
+ hidden?: boolean;
39
+ /** String minimum length — JSON Schema `minLength`. */
40
+ minLength?: number;
41
+ /** String maximum length — JSON Schema `maxLength`. */
42
+ maxLength?: number;
43
+ /** Number minimum — JSON Schema `minimum`. */
44
+ min?: number;
45
+ /** Number maximum — JSON Schema `maximum`. */
46
+ max?: number;
47
+ /** Regex pattern — JSON Schema `pattern`. */
48
+ pattern?: string;
49
+ /** Allowed values — JSON Schema `enum`. */
50
+ enum?: ReadonlyArray<string | number>;
51
+ /**
52
+ * Widen the property type to also accept `null`. Implemented as a
53
+ * draft-07 type tuple (`type: ['string','null']`) or anyOf branch
54
+ * (`anyOf: [..., { type: 'null' }]`). Existing nullability is
55
+ * preserved — never double-widens.
56
+ */
57
+ nullable?: boolean;
58
+ /** Human-readable description — JSON Schema `description`. */
59
+ description?: string;
29
60
  }
30
61
  /** Map of field name → FieldRule. */
31
62
  interface FieldRules {
@@ -56,9 +87,10 @@ interface JsonSchema {
56
87
  [key: `x-${string}`]: unknown;
57
88
  }
58
89
  /**
59
- * CRUD schema bundle — the four JSON Schemas every HTTP endpoint needs:
60
- * body validation on POST / PATCH, route-param validation on id routes, and
61
- * query-string validation on list endpoints.
90
+ * CRUD schema bundle — the JSON Schemas every HTTP endpoint needs:
91
+ * body validation on POST / PATCH, route-param validation on id routes,
92
+ * query-string validation on list endpoints, and (optionally) response-shape
93
+ * documentation for OpenAPI / strict reply serialization.
62
94
  */
63
95
  interface CrudSchemas {
64
96
  /** JSON Schema for create request body (POST). */
@@ -69,6 +101,23 @@ interface CrudSchemas {
69
101
  params: JsonSchema;
70
102
  /** JSON Schema for list/query parameters. */
71
103
  listQuery: JsonSchema;
104
+ /**
105
+ * JSON Schema for response shape (optional).
106
+ *
107
+ * Includes every field a client receives — server-set fields
108
+ * (`createdAt`, `updatedAt`, `_id`, immutable / readonly fields) ARE
109
+ * returned to clients and so ARE included in the response shape, in
110
+ * contrast to `createBody` / `updateBody` which exclude them. Only
111
+ * `fieldRules[field].hidden: true` excludes a field from responses
112
+ * (passwords, secrets, internal scoring).
113
+ *
114
+ * Set `additionalProperties: true` so virtuals and computed fields
115
+ * pass through without being stripped by AJV's strict serialization.
116
+ *
117
+ * Optional — kits that don't ship a response builder leave it unset
118
+ * and arc treats response validation as opt-out for that resource.
119
+ */
120
+ response?: JsonSchema;
72
121
  }
73
122
  /**
74
123
  * Options consumed by every kit's schema builder. Fields are additive:
@@ -78,6 +127,18 @@ interface CrudSchemas {
78
127
  interface SchemaBuilderOptions {
79
128
  /** Field rules for create/update schemas. */
80
129
  fieldRules?: FieldRules;
130
+ /**
131
+ * Global field exclusion — fields listed here are dropped from EVERY
132
+ * generated schema (create / update / response). Shortcut for setting
133
+ * `create.omitFields`, `update.omitFields`, AND `response.omitFields`
134
+ * to the same list. Use for fields that should never appear in any
135
+ * HTTP-facing schema (e.g. internal-only columns, framework-private
136
+ * fields).
137
+ *
138
+ * Per-purpose overrides still apply on top — a field listed here AND
139
+ * in `create.omitFields` is dropped once.
140
+ */
141
+ excludeFields?: string[];
81
142
  /**
82
143
  * When `true`, emit `"additionalProperties": false` on create/update/query
83
144
  * schemas. Default `false` so generators stay permissive by default;
@@ -113,6 +174,19 @@ interface SchemaBuilderOptions {
113
174
  type: string;
114
175
  } | unknown>;
115
176
  };
177
+ /**
178
+ * Response-schema overrides.
179
+ *
180
+ * Response shape includes server-set fields (`createdAt`, `updatedAt`,
181
+ * `_id`, immutable / readonly / systemManaged fields) since those ARE
182
+ * returned to clients. Only `fieldRules[field].hidden: true` fields are
183
+ * stripped automatically. Use `omitFields` to drop additional fields
184
+ * from responses without marking them globally hidden (e.g. internal
185
+ * scoring you want kept in update bodies but stripped from list reads).
186
+ */
187
+ response?: {
188
+ /** Extra fields to omit from the response shape. */omitFields?: string[];
189
+ };
116
190
  /**
117
191
  * Emit OpenAPI vendor extensions (`x-*` keywords like `x-ref` for populated
118
192
  * foreign-key fields).
@@ -0,0 +1,3 @@
1
+ import { ResolvedTenantConfig, TenantConfig, TenantFieldType, TenantStrategy } from "./types.mjs";
2
+ import { DEFAULT_TENANT_CONFIG, resolveTenantConfig } from "./resolve.mjs";
3
+ export { DEFAULT_TENANT_CONFIG, type ResolvedTenantConfig, type TenantConfig, type TenantFieldType, type TenantStrategy, resolveTenantConfig };
@@ -0,0 +1,2 @@
1
+ import { DEFAULT_TENANT_CONFIG, resolveTenantConfig } from "./resolve.mjs";
2
+ export { DEFAULT_TENANT_CONFIG, resolveTenantConfig };
@@ -0,0 +1,27 @@
1
+ import { ResolvedTenantConfig, TenantConfig } from "./types.mjs";
2
+
3
+ //#region src/tenant/resolve.d.ts
4
+ /**
5
+ * Sensible defaults for a freshly-built package (field strategy).
6
+ *
7
+ * `fieldType: 'objectId'` is the recommended default for new Mongo-shaped
8
+ * kits because it enables `$lookup` / `.populate()`. Existing kits that
9
+ * historically defaulted to `'string'` (mongokit pre-3.x) keep their own
10
+ * runtime default — `Pick<TenantConfig, 'fieldType'>` extension preserves
11
+ * type-level alignment without forcing a runtime default change.
12
+ */
13
+ declare const DEFAULT_TENANT_CONFIG: Required<Pick<TenantConfig, 'strategy' | 'enabled' | 'tenantField' | 'fieldType' | 'ref' | 'contextKey' | 'required'>>;
14
+ /**
15
+ * Resolve a possibly-partial {@link TenantConfig} against the defaults.
16
+ *
17
+ * - `false` → `enabled: false`, `strategy: 'none'`, `required: false`.
18
+ * - `true` / `undefined` → default field strategy.
19
+ * - Object with `strategy: 'custom'` → `resolve` is required; throws
20
+ * otherwise so the misconfiguration surfaces at boot, not runtime.
21
+ * - Object with `strategy: 'none'` → `enabled: false` (preserves
22
+ * user-supplied `tenantField` / `fieldType` / `ref` so the doc field
23
+ * stays correctly typed even with scoping off).
24
+ */
25
+ declare function resolveTenantConfig(config?: TenantConfig | boolean): ResolvedTenantConfig;
26
+ //#endregion
27
+ export { DEFAULT_TENANT_CONFIG, resolveTenantConfig };
@@ -0,0 +1,69 @@
1
+ //#region src/tenant/resolve.ts
2
+ /**
3
+ * Sensible defaults for a freshly-built package (field strategy).
4
+ *
5
+ * `fieldType: 'objectId'` is the recommended default for new Mongo-shaped
6
+ * kits because it enables `$lookup` / `.populate()`. Existing kits that
7
+ * historically defaulted to `'string'` (mongokit pre-3.x) keep their own
8
+ * runtime default — `Pick<TenantConfig, 'fieldType'>` extension preserves
9
+ * type-level alignment without forcing a runtime default change.
10
+ */
11
+ const DEFAULT_TENANT_CONFIG = {
12
+ strategy: "field",
13
+ enabled: true,
14
+ tenantField: "organizationId",
15
+ fieldType: "objectId",
16
+ ref: "organization",
17
+ contextKey: "organizationId",
18
+ required: true
19
+ };
20
+ /**
21
+ * Resolve a possibly-partial {@link TenantConfig} against the defaults.
22
+ *
23
+ * - `false` → `enabled: false`, `strategy: 'none'`, `required: false`.
24
+ * - `true` / `undefined` → default field strategy.
25
+ * - Object with `strategy: 'custom'` → `resolve` is required; throws
26
+ * otherwise so the misconfiguration surfaces at boot, not runtime.
27
+ * - Object with `strategy: 'none'` → `enabled: false` (preserves
28
+ * user-supplied `tenantField` / `fieldType` / `ref` so the doc field
29
+ * stays correctly typed even with scoping off).
30
+ */
31
+ function resolveTenantConfig(config) {
32
+ if (config === false) return {
33
+ ...DEFAULT_TENANT_CONFIG,
34
+ strategy: "none",
35
+ enabled: false,
36
+ required: false
37
+ };
38
+ if (config === true || config === void 0) return { ...DEFAULT_TENANT_CONFIG };
39
+ const strategy = config.strategy ?? (config.enabled === false ? "none" : "field");
40
+ const contextKey = config.contextKey ?? config.tenantField ?? DEFAULT_TENANT_CONFIG.contextKey;
41
+ if (strategy === "none") return {
42
+ ...DEFAULT_TENANT_CONFIG,
43
+ ...config,
44
+ contextKey,
45
+ strategy: "none",
46
+ enabled: false,
47
+ required: false
48
+ };
49
+ if (strategy === "custom") {
50
+ if (typeof config.resolve !== "function") throw new Error("[repo-core] TenantConfig.strategy 'custom' requires a 'resolve' function");
51
+ return {
52
+ ...DEFAULT_TENANT_CONFIG,
53
+ ...config,
54
+ contextKey,
55
+ strategy: "custom",
56
+ enabled: config.enabled ?? true,
57
+ resolve: config.resolve
58
+ };
59
+ }
60
+ return {
61
+ ...DEFAULT_TENANT_CONFIG,
62
+ ...config,
63
+ contextKey,
64
+ strategy: "field",
65
+ enabled: config.enabled ?? true
66
+ };
67
+ }
68
+ //#endregion
69
+ export { DEFAULT_TENANT_CONFIG, resolveTenantConfig };