@plasius/schema 1.0.18 → 1.1.1

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 (56) hide show
  1. package/README.md +140 -1
  2. package/dist/index.cjs +1934 -0
  3. package/dist/index.cjs.map +1 -0
  4. package/dist/index.d.cts +391 -0
  5. package/dist/index.d.ts +391 -0
  6. package/dist/index.js +1883 -0
  7. package/dist/index.js.map +1 -0
  8. package/package.json +18 -6
  9. package/.eslintrc.cjs +0 -7
  10. package/.github/workflows/cd.yml +0 -186
  11. package/.github/workflows/ci.yml +0 -16
  12. package/.nvmrc +0 -1
  13. package/.vscode/launch.json +0 -15
  14. package/CHANGELOG.md +0 -86
  15. package/CODE_OF_CONDUCT.md +0 -79
  16. package/CONTRIBUTING.md +0 -201
  17. package/CONTRIBUTORS.md +0 -27
  18. package/SECURITY.md +0 -17
  19. package/docs/adrs/adr-0001: schema.md +0 -45
  20. package/docs/adrs/adr-template.md +0 -67
  21. package/legal/CLA-REGISTRY.csv +0 -2
  22. package/legal/CLA.md +0 -22
  23. package/legal/CORPORATE_CLA.md +0 -57
  24. package/legal/INDIVIDUAL_CLA.md +0 -91
  25. package/sbom.cdx.json +0 -66
  26. package/src/components.ts +0 -39
  27. package/src/field.builder.ts +0 -119
  28. package/src/field.ts +0 -14
  29. package/src/index.ts +0 -7
  30. package/src/infer.ts +0 -34
  31. package/src/pii.ts +0 -165
  32. package/src/schema.ts +0 -826
  33. package/src/types.ts +0 -156
  34. package/src/validation/countryCode.ISO3166.ts +0 -256
  35. package/src/validation/currencyCode.ISO4217.ts +0 -191
  36. package/src/validation/dateTime.ISO8601.ts +0 -9
  37. package/src/validation/email.RFC5322.ts +0 -9
  38. package/src/validation/generalText.OWASP.ts +0 -39
  39. package/src/validation/index.ts +0 -13
  40. package/src/validation/name.OWASP.ts +0 -25
  41. package/src/validation/percentage.ISO80000-1.ts +0 -8
  42. package/src/validation/phone.E.164.ts +0 -9
  43. package/src/validation/richtext.OWASP.ts +0 -34
  44. package/src/validation/url.WHATWG.ts +0 -16
  45. package/src/validation/user.MS-GOOGLE-APPLE.ts +0 -31
  46. package/src/validation/uuid.RFC4122.ts +0 -10
  47. package/src/validation/version.SEMVER2.0.0.ts +0 -8
  48. package/tests/pii.test.ts +0 -139
  49. package/tests/schema.test.ts +0 -501
  50. package/tests/test-utils.ts +0 -97
  51. package/tests/validate.test.ts +0 -97
  52. package/tests/validation.test.ts +0 -98
  53. package/tsconfig.build.json +0 -19
  54. package/tsconfig.json +0 -7
  55. package/tsup.config.ts +0 -10
  56. package/vitest.config.js +0 -20
