@jskit-ai/crud-core 0.1.63 → 0.1.64

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.
@@ -1,5 +1,6 @@
1
1
  import test from "node:test";
2
2
  import assert from "node:assert/strict";
3
+ import { createSchema } from "json-rest-schema";
3
4
  import {
4
5
  DEFAULT_LIST_LIMIT,
5
6
  normalizeCrudListLimit,
@@ -80,6 +81,13 @@ function createQueryDouble() {
80
81
  };
81
82
  }
82
83
 
84
+ function createOperationSchemaDefinition(structure = {}, mode = "replace") {
85
+ return {
86
+ schema: createSchema(structure),
87
+ mode
88
+ };
89
+ }
90
+
83
91
  test("normalizeCrudListLimit enforces fallback and max", () => {
84
92
  assert.equal(normalizeCrudListLimit(null), DEFAULT_LIST_LIMIT);
85
93
  assert.equal(normalizeCrudListLimit("abc"), DEFAULT_LIST_LIMIT);
@@ -262,59 +270,48 @@ test("deriveRepositoryMappingFromResource reads schema keys and repository colum
262
270
  const resource = {
263
271
  operations: {
264
272
  view: {
265
- outputValidator: {
266
- schema: {
267
- type: "object",
268
- properties: {
269
- id: { type: "integer" },
270
- firstName: { type: "string" },
271
- createdAt: { type: "string" }
272
- }
273
+ output: createOperationSchemaDefinition({
274
+ id: { type: "integer", required: true },
275
+ firstName: { type: "string", required: true },
276
+ createdAt: {
277
+ type: "string",
278
+ required: true,
279
+ actualField: "created_at"
273
280
  }
274
- }
281
+ })
275
282
  },
276
283
  create: {
277
- bodyValidator: {
278
- schema: {
279
- type: "object",
280
- properties: {
281
- firstName: { type: "string" },
282
- vetId: { type: "integer" }
284
+ body: createOperationSchemaDefinition({
285
+ firstName: { type: "string" },
286
+ vetId: {
287
+ type: "integer",
288
+ actualField: "vet_id",
289
+ relation: {
290
+ kind: "lookup",
291
+ namespace: "vets",
292
+ valueKey: "id"
283
293
  }
284
294
  }
285
- }
295
+ }, "create")
286
296
  },
287
297
  patch: {
288
- bodyValidator: {
289
- schema: {
290
- type: "object",
291
- properties: {
292
- archivedAt: { type: "string" },
293
- vetId: { type: "integer" }
298
+ body: createOperationSchemaDefinition({
299
+ archivedAt: {
300
+ type: "string",
301
+ actualField: "archived_at"
302
+ },
303
+ vetId: {
304
+ type: "integer",
305
+ actualField: "vet_id",
306
+ relation: {
307
+ kind: "lookup",
308
+ namespace: "vets",
309
+ valueKey: "id"
294
310
  }
295
311
  }
296
- }
297
- }
298
- },
299
- fieldMeta: [
300
- {
301
- key: "createdAt",
302
- repository: { column: "created_at" }
303
- },
304
- {
305
- key: "vetId",
306
- repository: { column: "vet_id" },
307
- relation: {
308
- kind: "lookup",
309
- namespace: "vets",
310
- valueKey: "id"
311
- }
312
- },
313
- {
314
- key: "archivedAt",
315
- repository: { column: "archived_at" }
312
+ }, "patch")
316
313
  }
317
- ]
314
+ }
318
315
  };
319
316
 
320
317
  const mapping = deriveRepositoryMappingFromResource(resource);
@@ -335,36 +332,23 @@ test("deriveRepositoryMappingFromResource treats virtual output fields as non-co
335
332
  const resource = {
336
333
  operations: {
337
334
  view: {
338
- outputValidator: {
339
- schema: {
340
- type: "object",
341
- properties: {
342
- id: { type: "integer" },
343
- firstName: { type: "string" },
344
- remainingBatchWeight: { type: "number" }
335
+ output: createOperationSchemaDefinition({
336
+ id: { type: "integer", required: true },
337
+ firstName: { type: "string", required: true },
338
+ remainingBatchWeight: {
339
+ type: "number",
340
+ storage: {
341
+ virtual: true
345
342
  }
346
343
  }
347
- }
344
+ })
348
345
  },
349
346
  create: {
350
- bodyValidator: {
351
- schema: {
352
- type: "object",
353
- properties: {
354
- firstName: { type: "string" }
355
- }
356
- }
357
- }
358
- }
359
- },
360
- fieldMeta: [
361
- {
362
- key: "remainingBatchWeight",
363
- repository: {
364
- storage: "virtual"
365
- }
347
+ body: createOperationSchemaDefinition({
348
+ firstName: { type: "string" }
349
+ }, "create")
366
350
  }
367
- ]
351
+ }
368
352
  };
369
353
 
370
354
  const mapping = deriveRepositoryMappingFromResource(resource);
@@ -379,40 +363,32 @@ test("deriveRepositoryMappingFromResource rejects virtual fields in create schem
379
363
  const resource = {
380
364
  operations: {
381
365
  view: {
382
- outputValidator: {
383
- schema: {
384
- type: "object",
385
- properties: {
386
- id: { type: "integer" },
387
- remainingBatchWeight: { type: "number" }
366
+ output: createOperationSchemaDefinition({
367
+ id: { type: "integer", required: true },
368
+ remainingBatchWeight: {
369
+ type: "number",
370
+ storage: {
371
+ virtual: true
388
372
  }
389
373
  }
390
- }
374
+ })
391
375
  },
392
376
  create: {
393
- bodyValidator: {
394
- schema: {
395
- type: "object",
396
- properties: {
397
- remainingBatchWeight: { type: "number" }
377
+ body: createOperationSchemaDefinition({
378
+ remainingBatchWeight: {
379
+ type: "number",
380
+ storage: {
381
+ virtual: true
398
382
  }
399
383
  }
400
- }
401
- }
402
- },
403
- fieldMeta: [
404
- {
405
- key: "remainingBatchWeight",
406
- repository: {
407
- storage: "virtual"
408
- }
384
+ }, "create")
409
385
  }
410
- ]
386
+ }
411
387
  };
412
388
 
413
389
  assert.throws(
414
390
  () => deriveRepositoryMappingFromResource(resource),
415
- /resource create schema field "remainingBatchWeight" cannot use repository\.storage "virtual"/
391
+ /resource create schema field "remainingBatchWeight" cannot use storage\.virtual/
416
392
  );
417
393
  });
418
394
 
@@ -420,48 +396,35 @@ test("deriveRepositoryMappingFromResource rejects virtual fields in patch schema
420
396
  const resource = {
421
397
  operations: {
422
398
  view: {
423
- outputValidator: {
424
- schema: {
425
- type: "object",
426
- properties: {
427
- id: { type: "integer" },
428
- remainingBatchWeight: { type: "number" }
399
+ output: createOperationSchemaDefinition({
400
+ id: { type: "integer", required: true },
401
+ remainingBatchWeight: {
402
+ type: "number",
403
+ storage: {
404
+ virtual: true
429
405
  }
430
406
  }
431
- }
407
+ })
432
408
  },
433
409
  create: {
434
- bodyValidator: {
435
- schema: {
436
- type: "object",
437
- properties: {}
438
- }
439
- }
410
+ body: createOperationSchemaDefinition({}, "create")
440
411
  },
441
412
  patch: {
442
- bodyValidator: {
443
- schema: {
444
- type: "object",
445
- properties: {
446
- remainingBatchWeight: { type: "number" }
413
+ body: createOperationSchemaDefinition({
414
+ remainingBatchWeight: {
415
+ type: "number",
416
+ storage: {
417
+ virtual: true
447
418
  }
448
419
  }
449
- }
420
+ }, "patch")
450
421
  }
451
- },
452
- fieldMeta: [
453
- {
454
- key: "remainingBatchWeight",
455
- repository: {
456
- storage: "virtual"
457
- }
458
- }
459
- ]
422
+ }
460
423
  };
461
424
 
462
425
  assert.throws(
463
426
  () => deriveRepositoryMappingFromResource(resource),
464
- /resource patch schema field "remainingBatchWeight" cannot use repository\.storage "virtual"/
427
+ /resource patch schema field "remainingBatchWeight" cannot use storage\.virtual/
465
428
  );
466
429
  });
467
430
 
@@ -469,29 +432,18 @@ test("deriveRepositoryMappingFromResource excludes runtime-only lookups output k
469
432
  const resource = {
470
433
  operations: {
471
434
  view: {
472
- outputValidator: {
473
- schema: {
474
- type: "object",
475
- properties: {
476
- id: { type: "integer" },
477
- firstName: { type: "string" },
478
- lookups: { type: "object" }
479
- }
480
- }
481
- }
435
+ output: createOperationSchemaDefinition({
436
+ id: { type: "integer", required: true },
437
+ firstName: { type: "string", required: true },
438
+ lookups: { type: "object", opaque: true }
439
+ })
482
440
  },
483
441
  create: {
484
- bodyValidator: {
485
- schema: {
486
- type: "object",
487
- properties: {
488
- firstName: { type: "string" }
489
- }
490
- }
491
- }
442
+ body: createOperationSchemaDefinition({
443
+ firstName: { type: "string" }
444
+ }, "create")
492
445
  }
493
- },
494
- fieldMeta: []
446
+ }
495
447
  };
496
448
 
497
449
  const mapping = deriveRepositoryMappingFromResource(resource);
@@ -507,79 +459,58 @@ test("deriveRepositoryMappingFromResource excludes custom lookup output containe
507
459
  },
508
460
  operations: {
509
461
  view: {
510
- outputValidator: {
511
- schema: {
512
- type: "object",
513
- properties: {
514
- id: { type: "integer" },
515
- firstName: { type: "string" },
516
- lookupData: { type: "object" }
517
- }
518
- }
519
- }
462
+ output: createOperationSchemaDefinition({
463
+ id: { type: "integer", required: true },
464
+ firstName: { type: "string", required: true },
465
+ lookupData: { type: "object", opaque: true }
466
+ })
520
467
  },
521
468
  create: {
522
- bodyValidator: {
523
- schema: {
524
- type: "object",
525
- properties: {
526
- firstName: { type: "string" }
527
- }
528
- }
529
- }
469
+ body: createOperationSchemaDefinition({
470
+ firstName: { type: "string" }
471
+ }, "create")
530
472
  }
531
- },
532
- fieldMeta: []
473
+ }
533
474
  };
534
475
 
535
476
  const mapping = deriveRepositoryMappingFromResource(resource);
536
477
  assert.deepEqual(mapping.outputKeys, ["id", "firstName"]);
537
478
  });
538
479
 
539
- test("deriveRepositoryMappingFromResource throws when view schema properties are missing", () => {
480
+ test("deriveRepositoryMappingFromResource rejects non-schema view output definitions", () => {
540
481
  const resource = {
541
482
  operations: {
542
483
  view: {
543
- outputValidator: {
484
+ output: {
544
485
  schema: {
545
486
  type: "object"
546
487
  }
547
488
  }
548
489
  },
549
490
  create: {
550
- bodyValidator: {
551
- schema: {
552
- type: "object",
553
- properties: {
554
- firstName: { type: "string" }
555
- }
556
- }
557
- }
491
+ body: createOperationSchemaDefinition({
492
+ firstName: { type: "string" }
493
+ }, "create")
558
494
  }
559
495
  }
560
496
  };
561
497
 
562
498
  assert.throws(
563
499
  () => deriveRepositoryMappingFromResource(resource),
564
- /operations\.view\.outputValidator\.schema\.properties/
500
+ /operations\.view\.output\.schema must be a json-rest-schema schema instance/
565
501
  );
566
502
  });
567
503
 
568
- test("deriveRepositoryMappingFromResource throws when create schema properties are missing", () => {
504
+ test("deriveRepositoryMappingFromResource rejects non-schema create body definitions", () => {
569
505
  const resource = {
570
506
  operations: {
571
507
  view: {
572
- outputValidator: {
573
- schema: {
574
- type: "object",
575
- properties: {
576
- id: { type: "integer" }
577
- }
578
- }
579
- }
508
+ output: createOperationSchemaDefinition({
509
+ id: { type: "integer", required: true }
510
+ })
580
511
  },
581
512
  create: {
582
- bodyValidator: {
513
+ body: {
583
514
  schema: {
584
515
  type: "object"
585
516
  }
@@ -590,7 +521,7 @@ test("deriveRepositoryMappingFromResource throws when create schema properties a
590
521
 
591
522
  assert.throws(
592
523
  () => deriveRepositoryMappingFromResource(resource),
593
- /operations\.create\.bodyValidator\.schema\.properties/
524
+ /operations\.create\.body\.schema must be a json-rest-schema schema instance/
594
525
  );
595
526
  });
596
527
 
@@ -598,52 +529,32 @@ test("deriveRepositoryMappingFromResource tracks writable column-backed write se
598
529
  const resource = {
599
530
  operations: {
600
531
  view: {
601
- outputValidator: {
602
- schema: {
603
- type: "object",
604
- properties: {
605
- id: { type: "integer" },
606
- scheduledAt: { type: "string", format: "date-time" },
607
- archivedAt: { type: "string", format: "date-time" },
608
- remainingBatchWeight: { type: "number" }
532
+ output: createOperationSchemaDefinition({
533
+ id: { type: "integer", required: true },
534
+ scheduledAt: { type: "dateTime", required: true },
535
+ archivedAt: { type: "dateTime", required: true },
536
+ remainingBatchWeight: {
537
+ type: "number",
538
+ storage: {
539
+ virtual: true
609
540
  }
610
541
  }
611
- }
542
+ })
612
543
  },
613
544
  create: {
614
- bodyValidator: {
615
- schema: {
616
- type: "object",
617
- properties: {
618
- scheduledAt: { type: "string", format: "date-time" }
619
- }
620
- }
621
- }
545
+ body: createOperationSchemaDefinition({
546
+ scheduledAt: { type: "dateTime" }
547
+ }, "create")
622
548
  },
623
549
  patch: {
624
- bodyValidator: {
625
- schema: {
626
- type: "object",
627
- properties: {
628
- archivedAt: {
629
- anyOf: [
630
- { type: "string", format: "date-time" },
631
- { type: "null" }
632
- ]
633
- }
634
- }
550
+ body: createOperationSchemaDefinition({
551
+ archivedAt: {
552
+ type: "dateTime",
553
+ nullable: true
635
554
  }
636
- }
555
+ }, "patch")
637
556
  }
638
- },
639
- fieldMeta: [
640
- {
641
- key: "remainingBatchWeight",
642
- repository: {
643
- storage: "virtual"
644
- }
645
- }
646
- ]
557
+ }
647
558
  };
648
559
 
649
560
  const mapping = deriveRepositoryMappingFromResource(resource);
@@ -653,43 +564,132 @@ test("deriveRepositoryMappingFromResource tracks writable column-backed write se
653
564
  });
654
565
  });
655
566
 
656
- test("deriveRepositoryMappingFromResource keeps explicit repository.writeSerializer metadata", () => {
567
+ test("deriveRepositoryMappingFromResource keeps explicit storage.writeSerializer metadata", () => {
657
568
  const resource = {
658
569
  operations: {
659
570
  view: {
660
- outputValidator: {
661
- schema: {
662
- type: "object",
663
- properties: {
664
- id: { type: "integer" },
665
- arrivalDatetime: { type: "string", format: "date-time" }
571
+ output: createOperationSchemaDefinition({
572
+ id: { type: "integer", required: true },
573
+ arrivalDatetime: {
574
+ type: "dateTime",
575
+ required: true,
576
+ storage: {
577
+ writeSerializer: "datetime-utc"
666
578
  }
667
579
  }
668
- }
580
+ })
669
581
  },
670
582
  create: {
671
- bodyValidator: {
672
- schema: {
673
- type: "object",
674
- properties: {
675
- arrivalDatetime: { type: "string", format: "date-time" }
583
+ body: createOperationSchemaDefinition({
584
+ arrivalDatetime: {
585
+ type: "dateTime",
586
+ storage: {
587
+ writeSerializer: "datetime-utc"
676
588
  }
677
589
  }
678
- }
590
+ }, "create")
679
591
  }
592
+ }
593
+ };
594
+
595
+ const mapping = deriveRepositoryMappingFromResource(resource);
596
+ assert.deepEqual(mapping.writeSerializerByKey, {
597
+ arrivalDatetime: "datetime-utc"
598
+ });
599
+ });
600
+
601
+ test("deriveRepositoryMappingFromResource ignores transport export shape and uses authored field definitions", () => {
602
+ const viewSchema = createSchema({
603
+ recordId: {
604
+ type: "id",
605
+ required: true,
606
+ actualField: "record_id"
607
+ },
608
+ title: {
609
+ type: "string",
610
+ required: true
611
+ },
612
+ scheduledAt: {
613
+ type: "dateTime",
614
+ required: true,
615
+ actualField: "scheduled_at"
616
+ }
617
+ });
618
+ const createInputSchema = createSchema({
619
+ title: {
620
+ type: "string",
621
+ required: true
680
622
  },
681
- fieldMeta: [
682
- {
683
- key: "arrivalDatetime",
684
- repository: {
685
- writeSerializer: "datetime-utc"
623
+ scheduledAt: {
624
+ type: "dateTime",
625
+ required: true,
626
+ actualField: "scheduled_at"
627
+ }
628
+ });
629
+ const patchInputSchema = createSchema({
630
+ scheduledAt: {
631
+ type: "dateTime",
632
+ required: false,
633
+ actualField: "scheduled_at"
634
+ }
635
+ });
636
+
637
+ viewSchema.toJsonSchema = () => ({
638
+ type: "object",
639
+ properties: {
640
+ title: {
641
+ type: "number"
642
+ },
643
+ scheduledAt: {
644
+ type: "string"
645
+ }
646
+ }
647
+ });
648
+ createInputSchema.toJsonSchema = () => ({
649
+ type: "object",
650
+ properties: {
651
+ scheduledAt: {
652
+ type: "string"
653
+ }
654
+ }
655
+ });
656
+ patchInputSchema.toJsonSchema = () => ({
657
+ type: "object",
658
+ properties: {}
659
+ });
660
+
661
+ const resource = {
662
+ operations: {
663
+ view: {
664
+ output: {
665
+ schema: viewSchema,
666
+ mode: "replace"
667
+ }
668
+ },
669
+ create: {
670
+ body: {
671
+ schema: createInputSchema,
672
+ mode: "create"
673
+ }
674
+ },
675
+ patch: {
676
+ body: {
677
+ schema: patchInputSchema,
678
+ mode: "patch"
686
679
  }
687
680
  }
688
- ]
681
+ }
689
682
  };
690
683
 
691
684
  const mapping = deriveRepositoryMappingFromResource(resource);
685
+ assert.deepEqual(mapping.outputKeys, ["recordId", "title", "scheduledAt"]);
686
+ assert.deepEqual(mapping.outputRecordIdKeys, ["recordId"]);
687
+ assert.deepEqual(mapping.listSearchColumns, ["title"]);
692
688
  assert.deepEqual(mapping.writeSerializerByKey, {
693
- arrivalDatetime: "datetime-utc"
689
+ scheduledAt: "datetime-utc"
690
+ });
691
+ assert.deepEqual(mapping.columnOverrides, {
692
+ recordId: "record_id",
693
+ scheduledAt: "scheduled_at"
694
694
  });
695
695
  });