package/src/schema.ts DELETED
@@ -1,826 +0,0 @@
1
- import { Infer } from "./infer.js";
2
- import { FieldType, Schema, SchemaOptions, SchemaShape } from "./types.js";
3
- import { field } from "./field.js";
4
- import { PIIAction, PIIClassification, PIILogHandling } from "./pii.js";
5
- import {
6
- enforcePIIField,
7
- prepareForStorage as piiPrepareForStorage,
8
- prepareForRead as piiPrepareForRead,
9
- sanitizeForLog as piiSanitizeForLog,
10
- getPiiAudit as piiGetPiiAudit,
11
- scrubPiiForDelete as piiScrubPiiForDelete,
12
- } from "./pii.js";
13
- import { FieldBuilder } from "./field.builder.js";
14
-
15
- const globalSchemaRegistry = new Map<string, Schema<any>>();
16
-
17
- function validateEnum(
18
- parentKey: string,
19
- value: any,
20
- enumValues?: Array<string | number>
21
- ): string | undefined {
22
- if (!enumValues) return;
23
-
24
- const values = Array.isArray(enumValues)
25
- ? enumValues
26
- : Array.from(enumValues);
27
-
28
- if (Array.isArray(value)) {
29
- const invalid = value.filter((v) => !values.includes(v));
30
- if (invalid.length > 0) {
31
- return `Field ${parentKey} contains invalid enum values: ${invalid.join(
32
- ", "
33
- )}`;
34
- }
35
- } else {
36
- if (!values.includes(value)) {
37
- return `Field ${parentKey} must be one of: ${values.join(", ")}`;
38
- }
39
- }
40
- }
41
-
42
- // Helper: some builders may store enum/optional/validator under different keys
43
- // schema.ts
44
- function getEnumValues(def: any): Array<string | number> | undefined {
45
- const src = Array.isArray(def?.enum)
46
- ? def.enum
47
- : Array.isArray(def?.enumValues)
48
- ? def.enumValues
49
- : Array.isArray(def?._enum)
50
- ? def._enum
51
- : Array.isArray(def?._enumValues)
52
- ? def._enumValues
53
- : def?._enumSet instanceof Set
54
- ? Array.from(def._enumSet)
55
- : undefined;
56
-
57
- if (!src) return undefined;
58
- const ok = src.every(
59
- (v: unknown) => typeof v === "string" || typeof v === "number"
60
- );
61
- return ok ? (src as Array<string | number>) : undefined;
62
- }
63
-
64
- function isOptional(def: any): boolean {
65
- return (def?.isRequired ?? false) === false;
66
- }
67
-
68
- function getValidator(def: any): ((v: any) => boolean) | undefined {
69
- return def?._validator ?? undefined;
70
- }
71
-
72
- function getShape(def: any): Record<string, any> | undefined {
73
- return def?._shape ?? def?.shape ?? undefined;
74
- }
75
-
76
- // ──────────────────────────────────────────────────────────────────────────────
77
- // Validation helpers (extracted from validate())
78
- // ──────────────────────────────────────────────────────────────────────────────
79
-
80
- function checkMissingRequired(
81
- parentKey: string,
82
- key: string,
83
- value: any,
84
- def: any,
85
- errors: string[]
86
- ): { missing: boolean } {
87
- if (value === undefined || value === null) {
88
- const path = parentKey ? `${parentKey}.${key}` : key;
89
- if (!isOptional(def)) {
90
- errors.push(`Missing required field: ${path}`);
91
- }
92
- return { missing: true };
93
- }
94
- return { missing: false };
95
- }
96
-
97
- function checkImmutable(
98
- parentKey: string,
99
- key: string,
100
- value: any,
101
- def: any,
102
- existing: Record<string, any> | undefined,
103
- errors: string[]
104
- ): { immutableViolation: boolean } {
105
- if (
106
- def.isImmutable &&
107
- existing &&
108
- existing[key] !== undefined &&
109
- value !== existing[key]
110
- ) {
111
- const path = parentKey ? `${parentKey}.${key}` : key;
112
- errors.push(`Field is immutable: ${path}`);
113
- return { immutableViolation: true };
114
- }
115
- return { immutableViolation: false };
116
- }
117
-
118
- function runCustomValidator(
119
- parentKey: string,
120
- key: string,
121
- value: any,
122
- def: any,
123
- errors: string[]
124
- ): { invalid: boolean } {
125
- const validator = getValidator(def);
126
- if (validator && value !== undefined && value !== null) {
127
- const valid = validator(value);
128
- if (!valid) {
129
- const path = parentKey ? `${parentKey}.${key}` : key;
130
- errors.push(`Invalid value for field: ${path}`);
131
- return { invalid: true };
132
- }
133
- }
134
- return { invalid: false };
135
- }
136
-
137
- function validateStringField(
138
- parentKey: string,
139
- key: string,
140
- value: any,
141
- def: any,
142
- errors: string[]
143
- ) {
144
- const path = parentKey ? `${parentKey}.${key}` : key;
145
- if (typeof value !== "string") {
146
- errors.push(`Field ${path} must be string`);
147
- return;
148
- }
149
-
150
- const enumValues = getEnumValues(def);
151
- if (Array.isArray(enumValues)) {
152
- const enumError = validateEnum(path, value, enumValues);
153
- if (enumError) {
154
- errors.push(enumError);
155
- }
156
- }
157
- }
158
-
159
- function validateNumberField(
160
- parentKey: string,
161
- key: string,
162
- value: any,
163
- _def: any,
164
- errors: string[]
165
- ) {
166
- const enumPath = parentKey ? `${parentKey}.${key}` : key;
167
- if (typeof value !== "number") {
168
- errors.push(`Field ${enumPath} must be number`);
169
- }
170
- }
171
-
172
- function validateBooleanField(
173
- parentKey: string,
174
- key: string,
175
- value: any,
176
- _def: any,
177
- errors: string[]
178
- ) {
179
- const path = parentKey ? `${parentKey}.${key}` : key;
180
- if (typeof value !== "boolean") {
181
- errors.push(`Field ${path} must be boolean`);
182
- }
183
- }
184
-
185
- function validateObjectChildren(
186
- parentKey: string,
187
- obj: any,
188
- shape: Record<string, any>,
189
- errors: string[]
190
- ) {
191
- for (const [childKey, childDef] of Object.entries(shape) as [
192
- string,
193
- FieldBuilder<any>
194
- ][]) {
195
- const childValue = obj[childKey];
196
-
197
- // Required check
198
- const { missing } = checkMissingRequired(
199
- parentKey,
200
- childKey,
201
- childValue,
202
- childDef,
203
- errors
204
- );
205
- if (missing) continue;
206
-
207
- // Custom validator (per-field)
208
- const { invalid } = runCustomValidator(
209
- parentKey,
210
- childKey,
211
- childValue,
212
- childDef,
213
- errors
214
- );
215
- if (invalid) continue;
216
-
217
- // Type-specific validation (string/number/boolean/object/array/ref)
218
- // This will recurse into nested objects via validateObjectField → validateObjectChildren
219
- validateByType(parentKey, childKey, childValue, childDef, errors);
220
- }
221
- }
222
-
223
- function validateObjectField(
224
- parentKey: string,
225
- key: string,
226
- value: any,
227
- def: any,
228
- errors: string[]
229
- ) {
230
- const path = parentKey ? `${parentKey}.${key}` : key;
231
- if (typeof value !== "object" || value === null || Array.isArray(value)) {
232
- errors.push(`Field ${path} must be object`);
233
- return;
234
- }
235
- const objShape = getShape(def);
236
- if (objShape) validateObjectChildren(path, value, objShape, errors);
237
- }
238
-
239
- function validateArrayOfStrings(
240
- parentKey: string,
241
- key: string,
242
- arr: any[],
243
- itemDef: any,
244
- errors: string[]
245
- ) {
246
- const path = parentKey ? `${parentKey}.${key}` : key;
247
- if (!arr.every((v) => typeof v === "string")) {
248
- errors.push(`Field ${path} must be string[]`);
249
- return;
250
- }
251
- const enumValues = getEnumValues(itemDef);
252
- if (Array.isArray(enumValues)) {
253
- const enumError = validateEnum(path, arr, enumValues);
254
- if (enumError) {
255
- errors.push(enumError);
256
- }
257
- }
258
- }
259
-
260
- function validateArrayOfNumbers(
261
- parentKey: string,
262
- key: string,
263
- arr: any[],
264
- itemDef: any,
265
- errors: string[]
266
- ) {
267
- const path = parentKey ? `${parentKey}.${key}` : key;
268
- if (!arr.every((v) => typeof v === "number")) {
269
- errors.push(`Field ${path} must be number[]`);
270
- }
271
-
272
- const enumValues = getEnumValues(itemDef);
273
- if (Array.isArray(enumValues)) {
274
- const enumError = validateEnum(path, arr, enumValues);
275
- if (enumError) {
276
- errors.push(enumError);
277
- }
278
- }
279
- }
280
-
281
- function validateArrayOfBooleans(
282
- parentKey: string,
283
- key: string,
284
- arr: any[],
285
- _itemDef: any,
286
- errors: string[]
287
- ) {
288
- const path = parentKey ? `${parentKey}.${key}` : key;
289
- if (!arr.every((v) => typeof v === "boolean")) {
290
- errors.push(`Field ${path} must be boolean[]`);
291
- }
292
- }
293
-
294
- function validateArrayOfObjects(
295
- parentKey: string,
296
- key: string,
297
- arr: any[],
298
- itemDef: any,
299
- errors: string[]
300
- ) {
301
- const path = parentKey ? `${parentKey}.${key}` : key;
302
-
303
- if (
304
- !Array.isArray(arr) ||
305
- !arr.every((v) => typeof v === "object" && v !== null && !Array.isArray(v))
306
- ) {
307
- errors.push(`Field ${path} must be object[]`);
308
- return;
309
- }
310
-
311
- const itemShape = getShape(itemDef);
312
- if (!itemShape) return;
313
-
314
- arr.forEach((item, idx) => {
315
- const itemParent = `${path}[${idx}]`;
316
- for (const [childKey, childDef] of Object.entries(itemShape) as [
317
- string,
318
- FieldBuilder<any>
319
- ][]) {
320
- const childValue = (item as any)[childKey];
321
-
322
- // Required check (path-aware)
323
- const { missing } = checkMissingRequired(
324
- itemParent,
325
- childKey,
326
- childValue,
327
- childDef,
328
- errors
329
- );
330
- if (missing) continue;
331
-
332
- // Custom validator (path-aware)
333
- const { invalid } = runCustomValidator(
334
- itemParent,
335
- childKey,
336
- childValue,
337
- childDef,
338
- errors
339
- );
340
- if (invalid) continue;
341
-
342
- // Type-specific validation (recurses into nested object/array/ref)
343
- validateByType(itemParent, childKey, childValue, childDef, errors);
344
- }
345
- });
346
- }
347
-
348
- function validateArrayField(
349
- parentKey: string,
350
- key: string,
351
- value: any,
352
- def: any,
353
- errors: string[]
354
- ) {
355
- const path = parentKey ? `${parentKey}.${key}` : key;
356
- if (!Array.isArray(value)) {
357
- errors.push(`Field ${key} must be an array`);
358
- return;
359
- }
360
- const itemType = def.itemType?.type;
361
- if (itemType === "string")
362
- return validateArrayOfStrings(parentKey, key, value, def.itemType, errors);
363
- if (itemType === "number")
364
- return validateArrayOfNumbers(parentKey, key, value, def.itemType, errors);
365
- if (itemType === "boolean")
366
- return validateArrayOfBooleans(parentKey, key, value, def.itemType, errors);
367
- if (itemType === "object")
368
- return validateArrayOfObjects(parentKey, key, value, def.itemType, errors);
369
- if (itemType === "ref") {
370
- const expectedType = (def.itemType as any).refType;
371
- value.forEach((ref: any, idx: number) => {
372
- if (
373
- !ref ||
374
- typeof ref !== "object" ||
375
- ref === null ||
376
- typeof ref.type !== "string" ||
377
- typeof ref.id !== "string" ||
378
- (expectedType && ref.type !== expectedType)
379
- ) {
380
- errors.push(
381
- `Field ${path}[${idx}] must be a reference object with type: ${expectedType}`
382
- );
383
- }
384
- });
385
- const refShape = getShape(def.itemType);
386
- if (refShape) {
387
- value.forEach((ref: any, idx: number) => {
388
- if (ref && typeof ref === "object" && ref !== null) {
389
- for (const [childKey, childDef] of Object.entries(refShape) as [
390
- string,
391
- FieldBuilder<any>
392
- ][]) {
393
- const childValue = ref[childKey];
394
- if (
395
- (childValue === undefined || childValue === null) &&
396
- !isOptional(childDef)
397
- ) {
398
- errors.push(
399
- `Missing required field: ${path}[${idx}].${childKey}`
400
- );
401
- continue;
402
- }
403
- const childValidator = getValidator(childDef);
404
- if (
405
- childValidator &&
406
- childValue !== undefined &&
407
- childValue !== null
408
- ) {
409
- const valid = childValidator(childValue);
410
- if (!valid)
411
- errors.push(
412
- `Invalid value for field: ${path}[${idx}].${childKey}`
413
- );
414
- }
415
- }
416
- }
417
- });
418
- }
419
- return;
420
- }
421
- errors.push(`Field ${path} has unsupported array item type`);
422
- }
423
-
424
- function validateRefField(
425
- parentKey: string,
426
- key: string,
427
- value: any,
428
- _def: any,
429
- errors: string[]
430
- ) {
431
- if (
432
- typeof value !== "object" ||
433
- value === null ||
434
- typeof value.type !== "string" ||
435
- typeof value.id !== "string"
436
- ) {
437
- const path = parentKey ? `${parentKey}.${key}` : key;
438
- errors.push(`Field ${path} must be { type: string; id: string }`);
439
- }
440
- }
441
-
442
- function validateByType(
443
- parentKey: string,
444
- key: string,
445
- value: any,
446
- def: any,
447
- errors: string[]
448
- ) {
449
- const path = parentKey ? `${parentKey}.${key}` : key;
450
- switch (def.type) {
451
- case "string":
452
- return validateStringField(parentKey, key, value, def, errors);
453
- case "number":
454
- return validateNumberField(parentKey, key, value, def, errors);
455
- case "boolean":
456
- return validateBooleanField(parentKey, key, value, def, errors);
457
- case "object":
458
- return validateObjectField(parentKey, key, value, def, errors);
459
- case "array":
460
- return validateArrayField(parentKey, key, value, def, errors);
461
- case "ref":
462
- return validateRefField(parentKey, key, value, def, errors);
463
- default:
464
- errors.push(`Unknown type for field ${path}: ${def.type}`);
465
- }
466
- }
467
-
468
- export function createSchema<S extends SchemaShape>(
469
- _shape: S,
470
- entityType: string,
471
- options: SchemaOptions = {
472
- version: "1.0",
473
- table: "",
474
- schemaValidator: () => true,
475
- piiEnforcement: "none",
476
- }
477
- ): Schema<S> {
478
- const systemFields = {
479
- type: field.string().immutable().system(),
480
- version: field.string().immutable().system(),
481
- };
482
-
483
- const version = options.version || "1.0";
484
- const store = options.table || "";
485
- const schema: Schema<S> = {
486
- // 🔗 Define the schema shape
487
- _shape: {
488
- ...systemFields,
489
- ..._shape,
490
- } as S,
491
-
492
- // 🔗 Metadata about the schema
493
- meta: { entityType, version },
494
-
495
- // 🔗 Validate input against the schema
496
- validate(input: unknown, existing?: Record<string, any>) {
497
- const errors: string[] = [];
498
- const result: any = {};
499
-
500
- if (typeof input !== "object" || input === null) {
501
- return { valid: false, errors: ["Input must be an object"] } as any;
502
- }
503
- // Work on a non-mutating copy that includes system defaults for first-time objects
504
- const working: Record<string, any> = { ...(input as any) };
505
- if (working.type == null) working.type = entityType;
506
- if (working.version == null) working.version = version;
507
-
508
- for (const key in schema._shape) {
509
- const def = schema._shape[key];
510
- const value = working[key];
511
-
512
- if (!def) {
513
- errors.push(`Field definition missing for: ${key}`);
514
- continue;
515
- }
516
-
517
- // 1) Required
518
- const { missing } = checkMissingRequired("", key, value, def, errors);
519
- if (missing) continue;
520
-
521
- // 2) Immutable
522
- const { immutableViolation } = checkImmutable(
523
- "",
524
- key,
525
- value,
526
- def,
527
- existing,
528
- errors
529
- );
530
- if (immutableViolation) continue;
531
-
532
- // 3) PII enforcement (may short-circuit)
533
- const { shortCircuit } = enforcePIIField(
534
- "",
535
- key,
536
- value,
537
- def,
538
- options.piiEnforcement ?? "none",
539
- errors,
540
- console
541
- );
542
- if (shortCircuit) continue;
543
-
544
- // 4) Custom validator
545
- const { invalid } = runCustomValidator("", key, value, def, errors);
546
- if (invalid) continue;
547
-
548
- // 5) Type-specific validation
549
- validateByType("", key, value, def, errors);
550
-
551
- // Assign value regardless; storage transforms happen elsewhere
552
- result[key] = value;
553
- }
554
-
555
-
556
- if (errors.length === 0 && options.schemaValidator) {
557
- const castValue = result as Infer<S>;
558
- if (!options.schemaValidator(castValue)) {
559
- errors.push("Schema-level validation failed.");
560
- }
561
- }
562
-
563
- return {
564
- valid: errors.length === 0,
565
- value: result as Infer<S>,
566
- errors,
567
- };
568
- },
569
-
570
- // specific validator for a schema to allow conditional validation
571
- schemaValidator: options.schemaValidator!, // <== expose it here!
572
-
573
- /**
574
- * Recursively validates entity references defined in this schema.
575
- *
576
- * Traverses fields of type `ref` and arrays of `ref` and resolves each target
577
- * entity using the provided `resolveEntity` function. When `autoValidate` is
578
- * enabled (default) and the field's `refPolicy` is `eager`, the referenced
579
- * entity's schema is fetched and validated via `validateComposition` up to
580
- * `maxDepth` levels.
581
- *
582
- * Skips fields not listed in `onlyFields` when provided. Prevents cycles via
583
- * a `visited` set in `validatorContext`.
584
- *
585
- * @param entity The root entity to validate (must include `type` and `id`).
586
- * @param options Options controlling traversal and resolution behavior.
587
- * @param options.resolveEntity Function to resolve a referenced entity by type and id.
588
- * @param options.validatorContext Internal context (visited set) to prevent cycles.
589
- * @param options.maxDepth Maximum depth for recursive validation (default: 5).
590
- * @param options.onlyFields Optional whitelist of field names to validate.
591
- * @param options.log Optional logger for traversal/debug output.
592
- *
593
- * @throws Error if a broken reference is encountered (target cannot be resolved).
594
- */
595
- async validateComposition(entity, options) {
596
- const {
597
- resolveEntity,
598
- validatorContext = { visited: new Set() },
599
- maxDepth = 5,
600
- log,
601
- } = options;
602
-
603
- const entityKey = `${(entity as any).type}:${(entity as any).id}`;
604
- if (validatorContext.visited.has(entityKey)) {
605
- log?.(`Skipping already visited entity ${entityKey}`);
606
- return;
607
- }
608
- validatorContext.visited.add(entityKey);
609
- log?.(`Validating composition for entity ${entityKey}`);
610
-
611
- for (const [key, def] of Object.entries(schema._shape)) {
612
- // NEW: skip if not in onlyFields
613
- if (options.onlyFields && !options.onlyFields.includes(key)) {
614
- log?.(`Skipping field ${key} (not in onlyFields)`);
615
- continue;
616
- }
617
-
618
- const refType = (def as any).refType as string;
619
- const autoValidate = (def as any).autoValidate !== false;
620
- const refPolicy = (def as any).refPolicy ?? "eager";
621
- const value = (entity as any)[key];
622
- if (!value) continue;
623
-
624
- if (def.type === "ref") {
625
- const ref = value as { type: string; id: string };
626
- const target = await options.resolveEntity(refType, ref.id);
627
- if (!target)
628
- throw new Error(
629
- `Broken reference: ${refType} ${ref.id} in field ${key}`
630
- );
631
- log?.(`Resolved ${refType} ${ref.id} from field ${key}`);
632
-
633
- if (autoValidate && refPolicy === "eager") {
634
- const targetSchema = getSchemaForType(refType);
635
- if (options.maxDepth! > 0 && targetSchema) {
636
- await targetSchema.validateComposition(target, {
637
- ...options,
638
- maxDepth: options.maxDepth! - 1,
639
- });
640
- }
641
- }
642
- // Handle array of refs: type === "array" and itemType?.type === "ref"
643
- } else if (def.type === "array" && def.itemType?.type === "ref") {
644
- const refs = value as Array<{ type: string; id: string }>;
645
- for (const ref of refs) {
646
- const target = await options.resolveEntity(refType, ref.id);
647
- if (!target)
648
- throw new Error(
649
- `Broken reference: ${refType} ${ref.id} in field ${key}`
650
- );
651
- log?.(`Resolved ${refType} ${ref.id} from field ${key}`);
652
-
653
- if (autoValidate && refPolicy === "eager") {
654
- const targetSchema = getSchemaForType(refType);
655
- if (options.maxDepth! > 0 && targetSchema) {
656
- await targetSchema.validateComposition(target, {
657
- ...options,
658
- maxDepth: options.maxDepth! - 1,
659
- });
660
- }
661
- }
662
- }
663
- }
664
- }
665
- },
666
-
667
- /**
668
- * Returns the configured table name for this schema.
669
- *
670
- * @throws Error if no store/table name has been defined for this schema.
671
- */
672
- tableName(): string {
673
- if (!store || store === "") {
674
- throw new Error("Store is not defined for this schema");
675
- }
676
- return store;
677
- },
678
-
679
- /**
680
- * Transforms an input object for persistence by applying PII protection
681
- * according to field annotations (e.g., encryption and hashing).
682
- *
683
- * @param input The raw entity data.
684
- * @param encryptFn Function used to encrypt sensitive values.
685
- * @param hashFn Function used to hash sensitive values.
686
- * @returns A new object safe to store.
687
- */
688
- prepareForStorage(
689
- input: Record<string, any>,
690
- encryptFn: (value: any) => string,
691
- hashFn: (value: any) => string
692
- ): Record<string, any> {
693
- return piiPrepareForStorage(_shape, input, encryptFn, hashFn);
694
- },
695
-
696
- /**
697
- * Reverses storage transformations for read paths (e.g., decrypts values)
698
- * according to PII annotations, returning a consumer-friendly object.
699
- *
700
- * @param stored Data retrieved from storage.
701
- * @param decryptFn Function used to decrypt values that were encrypted on write.
702
- * @returns A new object suitable for application consumption.
703
- */
704
- prepareForRead(
705
- stored: Record<string, any>,
706
- decryptFn: (value: string) => any
707
- ): Record<string, any> {
708
- return piiPrepareForRead(_shape, stored, decryptFn);
709
- },
710
-
711
- /**
712
- * Produces a log-safe copy of the provided data by redacting or pseudonymizing
713
- * PII fields in accordance with field annotations.
714
- *
715
- * @param data Arbitrary data to sanitize for logging.
716
- * @param pseudonymFn Function producing stable pseudonyms for sensitive values.
717
- * @returns A copy safe to emit to logs.
718
- */
719
- sanitizeForLog(
720
- data: Record<string, any>,
721
- pseudonymFn: (value: any) => string
722
- ): Record<string, any> {
723
- return piiSanitizeForLog(_shape, data, pseudonymFn);
724
- },
725
-
726
- /**
727
- * Returns a list of fields annotated with PII metadata for auditing purposes.
728
- * Each entry includes classification, required action, and optional log policy.
729
- */
730
- getPiiAudit(): Array<{
731
- field: string;
732
- classification: PIIClassification;
733
- action: PIIAction;
734
- logHandling?: PIILogHandling;
735
- purpose?: string;
736
- }> {
737
- return piiGetPiiAudit(_shape);
738
- },
739
-
740
- /**
741
- * Produces a copy of stored data suitable for data deletion flows by scrubbing
742
- * or blanking PII per field annotations.
743
- *
744
- * @param stored Data as persisted.
745
- * @returns A copy with PII removed or neutralized for deletion.
746
- */
747
- scrubPiiForDelete(stored: Record<string, any>): Record<string, any> {
748
- return piiScrubPiiForDelete(_shape, stored);
749
- },
750
-
751
- /**
752
- * Returns a normalized description of the schema suitable for documentation
753
- * or UI rendering (type, optionality, enum values, PII flags, etc.).
754
- */
755
- describe() {
756
- const description: Record<string, any> = {};
757
- for (const [key, def] of Object.entries(schema._shape)) {
758
- description[key] = {
759
- type: def.type,
760
- optional: !!def.optional,
761
- description: def._description ?? "",
762
- version: def._version ?? "",
763
- enum: getEnumValues(def as any),
764
- refType: (def as any).refType ?? undefined,
765
- pii: def._pii ?? undefined,
766
- };
767
- }
768
-
769
- return {
770
- entityType,
771
- version,
772
- shape: description,
773
- };
774
- },
775
- };
776
-
777
- // Register the schema globally
778
- globalSchemaRegistry.set(entityType, schema);
779
- return schema;
780
- }
781
-
782
- /**
783
- * Retrieves a previously registered schema by its `entityType` from the
784
- * in-process global schema registry.
785
- */
786
- export function getSchemaForType(type: string): Schema<any> | undefined {
787
- return globalSchemaRegistry.get(type);
788
- }
789
-
790
- /**
791
- * Returns all schemas registered in the in-process global registry.
792
- */
793
- export function getAllSchemas(): Schema<any>[] {
794
- return Array.from(globalSchemaRegistry.values());
795
- }
796
-
797
- /**
798
- * Renders a schema into a simplified descriptor for front-end consumption.
799
- * Intended for documentation and admin tooling rather than validation.
800
- */
801
- export function renderSchemaDescription(
802
- schema: Schema<any>
803
- ): {
804
- title: string;
805
- fields: Array<{
806
- name: string;
807
- type: FieldType;
808
- optional: boolean;
809
- description: string;
810
- deprecated: boolean;
811
- pii?: string;
812
- }>;
813
- } {
814
- const meta = schema.describe();
815
- return {
816
- title: `${meta.entityType} (v${meta.version})`,
817
- fields: Object.entries(meta.shape).map(([name, def]) => ({
818
- name,
819
- type: def.type,
820
- optional: def.optional,
821
- description: def.description,
822
- deprecated: def.deprecated,
823
- pii: def.pii ? def.pii.classification : undefined,
824
- })),
825
- };
826
- }