@afora/core 0.0.7 → 0.0.8

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 (49) hide show
  1. package/README.md +34 -6
  2. package/dist/core/application/start.d.ts +7 -0
  3. package/dist/core/application/types/application.interface.d.ts +36 -2
  4. package/dist/core/command/types/command.interface.d.ts +7 -0
  5. package/dist/core/entity/abstract-entity.d.ts +28 -1
  6. package/dist/core/entity/entity.d.ts +1 -1
  7. package/dist/core/entity/event.d.ts +13 -2
  8. package/dist/core/entity/exceptions.d.ts +8 -0
  9. package/dist/core/entity/types/entity.interface.d.ts +66 -2
  10. package/dist/core/entity/version.d.ts +13 -0
  11. package/dist/core/event-policy/event-policy.d.ts +25 -0
  12. package/dist/core/event-policy/exceptions.d.ts +11 -0
  13. package/dist/core/event-policy/publish.d.ts +17 -0
  14. package/dist/core/event-policy/publishers.d.ts +18 -0
  15. package/dist/core/event-policy/types/event-policy.interface.d.ts +72 -0
  16. package/dist/core/output/coerce.d.ts +9 -0
  17. package/dist/core/output/openapi-sink.d.ts +138 -38
  18. package/dist/core/output/status.d.ts +1 -1
  19. package/dist/core/parse/parse.d.ts +10 -0
  20. package/dist/core/parse/strip.d.ts +13 -0
  21. package/dist/core/repository/exceptions.d.ts +30 -0
  22. package/dist/core/repository/in-memory.d.ts +14 -1
  23. package/dist/core/repository/plan.d.ts +20 -0
  24. package/dist/core/repository/provider.d.ts +1 -1
  25. package/dist/core/repository/query.d.ts +9 -2
  26. package/dist/core/repository/repository.d.ts +40 -8
  27. package/dist/core/repository/state.d.ts +30 -2
  28. package/dist/core/repository/types/query.interface.d.ts +106 -5
  29. package/dist/core/repository/types/repository.interface.d.ts +106 -7
  30. package/dist/experimental.js +4 -4
  31. package/dist/{index-r78dsvtc.js → index-qxfkyd6k.js} +612 -612
  32. package/dist/{index-q6hggpzw.js → index-t5qwtyj5.js} +1807 -280
  33. package/dist/index.d.ts +22 -11
  34. package/dist/index.js +62 -14
  35. package/dist/std/blueprint-plugin.d.ts +6 -2
  36. package/dist/std/blueprint.d.ts +3 -0
  37. package/dist/std/entity.d.ts +2 -1
  38. package/dist/std/event-policies.d.ts +2 -0
  39. package/dist/std/properties/exceptions.d.ts +11 -0
  40. package/dist/std/properties/json.d.ts +54 -0
  41. package/dist/std/properties/time.d.ts +4 -0
  42. package/dist/std/use-cases/crud.d.ts +6 -4
  43. package/dist/std/use-cases/shared.d.ts +32 -4
  44. package/dist/std/use-cases/types.d.ts +100 -21
  45. package/dist/testing/index.d.ts +2 -0
  46. package/dist/testing/index.js +347 -2
  47. package/dist/testing/memory-publisher.d.ts +16 -0
  48. package/dist/testing/provider-contract.d.ts +14 -0
  49. package/package.json +3 -2
@@ -11,6 +11,8 @@ import {
11
11
  recordFieldOf2,
12
12
  recordNameOf2,
13
13
  declaredRecordNameOf,
14
+ fieldsOf,
15
+ isPlainObject,
14
16
  listOf2,
15
17
  isBlueprintList2,
16
18
  UNEXPECTED_ERROR2,
@@ -28,6 +30,13 @@ import {
28
30
  storeRecordValue,
29
31
  runRecordEffects,
30
32
  AbstractRecord2,
33
+ InvalidPropertyValue2,
34
+ exports_exceptions2,
35
+ propertyOf2,
36
+ exports_exceptions3,
37
+ optionalOf2,
38
+ blueprintBuilderOf2,
39
+ blueprintFromProperties,
31
40
  IdentifiedRecord,
32
41
  isVersion,
33
42
  satisfies,
@@ -38,14 +47,8 @@ import {
38
47
  recordBuilderOf2,
39
48
  pluginContextOf,
40
49
  recordOf3,
41
- declareRecord,
42
- optionalOf2,
43
- exports_exceptions2,
44
- InvalidPropertyValue2,
45
- exports_exceptions3,
46
- propertyOf2,
47
- blueprintBuilderOf2
48
- } from "./index-r78dsvtc.js";
50
+ declareRecord
51
+ } from "./index-qxfkyd6k.js";
49
52
 
50
53
  // src/core/environment/environment.ts
51
54
  var exports_environment = {};
@@ -71,35 +74,46 @@ __export(exports_exceptions4, {
71
74
  UnregisteredUseCaseDependency: () => UnregisteredUseCaseDependency2
72
75
  });
73
76
 
74
- // src/core/logger/exceptions.ts
77
+ // src/core/event-policy/exceptions.ts
75
78
  var exports_exceptions5 = {};
76
79
  __export(exports_exceptions5, {
77
- InvalidLogDeclaration: () => InvalidLogDeclaration2
80
+ InvalidEventPolicy: () => InvalidEventPolicy2,
81
+ PublisherNotBound: () => PublisherNotBound2
78
82
  });
79
83
 
80
- // src/core/entity/exceptions.ts
84
+ // src/core/logger/exceptions.ts
81
85
  var exports_exceptions6 = {};
82
86
  __export(exports_exceptions6, {
87
+ InvalidLogDeclaration: () => InvalidLogDeclaration2
88
+ });
89
+
90
+ // src/core/entity/exceptions.ts
91
+ var exports_exceptions7 = {};
92
+ __export(exports_exceptions7, {
83
93
  DeletedEntity: () => DeletedEntity2,
84
94
  ImmutableEntityKey: () => ImmutableEntityKey2,
95
+ InvalidEntityExpiry: () => InvalidEntityExpiry2,
85
96
  ReservedEntityKey: () => ReservedEntityKey2
86
97
  });
87
98
 
88
99
  // src/core/repository/exceptions.ts
89
- var exports_exceptions7 = {};
90
- __export(exports_exceptions7, {
100
+ var exports_exceptions8 = {};
101
+ __export(exports_exceptions8, {
91
102
  InvalidRepositoryCursor: () => InvalidRepositoryCursor2,
92
103
  InvalidRepositoryData: () => InvalidRepositoryData2,
93
104
  InvalidRepositoryDeclaration: () => InvalidRepositoryDeclaration2,
94
105
  InvalidRepositoryEntity: () => InvalidRepositoryEntity2,
106
+ InvalidRepositoryIncrement: () => InvalidRepositoryIncrement2,
95
107
  InvalidRepositoryQuery: () => InvalidRepositoryQuery2,
108
+ RepositoryConcurrencyConflict: () => RepositoryConcurrencyConflict2,
96
109
  RepositoryEntityNotFound: () => RepositoryEntityNotFound2,
97
- RepositoryUniqueViolation: () => RepositoryUniqueViolation2
110
+ RepositoryUniqueViolation: () => RepositoryUniqueViolation2,
111
+ RepositoryUsedOutsideApplication: () => RepositoryUsedOutsideApplication2
98
112
  });
99
113
 
100
114
  // src/core/use-case/exceptions.ts
101
- var exports_exceptions8 = {};
102
- __export(exports_exceptions8, {
115
+ var exports_exceptions9 = {};
116
+ __export(exports_exceptions9, {
103
117
  IncompatibleUseCasePlugin: () => IncompatibleUseCasePlugin2,
104
118
  InvalidUseCasePlugin: () => InvalidUseCasePlugin2,
105
119
  InvalidUseCaseStep: () => InvalidUseCaseStep2,
@@ -123,8 +137,8 @@ __export(exports_use_case, {
123
137
  });
124
138
 
125
139
  // src/core/command/exceptions.ts
126
- var exports_exceptions9 = {};
127
- __export(exports_exceptions9, {
140
+ var exports_exceptions10 = {};
141
+ __export(exports_exceptions10, {
128
142
  CommandEventFailed: () => CommandEventFailed2,
129
143
  InvalidCommandContext: () => InvalidCommandContext2,
130
144
  InvalidCommandDeclaration: () => InvalidCommandDeclaration2,
@@ -133,14 +147,14 @@ __export(exports_exceptions9, {
133
147
  });
134
148
 
135
149
  // src/core/openapi/exceptions.ts
136
- var exports_exceptions10 = {};
137
- __export(exports_exceptions10, {
150
+ var exports_exceptions11 = {};
151
+ __export(exports_exceptions11, {
138
152
  InvalidOpenAPIModel: () => InvalidOpenAPIModel2
139
153
  });
140
154
 
141
155
  // src/core/output/exceptions.ts
142
- var exports_exceptions11 = {};
143
- __export(exports_exceptions11, {
156
+ var exports_exceptions12 = {};
157
+ __export(exports_exceptions12, {
144
158
  DuplicateOperation: () => DuplicateOperation2,
145
159
  InvalidOutputStage: () => InvalidOutputStage2
146
160
  });
@@ -213,23 +227,6 @@ function isEnvironment2(value) {
213
227
  return typeof value === "object" && value !== null && value[Kind2] === "environment";
214
228
  }
215
229
 
216
- // src/core/exception/guard.ts
217
- function guard2(scope, run, resolve) {
218
- const handle = (error) => {
219
- const exception = Exception2.from(error, scope);
220
- const pass = () => {
221
- throw exception === error ? exception.propagate(scope) : exception;
222
- };
223
- return resolve ? resolve(exception, pass) : pass();
224
- };
225
- try {
226
- const result = run();
227
- return result instanceof Promise ? result.catch(handle) : result;
228
- } catch (error) {
229
- return handle(error);
230
- }
231
- }
232
-
233
230
  // src/core/application/exceptions.ts
234
231
  var InvalidApplicationDeclaration2 = exceptionOf2({
235
232
  layer: "application" /* Application */,
@@ -329,6 +326,53 @@ function setStartedApplication(application) {
329
326
  slot().current = application;
330
327
  }
331
328
 
329
+ // src/core/event-policy/exceptions.ts
330
+ var InvalidEventPolicy2 = exceptionOf2({
331
+ layer: "event" /* Event */,
332
+ name: "InvalidEventPolicy",
333
+ message: ({ reason }) => reason
334
+ });
335
+ var PublisherNotBound2 = exceptionOf2({
336
+ layer: "application" /* Application */,
337
+ name: "PublisherNotBound",
338
+ message: ({ publisher }) => `No publisher "${publisher}" is declared: add it to applicationOf({ publishers }).`
339
+ });
340
+
341
+ // src/core/event-policy/publishers.ts
342
+ var ApplicationPublishersKey = Symbol("afora.application.publishers");
343
+ var boundPublishers = new WeakMap;
344
+ function bindPublishers(command, publishers) {
345
+ boundPublishers.set(command, publishers);
346
+ }
347
+ function isEventPublisher(value) {
348
+ return typeof value === "object" && value !== null && typeof value.publish === "function";
349
+ }
350
+ function resolvePublisher(command, name, source) {
351
+ const publishers = boundPublishers.get(command) ?? startedApplication()?.[ApplicationPublishersKey];
352
+ const publisher = publishers && Object.hasOwn(publishers, name) ? publishers[name] : undefined;
353
+ if (!publisher) {
354
+ throw new PublisherNotBound2({ source, details: { publisher: name } });
355
+ }
356
+ return publisher;
357
+ }
358
+
359
+ // src/core/exception/guard.ts
360
+ function guard2(scope, run, resolve) {
361
+ const handle = (error) => {
362
+ const exception = Exception2.from(error, scope);
363
+ const pass = () => {
364
+ throw exception === error ? exception.propagate(scope) : exception;
365
+ };
366
+ return resolve ? resolve(exception, pass) : pass();
367
+ };
368
+ try {
369
+ const result = run();
370
+ return result instanceof Promise ? result.catch(handle) : result;
371
+ } catch (error) {
372
+ return handle(error);
373
+ }
374
+ }
375
+
332
376
  // src/core/logger/exceptions.ts
333
377
  var InvalidLogDeclaration2 = exceptionOf2({
334
378
  layer: "application" /* Application */,
@@ -528,34 +572,81 @@ var DeletedEntity2 = exceptionOf2({
528
572
  name: "DeletedEntity",
529
573
  message: ({ id }, source) => `Entity "${source}" of id "${id}" was destroyed and cannot change.`
530
574
  });
575
+ var InvalidEntityExpiry2 = exceptionOf2({
576
+ layer: "entity" /* Entity */,
577
+ name: "InvalidEntityExpiry",
578
+ message: ({ reason }, source) => `Invalid expires of entity "${source}": ${reason}`
579
+ });
580
+
581
+ // src/core/entity/version.ts
582
+ import { Type } from "typebox";
583
+ class EntityVersion2 extends propertyOf2({
584
+ name: "version",
585
+ schema: Type.Integer({ minimum: 1 }),
586
+ default: 1
587
+ }) {
588
+ }
589
+ var VersionIdentity = blueprintBuilderOf2()(() => ({
590
+ version: EntityVersion2
591
+ }));
531
592
 
532
593
  // src/core/entity/entity.ts
533
594
  function entityBuilderOf2({
534
595
  identity,
535
596
  generateId,
536
597
  now,
598
+ expiresAt,
537
599
  plugins: catalogs
538
600
  }) {
539
601
  const clock = Object.freeze({ generateId, now });
540
602
  const context = pluginContextOf(catalogs ?? []);
541
603
  return (blueprint, options = {}) => {
604
+ const lifetime = lifetimeOf(blueprint, options);
605
+ const expiring = lifetime !== undefined;
606
+ const versioned = options.versioned === true;
607
+ const added = [
608
+ ...versioned ? [ENTITY_VERSION_KEY] : [],
609
+ ...expiring ? [EXPIRES_AT] : []
610
+ ];
611
+ if (expiring && expiresAt === undefined) {
612
+ throw new InvalidEntityExpiry2({
613
+ source: options.name ?? "entity",
614
+ details: {
615
+ reason: "the entity builder has no expiresAt field, which expires: { after } needs."
616
+ }
617
+ });
618
+ }
542
619
  for (const key of Object.keys(blueprint.properties)) {
543
- if (RESERVED_ENTITY_KEYS2.includes(key)) {
620
+ if (RESERVED_ENTITY_KEYS2.includes(key) || added.includes(key)) {
544
621
  throw new ReservedEntityKey2({ source: "entity", details: { key } });
545
622
  }
546
623
  }
547
624
  for (const key of options.change ?? []) {
548
- if (ENTITY_IDENTITY_KEYS2.includes(key)) {
625
+ if (ENTITY_IDENTITY_KEYS2.includes(key) || added.includes(key)) {
549
626
  throw new ImmutableEntityKey2({ source: "entity", details: { key } });
550
627
  }
551
628
  }
552
- const DeclaredEntity = declareRecord(AbstractEntity2, identity.join(blueprint), { ...options, unique: ["id", ...options.unique ?? []] }, "entity", ENTITY_MEMBERS, context);
629
+ const DeclaredEntity = declareRecord(AbstractEntity2, identity.join(...versioned ? [VersionIdentity] : [], ...expiring ? [
630
+ blueprintFromProperties([], {
631
+ [EXPIRES_AT]: expiresAt
632
+ })
633
+ ] : [], blueprint), {
634
+ ...withoutStorage(options),
635
+ unique: ["id", ...options.unique ?? []]
636
+ }, "entity", ENTITY_MEMBERS, context);
553
637
  Object.defineProperty(DeclaredEntity, IdentifiedRecord, { value: true });
554
638
  Object.defineProperty(DeclaredEntity, EntityClock, { value: clock });
555
639
  Object.defineProperty(DeclaredEntity, EntityClassBrand, { value: true });
556
640
  Object.defineProperty(DeclaredEntity, EntityUniqueKeys2, {
557
641
  value: DeclaredEntity.options.unique
558
642
  });
643
+ Object.defineProperty(DeclaredEntity, EntityLifetime, { value: lifetime });
644
+ Object.defineProperty(DeclaredEntity, EntitySettings2, {
645
+ value: Object.freeze({
646
+ versioned,
647
+ expires: options.expires
648
+ })
649
+ });
559
650
  Object.defineProperty(DeclaredEntity, EntityEventSettings, {
560
651
  value: Object.freeze({
561
652
  onCreate: options.onCreate,
@@ -572,6 +663,59 @@ function isEntityClass2(value) {
572
663
  function isEntity2(value) {
573
664
  return value instanceof AbstractEntity2;
574
665
  }
666
+ function withoutStorage(options) {
667
+ const { versioned: _versioned, expires: _expires, ...rest } = options;
668
+ return rest;
669
+ }
670
+ var DURATION_UNITS = {
671
+ days: 86400000,
672
+ hours: 3600000,
673
+ minutes: 60000,
674
+ seconds: 1000
675
+ };
676
+ function isDateField(field) {
677
+ if (!isProperty2(field))
678
+ return false;
679
+ const schema = field.type.schema.type.Type();
680
+ return schema.type === "string" && (schema.format === "date-time" || schema.format === "date");
681
+ }
682
+ function lifetimeOf(blueprint, options) {
683
+ const { expires } = options;
684
+ if (expires === undefined)
685
+ return;
686
+ const fail = (reason) => {
687
+ throw new InvalidEntityExpiry2({
688
+ source: options.name ?? "entity",
689
+ details: { reason }
690
+ });
691
+ };
692
+ const plain = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
693
+ if (!plain(expires) || Object.keys(expires).length !== 1)
694
+ return fail("expected { after: duration } or { at: key }.");
695
+ if ("at" in expires) {
696
+ const { at } = expires;
697
+ if (typeof at !== "string" || !Object.hasOwn(blueprint.properties, at))
698
+ return fail(`"${String(at)}" is not a key of the blueprint.`);
699
+ if (!isDateField(blueprint.properties[at]))
700
+ return fail(`"${at}" does not hold a date.`);
701
+ return;
702
+ }
703
+ if (!("after" in expires)) {
704
+ return fail("expected { after: duration } or { at: key }.");
705
+ }
706
+ const { after } = expires;
707
+ if (!plain(after) || Object.keys(after).length === 0)
708
+ return fail("after must be a non-empty duration, e.g. { days: 400 }.");
709
+ let total = 0;
710
+ for (const [unit, amount] of Object.entries(after)) {
711
+ if (!Object.hasOwn(DURATION_UNITS, unit))
712
+ return fail(`"${unit}" is not a unit of a duration (days, hours, minutes, seconds).`);
713
+ if (typeof amount !== "number" || !Number.isFinite(amount) || amount <= 0)
714
+ return fail(`${unit} must be a positive finite number.`);
715
+ total += amount * DURATION_UNITS[unit];
716
+ }
717
+ return total;
718
+ }
575
719
 
576
720
  // src/core/entity/event.ts
577
721
  var EntityClassBrand = Symbol("entityClass");
@@ -579,14 +723,20 @@ var EventBrand = Symbol("event");
579
723
  var EntityEventSettings = Symbol("entityEvents");
580
724
  var definitions = new WeakMap;
581
725
  var queues = new WeakMap;
726
+ var withHandler = new WeakSet;
582
727
  function eventOf2(name, entityClass, handler) {
583
728
  if (!isEntityClass2(entityClass)) {
584
729
  throw new TypeError("An event requires an entity class");
585
730
  }
731
+ if (handler !== undefined && typeof handler !== "function") {
732
+ throw new TypeError(`The handler of event ${name} must be a function`);
733
+ }
586
734
  const event = Object.freeze({
587
735
  name,
588
736
  [EventBrand]: true
589
737
  });
738
+ if (handler)
739
+ withHandler.add(event);
590
740
  definitions.set(event, (entity) => {
591
741
  if (!(entity instanceof entityClass)) {
592
742
  throw new TypeError(`Incompatible entity for event ${name}`);
@@ -595,6 +745,8 @@ function eventOf2(name, entityClass, handler) {
595
745
  name,
596
746
  entity,
597
747
  async handle(context) {
748
+ if (!handler)
749
+ return;
598
750
  await handler(entity, {
599
751
  logger: context?.logger ?? resolveLogger(undefined, undefined) ?? silentLogger
600
752
  });
@@ -603,6 +755,23 @@ function eventOf2(name, entityClass, handler) {
603
755
  });
604
756
  return event;
605
757
  }
758
+ function eventHasHandler(event) {
759
+ return withHandler.has(event);
760
+ }
761
+ function lifecycleEventsOf(entityClass) {
762
+ const settings = entityClass[EntityEventSettings];
763
+ const found = {};
764
+ for (const cycle of ["onCreate", "onUpdate", "onDelete"]) {
765
+ const reference = settings?.[cycle];
766
+ if (reference === undefined)
767
+ continue;
768
+ const events = typeof reference === "function" ? reference() : reference;
769
+ const list = Array.isArray(events) ? events : [events];
770
+ if (list.length > 0)
771
+ found[cycle] = list;
772
+ }
773
+ return found;
774
+ }
606
775
  function enqueueEvent(entity, event) {
607
776
  const bind = definitions.get(event);
608
777
  if (!bind)
@@ -644,6 +813,30 @@ var RESERVED_ENTITY_KEYS2 = Object.freeze([
644
813
  ]);
645
814
  var EntityClock = Symbol("entityClock");
646
815
  var EntityUniqueKeys2 = Symbol("entityUniqueKeys");
816
+ var EntitySettings2 = Symbol("entitySettings");
817
+ var EntityLifetime = Symbol("entityLifetime");
818
+ var EXPIRES_AT = "expiresAt";
819
+ var ENTITY_VERSION_KEY = "version";
820
+ function isVersionedEntityClass(entityClass) {
821
+ if (typeof entityClass !== "function")
822
+ return false;
823
+ return entityClass[EntitySettings2]?.versioned === true;
824
+ }
825
+ function identityKeysOf(entityClass) {
826
+ const keys = [...ENTITY_IDENTITY_KEYS2];
827
+ if (isVersionedEntityClass(entityClass))
828
+ keys.push(ENTITY_VERSION_KEY);
829
+ if (typeof entityClass === "function" && entityClass[EntityLifetime] !== undefined) {
830
+ keys.push(EXPIRES_AT);
831
+ }
832
+ return keys;
833
+ }
834
+ function isIdentityKey(Entity, key) {
835
+ return identityKeysOf(Entity).includes(key);
836
+ }
837
+ function storeEntityVersion(entity, version) {
838
+ storeRecordValue(entity, ENTITY_VERSION_KEY, version);
839
+ }
647
840
  function identityOf(entity) {
648
841
  return entity;
649
842
  }
@@ -658,23 +851,34 @@ function assertAlive(entity) {
658
851
  });
659
852
  }
660
853
  }
661
- function withIdentity(value, clock) {
854
+ function withIdentity(value, clock, lifetime, versioned) {
662
855
  if (typeof value !== "object" || value === null || Array.isArray(value)) {
663
856
  return value;
664
857
  }
665
- const { id, createdAt } = value;
666
- if (id !== undefined && createdAt !== undefined)
858
+ const { id, createdAt, expiresAt, version } = value;
859
+ const expiring = lifetime !== undefined && expiresAt === undefined;
860
+ const unversioned = versioned && version === undefined;
861
+ if (id !== undefined && createdAt !== undefined && !expiring && !unversioned)
667
862
  return value;
668
- return {
863
+ const created = createdAt === undefined ? clock.now() : createdAt;
864
+ const identity = {
669
865
  ...value,
670
866
  id: id === undefined ? clock.generateId() : id,
671
- createdAt: createdAt === undefined ? clock.now() : createdAt
867
+ createdAt: created,
868
+ ...unversioned ? { [ENTITY_VERSION_KEY]: 1 } : {}
672
869
  };
870
+ if (expiring) {
871
+ const start = typeof created === "string" ? Date.parse(created) : NaN;
872
+ if (!Number.isNaN(start)) {
873
+ identity[EXPIRES_AT] = new Date(start + lifetime).toISOString();
874
+ }
875
+ }
876
+ return identity;
673
877
  }
674
878
 
675
879
  class AbstractEntity2 extends AbstractRecord2 {
676
880
  constructor(value) {
677
- super(withIdentity(value, new.target[EntityClock]));
881
+ super(withIdentity(value, new.target[EntityClock], new.target[EntityLifetime], isVersionedEntityClass(new.target)));
678
882
  const input = value;
679
883
  if (input.id === undefined && input.createdAt === undefined) {
680
884
  enqueueLifecycle(this, "onCreate");
@@ -701,7 +905,7 @@ class AbstractEntity2 extends AbstractRecord2 {
701
905
  }
702
906
  set(key, value) {
703
907
  const name = String(key);
704
- if (ENTITY_IDENTITY_KEYS2.includes(name)) {
908
+ if (isIdentityKey(this.constructor, name)) {
705
909
  throw new ImmutableEntityKey2({
706
910
  source: recordNameOf2(this.constructor),
707
911
  details: { key: name }
@@ -755,11 +959,34 @@ var RepositoryUniqueViolation2 = exceptionOf2({
755
959
  name: "RepositoryUniqueViolation",
756
960
  message: ({ key }, source) => `"${source}" already holds an entity with this "${key}".`
757
961
  });
962
+ var RepositoryUsedOutsideApplication2 = exceptionOf2({
963
+ layer: "repository" /* Repository */,
964
+ name: "RepositoryUsedOutsideApplication",
965
+ message: ({
966
+ application,
967
+ repository
968
+ }, source) => `"${source}" is bound to the application "${application}", and its declaration no longer reads or writes: use app.repositories.${repository}, run its use cases with app.run, or give the repository of the application to execute.`
969
+ });
758
970
  var RepositoryEntityNotFound2 = exceptionOf2({
759
971
  layer: "repository" /* Repository */,
760
972
  name: "RepositoryEntityNotFound",
761
973
  message: ({ id }, source) => `"${source}" holds no entity of id "${id}".`
762
974
  });
975
+ var RepositoryConcurrencyConflict2 = exceptionOf2({
976
+ layer: "repository" /* Repository */,
977
+ name: "RepositoryConcurrencyConflict",
978
+ message: ({
979
+ id,
980
+ expected
981
+ }, source) => `"${source}" holds another version of "${id}" than ${expected}: it changed since it was read.`
982
+ });
983
+ var InvalidRepositoryIncrement2 = exceptionOf2({
984
+ layer: "repository" /* Repository */,
985
+ name: "InvalidRepositoryIncrement",
986
+ message: ({
987
+ reason
988
+ }) => reason
989
+ });
763
990
 
764
991
  // src/core/repository/cursor.ts
765
992
  var VERSION = 1;
@@ -902,7 +1129,14 @@ function plain(value) {
902
1129
  function isMembership(value) {
903
1130
  return plain(value) && Object.keys(value).length === 1 && Array.isArray(value.in);
904
1131
  }
905
- var QUERY_OPTIONS = ["where", "orderBy", "limit", "offset", "withDeleted"];
1132
+ var QUERY_OPTIONS = [
1133
+ "where",
1134
+ "orderBy",
1135
+ "limit",
1136
+ "offset",
1137
+ "withDeleted",
1138
+ "withExpired"
1139
+ ];
906
1140
  function prepareQuery(context, input = {}, map = (_key, value) => value, options = QUERY_OPTIONS) {
907
1141
  const fail = (reason) => {
908
1142
  throw new InvalidRepositoryQuery2({
@@ -927,6 +1161,8 @@ function prepareQuery(context, input = {}, map = (_key, value) => value, options
927
1161
  }
928
1162
  if (input.withDeleted !== undefined && typeof input.withDeleted !== "boolean")
929
1163
  fail("Expected withDeleted to be boolean.");
1164
+ if (input.withExpired !== undefined && typeof input.withExpired !== "boolean")
1165
+ fail("Expected withExpired to be boolean.");
930
1166
  let where;
931
1167
  if (input.where !== undefined) {
932
1168
  if (!plain(input.where))
@@ -967,8 +1203,18 @@ function sameValue(a, b) {
967
1203
  const keys = Object.keys(a);
968
1204
  return keys.length === Object.keys(b).length && keys.every((key) => Object.hasOwn(b, key) && sameValue(a[key], b[key]));
969
1205
  }
970
- function matchesQuery(data, query) {
971
- return (query.withDeleted === true || data.deletedAt === undefined) && Object.entries(query.where ?? {}).every(([key, value]) => isMembership(value) ? value.in.some((candidate) => sameValue(data[key], candidate)) : sameValue(data[key], value));
1206
+ function isExpired(value, now) {
1207
+ if (value === undefined || value === null)
1208
+ return false;
1209
+ const at = value instanceof Date ? value.getTime() : typeof value === "string" ? Date.parse(value) : Number.NaN;
1210
+ return !Number.isNaN(at) && at <= now;
1211
+ }
1212
+ function nowOf(query) {
1213
+ const now = query.now === undefined ? Number.NaN : Date.parse(query.now);
1214
+ return Number.isNaN(now) ? Date.now() : now;
1215
+ }
1216
+ function matchesQuery(data, query, expiry, now = expiry === undefined ? 0 : nowOf(query)) {
1217
+ return (query.withDeleted === true || data.deletedAt === undefined) && (expiry === undefined || query.withExpired === true || !isExpired(data[expiry], now)) && Object.entries(query.where ?? {}).every(([key, value]) => isMembership(value) ? value.in.some((candidate) => sameValue(data[key], candidate)) : sameValue(data[key], value));
972
1218
  }
973
1219
  function codePoints(text) {
974
1220
  return Array.from(text, (char) => char.codePointAt(0));
@@ -1018,8 +1264,9 @@ function rowsAfter(rows, orders, after) {
1018
1264
  const position = Object.fromEntries(orders.map(({ key }, index) => [key, after[index]]));
1019
1265
  return rows.filter((row) => compareRows(orders, row, position) > 0);
1020
1266
  }
1021
- function selectRows(rows, query) {
1022
- const ordered = orderRows([...rows].filter((data) => matchesQuery(data, query)), query);
1267
+ function selectRows(rows, query, expiry) {
1268
+ const now = expiry === undefined ? 0 : nowOf(query);
1269
+ const ordered = orderRows([...rows].filter((data) => matchesQuery(data, query, expiry, now)), query);
1023
1270
  const matched = query.after === undefined ? ordered : rowsAfter(ordered, query.orderBy ?? [], query.after);
1024
1271
  const start = query.offset ?? 0;
1025
1272
  return matched.slice(start, query.limit === undefined ? undefined : start + query.limit);
@@ -1072,15 +1319,18 @@ function scopeOf(initial, root) {
1072
1319
  }
1073
1320
  };
1074
1321
  const provider = {
1075
- capabilities: Object.freeze({ transactions: true, keyset: true }),
1322
+ capabilities: Object.freeze({
1323
+ transactions: true,
1324
+ keyset: true,
1325
+ versioning: true,
1326
+ atomic: true,
1327
+ expiry: true
1328
+ }),
1076
1329
  findOne: (context, query = {}) => whenIdle(() => {
1077
- const [first] = selectRows(tableOf(context).values(), {
1078
- ...query,
1079
- limit: Math.min(query.limit ?? 1, 1)
1080
- });
1330
+ const [first] = selectRows(tableOf(context).values(), { ...query, limit: Math.min(query.limit ?? 1, 1) }, context.expiry?.key);
1081
1331
  return first === undefined ? undefined : copy(first);
1082
1332
  }),
1083
- findMany: (context, query = {}) => whenIdle(() => selectRows(tableOf(context).values(), query).map(copy)),
1333
+ findMany: (context, query = {}) => whenIdle(() => selectRows(tableOf(context).values(), query, context.expiry?.key).map(copy)),
1084
1334
  insert: (context, data) => whenIdle(() => {
1085
1335
  const table = tableOf(context);
1086
1336
  if (table.has(idOf(data))) {
@@ -1092,17 +1342,61 @@ function scopeOf(initial, root) {
1092
1342
  assertUnique(context, data);
1093
1343
  table.set(idOf(data), copy(data));
1094
1344
  }),
1095
- update: (context, data) => whenIdle(() => {
1345
+ update: (context, data, options = {}) => whenIdle(() => {
1096
1346
  const table = tableOf(context);
1097
- if (!table.has(idOf(data))) {
1347
+ const stored = table.get(idOf(data));
1348
+ if (!stored) {
1098
1349
  throw new RepositoryEntityNotFound2({
1099
1350
  source: context.name,
1100
1351
  details: { id: idOf(data) }
1101
1352
  });
1102
1353
  }
1354
+ const { expectedVersion: expected } = options;
1355
+ if (expected !== undefined && stored.version !== expected) {
1356
+ throw new RepositoryConcurrencyConflict2({
1357
+ source: context.name,
1358
+ details: {
1359
+ id: idOf(data),
1360
+ expected,
1361
+ ...typeof stored.version === "number" ? { actual: stored.version } : {}
1362
+ }
1363
+ });
1364
+ }
1103
1365
  assertUnique(context, data);
1104
1366
  table.set(idOf(data), copy(data));
1105
1367
  }),
1368
+ increment: (context, id, deltas, options = {}) => whenIdle(() => {
1369
+ const table = tableOf(context);
1370
+ const found = table.get(id);
1371
+ const expiry = context.expiry?.key;
1372
+ const expired = found !== undefined && expiry !== undefined && !matchesQuery(found, { withDeleted: true, now: options.now }, expiry);
1373
+ const stored = expired ? undefined : found;
1374
+ if (!stored || stored.deletedAt != null) {
1375
+ if (!options.create) {
1376
+ throw new RepositoryEntityNotFound2({
1377
+ source: context.name,
1378
+ details: { id }
1379
+ });
1380
+ }
1381
+ if (stored) {
1382
+ throw new RepositoryUniqueViolation2({
1383
+ source: context.name,
1384
+ details: { key: "id" }
1385
+ });
1386
+ }
1387
+ const created = { ...options.create, id };
1388
+ assertUnique(context, created);
1389
+ table.set(id, copy(created));
1390
+ return copy(created);
1391
+ }
1392
+ const next = { ...stored, ...options.set };
1393
+ for (const [key, delta] of Object.entries(deltas)) {
1394
+ const value = stored[key];
1395
+ next[key] = (typeof value === "number" ? value : 0) + delta;
1396
+ }
1397
+ table.set(id, copy(next));
1398
+ return copy(next);
1399
+ }),
1106
1400
  remove: (context, id) => whenIdle(() => {
1107
1401
  if (!tableOf(context).delete(id)) {
1108
1402
  throw new RepositoryEntityNotFound2({
@@ -1112,9 +1406,11 @@ function scopeOf(initial, root) {
1112
1406
  }
1113
1407
  }),
1114
1408
  count: (context, query = {}) => whenIdle(() => {
1409
+ const expiry = context.expiry?.key;
1410
+ const now = expiry === undefined ? 0 : nowOf(query);
1115
1411
  let total = 0;
1116
1412
  for (const data of tableOf(context).values()) {
1117
- if (matchesQuery(data, query))
1413
+ if (matchesQuery(data, query, expiry, now))
1118
1414
  total++;
1119
1415
  }
1120
1416
  return total;
@@ -1155,6 +1451,35 @@ function inMemoryProvider2() {
1155
1451
  });
1156
1452
  }
1157
1453
 
1454
+ // src/core/repository/plan.ts
1455
+ var ID = Object.freeze({ access: "id" });
1456
+ var UNIQUE = Object.freeze({ access: "unique" });
1457
+ var SCAN = Object.freeze({ access: "scan" });
1458
+ function isMembership2(value) {
1459
+ return typeof value === "object" && value !== null && !Array.isArray(value) && Object.keys(value).length === 1 && Array.isArray(value.in);
1460
+ }
1461
+ function planQuery(repository, query = {}) {
1462
+ const where = query.where ?? {};
1463
+ const keys = Object.keys(where);
1464
+ if (keys.includes("id"))
1465
+ return ID;
1466
+ if (keys.some((key) => repository.unique.includes(key)))
1467
+ return UNIQUE;
1468
+ const equal = new Set(keys.filter((key) => !isMembership2(where[key])));
1469
+ const first = query.orderBy?.[0]?.key;
1470
+ let best;
1471
+ for (const entry of Object.entries(repository.indexes ?? {})) {
1472
+ const [, index] = entry;
1473
+ if (!index.where.every((key) => equal.has(key)))
1474
+ continue;
1475
+ if (first !== undefined && index.orderBy !== first)
1476
+ continue;
1477
+ if (best === undefined || index.where.length > best[1].where.length)
1478
+ best = entry;
1479
+ }
1480
+ return best === undefined ? SCAN : Object.freeze({ access: "index", index: best[0] });
1481
+ }
1482
+
1158
1483
  // src/core/repository/state.ts
1159
1484
  var states = new WeakMap;
1160
1485
  function findState(repository) {
@@ -1173,11 +1498,22 @@ function stateOf(repository) {
1173
1498
  throw new TypeError("Expected a repository made by repositoryOf");
1174
1499
  return state;
1175
1500
  }
1501
+ function expiryKeyOf(entity) {
1502
+ const expires = entity[EntitySettings2]?.expires;
1503
+ if (expires === undefined)
1504
+ return;
1505
+ return "at" in expires ? expires.at : EXPIRES_AT;
1506
+ }
1176
1507
  function contextOf(state) {
1177
- state.context ??= Object.freeze({
1508
+ if (state.context)
1509
+ return state.context;
1510
+ const key = expiryKeyOf(state.entity);
1511
+ state.context = Object.freeze({
1178
1512
  entity: state.entity,
1179
1513
  name: state.name,
1180
1514
  unique: state.entity[EntityUniqueKeys2],
1515
+ indexes: state.indexes,
1516
+ ...key === undefined ? {} : { expiry: Object.freeze({ key }) },
1181
1517
  env: loadEnvironments(state.environment)
1182
1518
  });
1183
1519
  return state.context;
@@ -1191,6 +1527,8 @@ function inspectRepository(repository) {
1191
1527
  entity: state.entity,
1192
1528
  actions: state.actions,
1193
1529
  deletion: state.deletion,
1530
+ indexes: state.indexes,
1531
+ scans: state.scans,
1194
1532
  explicitProvider: state.explicitProvider,
1195
1533
  provider: state.explicitProvider ? state.provider : undefined,
1196
1534
  environments: environment === undefined ? [] : Array.isArray(environment) ? environment : [environment],
@@ -1210,33 +1548,53 @@ function relatedRepositoryOf(repository, Entity) {
1210
1548
  const related = findState(repository)?.relations.get(Entity);
1211
1549
  return related ? declared.get(declarationOf(related)) : undefined;
1212
1550
  }
1213
- function bindState(state, provider, instances, bound = new Map) {
1551
+ function bindState(state, provider, instances, logger, bound = new Map) {
1214
1552
  const done = bound.get(state);
1215
1553
  if (done)
1216
1554
  return done;
1217
1555
  const copy = {
1218
1556
  ...state,
1557
+ logger,
1219
1558
  provider: state.explicitProvider ? instances?.get(state.provider) ?? state.provider : provider ?? state.provider,
1220
1559
  context: undefined,
1221
1560
  relations: new Map([...state.relations].map(([target, related]) => [
1222
1561
  target,
1223
- bindState(related, provider, instances, bound)
1562
+ bindState(related, provider, instances, logger, bound)
1224
1563
  ]))
1225
1564
  };
1226
1565
  origins.set(copy, declarationOf(state));
1227
1566
  bound.set(state, copy);
1228
1567
  return copy;
1229
1568
  }
1230
- function bindRepository(repository, provider, instances) {
1569
+ var owners = new WeakMap;
1570
+ function assertUnbound(state) {
1571
+ const owner = owners.get(state);
1572
+ if (owner) {
1573
+ throw new RepositoryUsedOutsideApplication2({
1574
+ source: state.name,
1575
+ details: owner
1576
+ });
1577
+ }
1578
+ }
1579
+ function bindRepository(repository, provider, instances, binding) {
1231
1580
  const state = stateOf(repository);
1232
1581
  const Declared = repository.constructor;
1233
1582
 
1234
1583
  class Bound extends Declared {
1235
1584
  }
1236
1585
  Object.defineProperty(Bound, "name", { value: Declared.name });
1237
- states.set(Bound, bindState(state, provider, instances));
1586
+ states.set(Bound, bindState(state, provider, instances, binding?.logger));
1587
+ if (binding) {
1588
+ owners.set(declarationOf(state), {
1589
+ application: binding.application,
1590
+ repository: binding.key
1591
+ });
1592
+ }
1238
1593
  return Object.freeze(new Bound);
1239
1594
  }
1595
+ function providerOfRepository(repository) {
1596
+ return findState(repository)?.provider;
1597
+ }
1240
1598
 
1241
1599
  // src/core/repository/references.ts
1242
1600
  function isPlain(value) {
@@ -1354,7 +1712,8 @@ function parseEntity(state, data) {
1354
1712
  function loadByIds(state, ids, scope) {
1355
1713
  return scopedProvider(scope, state.provider).findMany(contextOf(state), {
1356
1714
  where: { id: { in: ids } },
1357
- withDeleted: true
1715
+ withDeleted: true,
1716
+ withExpired: true
1358
1717
  });
1359
1718
  }
1360
1719
  async function resolveReferences(state, datas, session, scope) {
@@ -1445,50 +1804,125 @@ class AbstractRepository2 {
1445
1804
  }
1446
1805
  function action(method) {
1447
1806
  return function(...args) {
1448
- return guard2({ layer: "repository" /* Repository */, source: stateOf(this).name }, () => method(this, ...args));
1807
+ const state = stateOf(this);
1808
+ return guard2({ layer: "repository" /* Repository */, source: state.name }, async () => {
1809
+ assertUnbound(state);
1810
+ return method(this, ...args);
1811
+ });
1449
1812
  };
1450
1813
  }
1451
1814
  function queryValueOf(context, key, value) {
1452
1815
  const properties = context.entity.blueprint.properties;
1453
1816
  return recordFieldDataOf(properties[key], value, REFERENCES);
1454
1817
  }
1818
+ function expiryOf(self, withExpired) {
1819
+ const { context, provider } = self;
1820
+ if (context.expiry === undefined)
1821
+ return {};
1822
+ if (provider.capabilities?.expiry !== true) {
1823
+ throw new InvalidRepositoryQuery2({
1824
+ source: context.name,
1825
+ details: {
1826
+ reason: "The provider does not leave expired records out (capabilities.expiry), which an entity with expires needs."
1827
+ }
1828
+ });
1829
+ }
1830
+ const clock = context.entity[EntityClock];
1831
+ return { withExpired: withExpired ?? false, now: clock.now() };
1832
+ }
1455
1833
  function queryOf(self, query) {
1456
1834
  const prepared = prepareQuery(self.context, query, (key, value) => queryValueOf(self.context, key, value));
1457
1835
  return {
1458
1836
  ...prepared,
1459
- withDeleted: prepared.withDeleted ?? stateOf(self).deletion === "hard"
1837
+ withDeleted: prepared.withDeleted ?? stateOf(self).deletion === "hard",
1838
+ ...expiryOf(self, prepared.withExpired)
1460
1839
  };
1461
1840
  }
1462
1841
  var equals = (self, key, value) => queryOf(self, { where: { [key]: value } });
1842
+ function planned(self, method, query, orderBy = query.orderBy) {
1843
+ const state = stateOf(self);
1844
+ const plan = planQuery(self.context, { where: query.where, orderBy });
1845
+ if (plan.access === "scan" && state.scans !== "allow") {
1846
+ const fields = {
1847
+ repository: state.name,
1848
+ method,
1849
+ where: Object.keys(query.where ?? {}),
1850
+ orderBy: (orderBy ?? []).map(({ key }) => key)
1851
+ };
1852
+ if (state.scans === "reject") {
1853
+ throw new InvalidRepositoryQuery2({
1854
+ source: state.name,
1855
+ details: {
1856
+ reason: `"${method}" matches no index of ${state.name} and would read every row (scans: "reject").`,
1857
+ scan: true,
1858
+ ...fields
1859
+ }
1860
+ });
1861
+ }
1862
+ const scope = self[ScopeKey];
1863
+ writeLog(resolveLogger(scope?.logger ?? state.logger, undefined), "warn", "repository-scan", fields);
1864
+ }
1865
+ return plan.index === undefined ? query : { ...query, index: plan.index };
1866
+ }
1463
1867
  var findBy = (key) => action(async (self, value) => {
1464
1868
  const data = await self.provider.findOne(self.context, equals(self, key, value));
1465
1869
  return data === undefined ? undefined : await self.hydrate(data);
1466
1870
  });
1467
- var findManyBy = (key) => action(async (self, value) => self.hydrateMany(await self.provider.findMany(self.context, equals(self, key, value))));
1468
- var findMany = action(async (self, query) => self.hydrateMany(await self.provider.findMany(self.context, queryOf(self, query))));
1469
- var exists = action(async (self, query) => await self.provider.findOne(self.context, queryOf(self, query)) !== undefined);
1871
+ var findManyBy = (key) => action(async (self, value) => self.hydrateMany(await self.provider.findMany(self.context, planned(self, `findManyBy${capitalize(key)}`, equals(self, key, value)))));
1872
+ var findMany = action(async (self, query) => self.hydrateMany(await self.provider.findMany(self.context, planned(self, "findMany", queryOf(self, query)))));
1873
+ var exists = action(async (self, query) => await self.provider.findOne(self.context, planned(self, "exists", queryOf(self, query))) !== undefined);
1874
+ var EXPLAIN_OPTIONS = [
1875
+ "where",
1876
+ "orderBy",
1877
+ "limit",
1878
+ "offset",
1879
+ "withDeleted",
1880
+ "after"
1881
+ ];
1882
+ var explain = action(async (self, query = {}) => {
1883
+ const prepared = prepareQuery(self.context, query, (key, value) => queryValueOf(self.context, key, value), EXPLAIN_OPTIONS);
1884
+ return planQuery(self.context, prepared);
1885
+ });
1470
1886
  var findPage = action(async (self, query) => {
1471
1887
  const {
1472
1888
  orderBy,
1473
1889
  where,
1474
1890
  withDeleted,
1891
+ withExpired,
1892
+ now,
1475
1893
  limit = 20,
1476
- offset = 0
1477
- } = queryOf(self, query);
1894
+ offset = 0,
1895
+ index
1896
+ } = planned(self, "findPage", queryOf(self, query));
1897
+ const expiry = withExpired === undefined ? {} : { withExpired, now };
1478
1898
  const [rows, total] = await Promise.all([
1479
1899
  self.provider.findMany(self.context, {
1480
1900
  where,
1481
1901
  orderBy,
1482
1902
  withDeleted,
1903
+ ...expiry,
1483
1904
  limit,
1484
- offset
1905
+ offset,
1906
+ ...index === undefined ? {} : { index }
1485
1907
  }),
1486
- self.provider.count(self.context, { where, withDeleted })
1908
+ self.provider.count(self.context, {
1909
+ where,
1910
+ withDeleted,
1911
+ ...expiry,
1912
+ ...index === undefined ? {} : { index }
1913
+ })
1487
1914
  ]);
1488
1915
  return { items: await self.hydrateMany(rows), total, limit, offset };
1489
1916
  });
1490
- var CURSOR_OPTIONS = ["where", "orderBy", "limit", "withDeleted", "after"];
1491
- var findCursor = action(async (self, query = {}) => {
1917
+ var CURSOR_OPTIONS = [
1918
+ "where",
1919
+ "orderBy",
1920
+ "limit",
1921
+ "withDeleted",
1922
+ "withExpired",
1923
+ "after"
1924
+ ];
1925
+ function cursorReadOf(self, query) {
1492
1926
  const { context } = self;
1493
1927
  const fail = (reason) => {
1494
1928
  throw new InvalidRepositoryQuery2({
@@ -1497,7 +1931,6 @@ var findCursor = action(async (self, query = {}) => {
1497
1931
  });
1498
1932
  };
1499
1933
  const prepared = prepareQuery(context, query, (key, value) => queryValueOf(context, key, value), CURSOR_OPTIONS);
1500
- const { after } = query;
1501
1934
  const { where, limit = 20 } = prepared;
1502
1935
  if (limit < 1)
1503
1936
  fail("Expected a positive integer for limit.");
@@ -1510,11 +1943,27 @@ var findCursor = action(async (self, query = {}) => {
1510
1943
  }
1511
1944
  if (self.provider.capabilities?.keyset !== true)
1512
1945
  fail("The provider does not read by cursor (capabilities.keyset).");
1946
+ const { index } = planned(self, "findCursor", { where }, declared);
1947
+ expiryOf(self, prepared.withExpired);
1948
+ return {
1949
+ where,
1950
+ orderBy,
1951
+ withDeleted,
1952
+ withExpired: prepared.withExpired,
1953
+ limit,
1954
+ ...index === undefined ? {} : { index }
1955
+ };
1956
+ }
1957
+ async function readCursor(self, read, after) {
1958
+ const { context } = self;
1959
+ const { where, orderBy, withDeleted, limit, index } = read;
1513
1960
  const rows = await self.provider.findMany(context, {
1514
1961
  where,
1515
1962
  orderBy,
1516
1963
  withDeleted,
1964
+ ...expiryOf(self, read.withExpired),
1517
1965
  limit: limit + 1,
1966
+ ...index === undefined ? {} : { index },
1518
1967
  ...after === undefined ? {} : { after: decodeCursor(context.name, orderBy, after) }
1519
1968
  });
1520
1969
  const page = rows.slice(0, limit);
@@ -1524,6 +1973,49 @@ var findCursor = action(async (self, query = {}) => {
1524
1973
  limit,
1525
1974
  next: rows.length > limit && last !== undefined ? encodeCursor(orderBy, last) : undefined
1526
1975
  };
1976
+ }
1977
+ var findCursor = action(async (self, query = {}) => readCursor(self, cursorReadOf(self, query), query.after));
1978
+ function stopperOf(source, until) {
1979
+ if (until === undefined)
1980
+ return () => false;
1981
+ if (typeof until === "function")
1982
+ return () => Boolean(until());
1983
+ const deadline = until instanceof Date ? until.getTime() : typeof until === "number" ? until : Number.NaN;
1984
+ if (Number.isNaN(deadline)) {
1985
+ throw new InvalidRepositoryQuery2({
1986
+ source,
1987
+ details: {
1988
+ reason: "Expected until to be a Date, epoch milliseconds or a function."
1989
+ }
1990
+ });
1991
+ }
1992
+ return () => Date.now() >= deadline;
1993
+ }
1994
+ var forEachPage = action(async (self, query, fn, options) => {
1995
+ const { context } = self;
1996
+ if (typeof fn !== "function") {
1997
+ throw new InvalidRepositoryQuery2({
1998
+ source: context.name,
1999
+ details: { reason: "Expected a function to run on each page." }
2000
+ });
2001
+ }
2002
+ const read = cursorReadOf(self, query ?? {});
2003
+ const stop = stopperOf(context.name, options?.until);
2004
+ let after = query?.after;
2005
+ let pages = 0;
2006
+ let items = 0;
2007
+ while (!stop()) {
2008
+ const page = await readCursor(self, read, after);
2009
+ if (page.items.length > 0) {
2010
+ await fn(page.items, { index: pages, next: page.next });
2011
+ pages += 1;
2012
+ items += page.items.length;
2013
+ }
2014
+ if (page.next === undefined)
2015
+ return { done: true, pages, items };
2016
+ after = page.next;
2017
+ }
2018
+ return { done: false, next: after, pages, items };
1527
2019
  });
1528
2020
  var create = action(async (self, input) => {
1529
2021
  const { entity: Entity } = self.context;
@@ -1531,8 +2023,119 @@ var create = action(async (self, input) => {
1531
2023
  await self.provider.insert(self.context, self.dataOf(entity));
1532
2024
  return entity;
1533
2025
  });
2026
+ function nowOf2(context) {
2027
+ return context.entity[EntityClock].now();
2028
+ }
2029
+ function assertVersioning(self) {
2030
+ if (self.provider.capabilities?.versioning === true)
2031
+ return;
2032
+ invalid3(self.context.name, `${recordNameOf2(self.context.entity)} is versioned, and the provider does not check versions (capabilities.versioning).`);
2033
+ }
2034
+ async function write(self, data) {
2035
+ if (!isVersionedEntityClass(self.context.entity)) {
2036
+ await self.provider.update(self.context, data);
2037
+ return data;
2038
+ }
2039
+ assertVersioning(self);
2040
+ const expected = data.version;
2041
+ const next = { ...data, version: expected + 1 };
2042
+ await self.provider.update(self.context, next, {
2043
+ expectedVersion: expected
2044
+ });
2045
+ return next;
2046
+ }
2047
+ async function writeEntity(self, entity) {
2048
+ const written = await write(self, self.dataOf(entity));
2049
+ if (written.version !== undefined && isEntity2(entity)) {
2050
+ storeEntityVersion(entity, written.version);
2051
+ }
2052
+ }
1534
2053
  var update = action(async (self, entity) => {
1535
- await self.provider.update(self.context, self.dataOf(entity));
2054
+ await writeEntity(self, entity);
2055
+ return entity;
2056
+ });
2057
+ function isPlainObject2(value) {
2058
+ return typeof value === "object" && value !== null && !Array.isArray(value);
2059
+ }
2060
+ function isNumericField(field) {
2061
+ if (field === undefined)
2062
+ return false;
2063
+ const property = fieldOf(field);
2064
+ if (!isProperty2(property))
2065
+ return false;
2066
+ const { type } = property.type.schema.type.Type();
2067
+ return type === "number" || type === "integer";
2068
+ }
2069
+ function deltasOf(context, deltas) {
2070
+ const { name: source } = context;
2071
+ const fail = (reason, issues) => {
2072
+ throw new InvalidRepositoryIncrement2({
2073
+ source,
2074
+ details: { reason, issues }
2075
+ });
2076
+ };
2077
+ if (!isPlainObject2(deltas) || Object.keys(deltas).length === 0) {
2078
+ const message = "Expected an object of the numbers to add, by key.";
2079
+ fail(message, [{ code: "invalid_type", path: [], message, source }]);
2080
+ }
2081
+ const { change } = context.entity.options;
2082
+ const properties = context.entity.blueprint.properties;
2083
+ const issues = [];
2084
+ for (const [key, value] of Object.entries(deltas)) {
2085
+ if (!change.includes(key) || !isNumericField(properties[key])) {
2086
+ issues.push({
2087
+ code: "undeclared_key",
2088
+ path: [key],
2089
+ message: `"${key}" is not a number in the change list of ${recordNameOf2(context.entity)}.`,
2090
+ source
2091
+ });
2092
+ } else if (typeof value !== "number" || !Number.isFinite(value)) {
2093
+ issues.push({
2094
+ code: "invalid_value",
2095
+ path: [key],
2096
+ message: "Expected a finite number.",
2097
+ source
2098
+ });
2099
+ }
2100
+ }
2101
+ if (issues.length > 0) {
2102
+ fail(`"${source}" cannot increment ${issues.map((issue) => `"${issue.path[0]}"`).join(", ")}.`, issues);
2103
+ }
2104
+ return deltas;
2105
+ }
2106
+ function createdOf(context, id, input, deltas) {
2107
+ const identity = identityKeysOf(context.entity);
2108
+ const value = isPlainObject2(input) ? Object.fromEntries(Object.entries(input).filter(([key]) => !identity.includes(key))) : {};
2109
+ for (const [key, delta] of Object.entries(deltas)) {
2110
+ const base = value[key];
2111
+ value[key] = (typeof base === "number" ? base : 0) + delta;
2112
+ }
2113
+ const Entity = context.entity;
2114
+ const entity = new Entity({ ...value, id });
2115
+ enqueueLifecycle(entity, "onCreate");
2116
+ return entity;
2117
+ }
2118
+ var increment = action(async (self, target, deltas, options) => {
2119
+ const { context } = self;
2120
+ const id = typeof target === "string" ? target : self.dataOf(target).id;
2121
+ const checked = deltasOf(context, deltas);
2122
+ if (options !== undefined && !isPlainObject2(options)) {
2123
+ invalid3(context.name, "Expected the options of increment as an object.");
2124
+ }
2125
+ const versioned = isVersionedEntityClass(context.entity);
2126
+ if (versioned)
2127
+ assertVersioning(self);
2128
+ const { create } = options ?? {};
2129
+ const created = create === undefined ? undefined : createdOf(context, id, create, checked);
2130
+ const stored = await self.provider.increment(context, id, versioned ? { ...checked, version: 1 } : checked, {
2131
+ set: { updatedAt: nowOf2(context) },
2132
+ ...created === undefined ? {} : { create: self.dataOf(created) },
2133
+ ...context.expiry === undefined ? {} : { now: expiryOf(self, false).now }
2134
+ });
2135
+ if (created !== undefined && stored.updatedAt === undefined)
2136
+ return created;
2137
+ const entity = await self.hydrate(stored);
2138
+ enqueueLifecycle(entity, "onUpdate");
1536
2139
  return entity;
1537
2140
  });
1538
2141
  var remove = action(async (self, target) => {
@@ -1553,7 +2156,7 @@ var remove = action(async (self, target) => {
1553
2156
  if (!destroyable.isDeleted)
1554
2157
  destroyable.destroy();
1555
2158
  if (stateOf(self).deletion === "soft") {
1556
- await self.provider.update(context, self.dataOf(entity));
2159
+ await writeEntity(self, entity);
1557
2160
  } else {
1558
2161
  await self.provider.remove(context, data.id);
1559
2162
  }
@@ -1575,14 +2178,13 @@ var restore = action(async (self, target) => {
1575
2178
  if (data.deletedAt === undefined)
1576
2179
  return self.hydrate(data);
1577
2180
  const { deletedAt: _deletedAt, ...alive } = data;
1578
- const restored = {
1579
- ...alive,
1580
- updatedAt: context.entity[EntityClock].now()
1581
- };
1582
- await self.provider.update(context, restored);
1583
- return self.hydrate(restored);
2181
+ const restored = { ...alive, updatedAt: nowOf2(context) };
2182
+ return self.hydrate(await write(self, restored));
2183
+ });
2184
+ var count = action(async (self, query) => {
2185
+ const prepared = queryOf(self, query);
2186
+ return self.provider.count(self.context, planned(self, "count", prepared, []));
1584
2187
  });
1585
- var count = action(async (self, query) => self.provider.count(self.context, queryOf(self, query)));
1586
2188
  var NEEDS = {
1587
2189
  findBy: ["findOne"],
1588
2190
  findManyBy: ["findMany"],
@@ -1594,7 +2196,10 @@ var NEEDS = {
1594
2196
  findMany: ["findMany"],
1595
2197
  exists: ["findOne"],
1596
2198
  findPage: ["findMany", "count"],
1597
- findCursor: ["findMany"]
2199
+ findCursor: ["findMany"],
2200
+ explain: [],
2201
+ increment: ["increment"],
2202
+ forEachPage: ["findMany"]
1598
2203
  };
1599
2204
  function capitalize(key) {
1600
2205
  return `${key.charAt(0).toUpperCase()}${key.slice(1)}`;
@@ -1615,6 +2220,57 @@ function unknownAction(entity, action, methods) {
1615
2220
  function invalid3(source, reason) {
1616
2221
  throw new InvalidRepositoryDeclaration2({ source, details: { reason } });
1617
2222
  }
2223
+ var INDEX_NAME = /^[A-Za-z_$][A-Za-z0-9_$]*$/;
2224
+ function indexesOf(name, entity, declared) {
2225
+ if (declared === undefined)
2226
+ return Object.freeze({});
2227
+ if (typeof declared !== "object" || declared === null || Array.isArray(declared)) {
2228
+ invalid3(name, "Expected indexes to be an object of indexes by name.");
2229
+ }
2230
+ const properties = entity.blueprint.properties;
2231
+ const sensitive = entity.options.sensitive;
2232
+ const keyOf = (index, key) => {
2233
+ if (typeof key !== "string" || !Object.hasOwn(properties, key)) {
2234
+ invalid3(name, `The index "${index}" names "${String(key)}", which is not a key of ${recordNameOf2(entity)}.`);
2235
+ }
2236
+ if (sensitive.includes(key)) {
2237
+ invalid3(name, `The index "${index}" names "${key}", which is sensitive and cannot be indexed.`);
2238
+ }
2239
+ return key;
2240
+ };
2241
+ const seen = new Map;
2242
+ const indexes = {};
2243
+ for (const [index, value] of Object.entries(declared)) {
2244
+ if (!INDEX_NAME.test(index) || index === "__proto__") {
2245
+ invalid3(name, `Expected the index "${index}" to be named by an identifier.`);
2246
+ }
2247
+ if (typeof value !== "object" || value === null || Array.isArray(value) || Object.keys(value).some((key) => key !== "where" && key !== "orderBy") || !Array.isArray(value.where) || value.where.length === 0) {
2248
+ invalid3(name, `Expected the index "${index}" to be { where: [keys], orderBy?: key }.`);
2249
+ }
2250
+ const where = value.where.map((key) => keyOf(index, key));
2251
+ if (new Set(where).size !== where.length) {
2252
+ invalid3(name, `The index "${index}" names a key twice in where.`);
2253
+ }
2254
+ const orderBy = value.orderBy === undefined ? undefined : keyOf(index, value.orderBy);
2255
+ if (orderBy !== undefined) {
2256
+ const field = properties[orderBy];
2257
+ if (isBlueprint2(field) || isBlueprintList2(field) || recordFieldOf2(field)) {
2258
+ invalid3(name, `The index "${index}" orders by "${orderBy}", which is not scalar.`);
2259
+ }
2260
+ }
2261
+ const signature = JSON.stringify([[...where].sort(), orderBy ?? null]);
2262
+ const other = seen.get(signature);
2263
+ if (other !== undefined) {
2264
+ invalid3(name, `The indexes "${other}" and "${index}" are the same.`);
2265
+ }
2266
+ seen.set(signature, index);
2267
+ indexes[index] = Object.freeze({
2268
+ where: Object.freeze(where),
2269
+ ...orderBy === undefined ? {} : { orderBy }
2270
+ });
2271
+ }
2272
+ return Object.freeze(indexes);
2273
+ }
1618
2274
  function repositoryOf2(settings) {
1619
2275
  const { entity, actions, environmentDependencies } = settings ?? {};
1620
2276
  if (!isEntityClass2(entity) || !(EntityUniqueKeys2 in entity)) {
@@ -1625,6 +2281,10 @@ function repositoryOf2(settings) {
1625
2281
  if (deletion !== "hard" && deletion !== "soft") {
1626
2282
  invalid3(name, 'Expected deletion to be "hard" or "soft".');
1627
2283
  }
2284
+ const scans = settings.scans ?? "allow";
2285
+ if (scans !== "allow" && scans !== "warn" && scans !== "reject") {
2286
+ invalid3(name, 'Expected scans to be "allow", "warn" or "reject".');
2287
+ }
1628
2288
  const provider = settings.provider ?? inMemoryProvider2();
1629
2289
  if (!Array.isArray(actions) || actions.length === 0) {
1630
2290
  invalid3(name, "Expected a list of actions.");
@@ -1633,6 +2293,7 @@ function repositoryOf2(settings) {
1633
2293
  invalid3(name, "Expected environments made by environmentOf.");
1634
2294
  }
1635
2295
  const relations = relationsOf(name, entity, settings.relations);
2296
+ const indexes = indexesOf(name, entity, settings.indexes);
1636
2297
  const unique = entity[EntityUniqueKeys2];
1637
2298
  const methods = new Map;
1638
2299
  for (const key of Object.keys(entity.blueprint.properties)) {
@@ -1652,6 +2313,9 @@ function repositoryOf2(settings) {
1652
2313
  methods.set("exists", ["exists", exists]);
1653
2314
  methods.set("findPage", ["findPage", findPage]);
1654
2315
  methods.set("findCursor", ["findCursor", findCursor]);
2316
+ methods.set("explain", ["explain", explain]);
2317
+ methods.set("increment", ["increment", increment]);
2318
+ methods.set("forEachPage", ["forEachPage", forEachPage]);
1655
2319
 
1656
2320
  class DeclaredRepository extends AbstractRepository2 {
1657
2321
  }
@@ -1663,6 +2327,9 @@ function repositoryOf2(settings) {
1663
2327
  if (kind === "restore" && deletion !== "soft") {
1664
2328
  invalid3(name, '"restore" needs deletion to be "soft".');
1665
2329
  }
2330
+ if (kind === "findCursor" && settings.provider !== undefined && provider[Kind2] !== "provider-slot" && provider.capabilities?.keyset !== true) {
2331
+ invalid3(name, '"findCursor" needs a provider that reads by cursor (capabilities.keyset).');
2332
+ }
1666
2333
  const needs = deletion === "soft" && kind === "delete" ? ["findOne", "update"] : NEEDS[kind] ?? [];
1667
2334
  for (const need of needs) {
1668
2335
  if (typeof provider?.[need] !== "function") {
@@ -1684,7 +2351,9 @@ function repositoryOf2(settings) {
1684
2351
  provider,
1685
2352
  explicitProvider: settings.provider !== undefined,
1686
2353
  environment: environmentDependencies,
1687
- relations
2354
+ relations,
2355
+ indexes,
2356
+ scans
1688
2357
  };
1689
2358
  states.set(DeclaredRepository, state);
1690
2359
  const repository = Object.freeze(new DeclaredRepository);
@@ -1867,7 +2536,6 @@ function propertyInputOf2(field) {
1867
2536
  }
1868
2537
 
1869
2538
  // src/core/use-case/target.ts
1870
- var IDENTITY = ENTITY_IDENTITY_KEYS2;
1871
2539
  function isOptionalField(field) {
1872
2540
  if (isProperty2(field))
1873
2541
  return field.type.optional === true;
@@ -1898,7 +2566,7 @@ function planOf(entity, key, owners) {
1898
2566
  return Object.freeze({
1899
2567
  key,
1900
2568
  field,
1901
- identity: IDENTITY.includes(key),
2569
+ identity: identityKeysOf(entity).includes(key),
1902
2570
  plugin: owners.get(key),
1903
2571
  sensitive: entity.options.sensitive.includes(key),
1904
2572
  unique: entity.options.unique.includes(key),
@@ -2063,7 +2731,7 @@ async function executeSteps(dependencies = {}) {
2063
2731
  bind(await child(value(step.input)));
2064
2732
  } else if (Object.hasOwn(step, "run")) {
2065
2733
  const fn = step.fn;
2066
- bind(await fn(...list(step.args), { context: this.context }));
2734
+ bind(await fn(...list(step.args), { context: this.context, dependencies }));
2067
2735
  } else {
2068
2736
  const action = ["create", "update", "delete", "restore"].find((kind) => Object.hasOwn(step, kind));
2069
2737
  const methods = dependencies[step[action]];
@@ -2108,7 +2776,13 @@ var ENSURE_KINDS = new Set([
2108
2776
  "false"
2109
2777
  ]);
2110
2778
  var READ_ACTION = /^(?:findBy.+|findManyBy.+|findMany|exists|findPage|findCursor|count)$/;
2111
- var WRITE_ACTIONS = new Set(["create", "update", "delete", "restore"]);
2779
+ var WRITE_ACTIONS = new Set([
2780
+ "create",
2781
+ "update",
2782
+ "delete",
2783
+ "restore",
2784
+ "increment"
2785
+ ]);
2112
2786
  function validateSteps(declaration, steps) {
2113
2787
  const { name } = declaration;
2114
2788
  const fail = (index, code, reason) => {
@@ -2230,6 +2904,9 @@ function validateSteps(declaration, steps) {
2230
2904
  if (WRITE_ACTIONS.has(action)) {
2231
2905
  fail(index, "write_in_find", `find cannot call "${action}".`);
2232
2906
  }
2907
+ if (action === "forEachPage") {
2908
+ fail(index, "unknown_action", '"forEachPage" runs a function on each page and cannot be a step; call it from a use case written in TypeScript.');
2909
+ }
2233
2910
  if (!READ_ACTION.test(action)) {
2234
2911
  fail(index, "unknown_action", `"${action}" is not a read action.`);
2235
2912
  }
@@ -3250,12 +3927,16 @@ async function applyPolicy(command, policy, onEventError, call) {
3250
3927
  get env() {
3251
3928
  return call.env();
3252
3929
  },
3253
- logger: call.logger
3930
+ logger: call.logger,
3931
+ publisher: (name) => resolvePublisher(command, name, source)
3254
3932
  };
3255
3933
  if (typeof policy === "function") {
3256
3934
  try {
3257
3935
  await policy(raised, info);
3258
3936
  } catch (cause) {
3937
+ if (cause instanceof Exception2 && cause.name === "CommandEventFailed") {
3938
+ throw cause;
3939
+ }
3259
3940
  throw new CommandEventFailed2({
3260
3941
  source,
3261
3942
  cause,
@@ -3301,6 +3982,81 @@ function isCommand2(value) {
3301
3982
  return typeof value === "object" && value !== null && value[Kind2] === "command";
3302
3983
  }
3303
3984
 
3985
+ // src/core/event-policy/event-policy.ts
3986
+ var described = new WeakMap;
3987
+ var factories = new WeakMap;
3988
+ function invalid4(source, reason) {
3989
+ throw new InvalidEventPolicy2({ source, details: { reason } });
3990
+ }
3991
+ function optionsOf(name, given, validate) {
3992
+ const options = frozenJson(given ?? {}, "Event policy options", (reason) => invalid4(name, reason));
3993
+ if (options === null || typeof options !== "object" || Array.isArray(options)) {
3994
+ invalid4(name, "Event policy options must be an object.");
3995
+ }
3996
+ const reason = validate?.(options);
3997
+ if (reason !== undefined)
3998
+ invalid4(name, reason);
3999
+ return options;
4000
+ }
4001
+ function eventPolicyOf2(settings) {
4002
+ const name = settings?.name;
4003
+ if (typeof name !== "string" || !/^[A-Za-z_$][\w$]*$/.test(name)) {
4004
+ invalid4("eventPolicyOf", "Expected the policy name to be an identifier.");
4005
+ }
4006
+ if (typeof settings.handle !== "function") {
4007
+ invalid4(name, "Expected handle to be a function.");
4008
+ }
4009
+ if (settings.publishers !== undefined && typeof settings.publishers !== "function") {
4010
+ invalid4(name, "Expected publishers to be a function.");
4011
+ }
4012
+ const { handle, validate, publishers } = settings;
4013
+ const factory = (given) => {
4014
+ const options = optionsOf(name, given, validate);
4015
+ const handler = (events, info) => handle(events, info, options);
4016
+ Object.defineProperty(handler, "name", { value: name });
4017
+ described.set(handler, {
4018
+ description: Object.freeze({
4019
+ kind: "policy",
4020
+ name,
4021
+ options
4022
+ }),
4023
+ publishers: Object.freeze([...publishers?.(options) ?? []])
4024
+ });
4025
+ return handler;
4026
+ };
4027
+ factories.set(factory, name);
4028
+ return factory;
4029
+ }
4030
+ function eventPolicyDescriptionOf2(handler) {
4031
+ return typeof handler === "function" ? described.get(handler)?.description : undefined;
4032
+ }
4033
+ function eventPolicyPublishersOf(handler) {
4034
+ return typeof handler === "function" ? described.get(handler)?.publishers ?? [] : [];
4035
+ }
4036
+ function eventPolicyCatalogOf2(...policies) {
4037
+ const byName = {};
4038
+ for (const factory of policies) {
4039
+ const name = factories.get(factory);
4040
+ if (name === undefined) {
4041
+ invalid4("catalog", "A catalog takes factories made by `eventPolicyOf`.");
4042
+ }
4043
+ if (Object.hasOwn(byName, name)) {
4044
+ invalid4(name, "The catalog has another event policy with its name.");
4045
+ }
4046
+ byName[name] = factory;
4047
+ }
4048
+ return Object.freeze({
4049
+ [Kind2]: "event-policy-catalog",
4050
+ policies: Object.freeze(byName),
4051
+ resolve(name, options) {
4052
+ const factory = Object.hasOwn(byName, name) ? byName[name] : undefined;
4053
+ if (!factory)
4054
+ invalid4("catalog", `Unknown event policy "${name}".`);
4055
+ return factory(options);
4056
+ }
4057
+ });
4058
+ }
4059
+
3304
4060
  // src/core/openapi/exceptions.ts
3305
4061
  var InvalidOpenAPIModel2 = exceptionOf2({
3306
4062
  layer: "output" /* Output */,
@@ -3334,18 +4090,19 @@ function isExportedException(Declared) {
3334
4090
  collected = true;
3335
4091
  for (const module of [
3336
4092
  exports_exceptions4,
3337
- exports_exceptions2,
3338
- exports_exceptions9,
3339
- exports_exceptions6,
4093
+ exports_exceptions3,
4094
+ exports_exceptions10,
4095
+ exports_exceptions7,
3340
4096
  exports_environment,
3341
4097
  exports_exceptions5,
3342
- exports_exceptions10,
4098
+ exports_exceptions6,
3343
4099
  exports_exceptions11,
3344
- exports_exceptions3,
4100
+ exports_exceptions12,
4101
+ exports_exceptions2,
3345
4102
  exports_exceptions,
3346
- exports_exceptions7,
4103
+ exports_exceptions8,
3347
4104
  exports_use_case,
3348
- exports_exceptions8
4105
+ exports_exceptions9
3349
4106
  ]) {
3350
4107
  markExported(...Object.values(module));
3351
4108
  }
@@ -3404,7 +4161,7 @@ function recordSchemaOf(record, scope) {
3404
4161
  const { sensitive = [] } = record.options ?? {};
3405
4162
  return objectOf(record.blueprint.properties, scope, {
3406
4163
  writeOnly: sensitive,
3407
- readOnly: isEntityClass2(record) ? ENTITY_IDENTITY_KEYS2 : []
4164
+ readOnly: isEntityClass2(record) ? identityKeysOf(record) : []
3408
4165
  });
3409
4166
  }
3410
4167
  function schemaOf(field, scope) {
@@ -3465,34 +4222,34 @@ function toOpenAPI2(modelOrModels, { refPrefix = "#/components/schemas/" } = {})
3465
4222
 
3466
4223
  // src/core/output/stage.ts
3467
4224
  var descriptions2 = new WeakMap;
3468
- var factories = new WeakMap;
4225
+ var factories2 = new WeakMap;
3469
4226
  var opaque2 = Object.freeze({ kind: "opaque" });
3470
- function invalid4(source, reason) {
4227
+ function invalid5(source, reason) {
3471
4228
  throw new InvalidOutputStage2({ source, details: { reason } });
3472
4229
  }
3473
- function optionsOf(name, given, validate) {
3474
- const options = frozenJson(given ?? {}, "Stage options", (reason) => invalid4(name, reason));
4230
+ function optionsOf2(name, given, validate) {
4231
+ const options = frozenJson(given ?? {}, "Stage options", (reason) => invalid5(name, reason));
3475
4232
  if (options === null || typeof options !== "object" || Array.isArray(options)) {
3476
- invalid4(name, "Stage options must be an object.");
4233
+ invalid5(name, "Stage options must be an object.");
3477
4234
  }
3478
4235
  const reason = validate?.(options);
3479
4236
  if (reason !== undefined)
3480
- invalid4(name, reason);
4237
+ invalid5(name, reason);
3481
4238
  return options;
3482
4239
  }
3483
4240
  function checkName(source, name) {
3484
4241
  if (typeof name !== "string" || !name.trim()) {
3485
- invalid4(source, "Expected the stage name to be a non-empty string.");
4242
+ invalid5(source, "Expected the stage name to be a non-empty string.");
3486
4243
  }
3487
4244
  }
3488
4245
  function transformOf2(settings) {
3489
4246
  checkName("transformOf", settings?.name);
3490
4247
  if (typeof settings.apply !== "function") {
3491
- invalid4(settings.name, "Expected apply to be a function.");
4248
+ invalid5(settings.name, "Expected apply to be a function.");
3492
4249
  }
3493
4250
  const { name, apply, validate } = settings;
3494
4251
  const factory = (given) => {
3495
- const options = optionsOf(name, given, validate);
4252
+ const options = optionsOf2(name, given, validate);
3496
4253
  const stage = Object.freeze({
3497
4254
  kind: "transform",
3498
4255
  name,
@@ -3506,17 +4263,17 @@ function transformOf2(settings) {
3506
4263
  }));
3507
4264
  return stage;
3508
4265
  };
3509
- factories.set(factory, { kind: "transform", name });
4266
+ factories2.set(factory, { kind: "transform", name });
3510
4267
  return factory;
3511
4268
  }
3512
4269
  function sinkOf2(settings) {
3513
4270
  checkName("sinkOf", settings?.name);
3514
4271
  if (typeof settings.build !== "function") {
3515
- invalid4(settings.name, "Expected build to be a function.");
4272
+ invalid5(settings.name, "Expected build to be a function.");
3516
4273
  }
3517
4274
  const { name, build, validate } = settings;
3518
4275
  const factory = (given) => {
3519
- const options = optionsOf(name, given, validate);
4276
+ const options = optionsOf2(name, given, validate);
3520
4277
  const stage = Object.freeze({
3521
4278
  kind: "sink",
3522
4279
  name,
@@ -3530,14 +4287,14 @@ function sinkOf2(settings) {
3530
4287
  }));
3531
4288
  return stage;
3532
4289
  };
3533
- factories.set(factory, { kind: "sink", name });
4290
+ factories2.set(factory, { kind: "sink", name });
3534
4291
  return factory;
3535
4292
  }
3536
4293
  function stageDescriptionOf2(stage) {
3537
4294
  return descriptions2.get(stage) ?? opaque2;
3538
4295
  }
3539
4296
  function stageKindOf2(factory) {
3540
- return factories.get(factory)?.kind;
4297
+ return factories2.get(factory)?.kind;
3541
4298
  }
3542
4299
  function isStage(value) {
3543
4300
  if (typeof value !== "object" || value === null)
@@ -3548,12 +4305,12 @@ function isStage(value) {
3548
4305
  function stageCatalogOf2(...stages) {
3549
4306
  const byName = {};
3550
4307
  for (const factory of stages) {
3551
- const known = factories.get(factory);
4308
+ const known = factories2.get(factory);
3552
4309
  if (!known) {
3553
- invalid4("catalog", "A catalog takes factories made by `transformOf` or `sinkOf`.");
4310
+ invalid5("catalog", "A catalog takes factories made by `transformOf` or `sinkOf`.");
3554
4311
  }
3555
4312
  if (Object.hasOwn(byName, known.name)) {
3556
- invalid4(known.name, "The catalog has another stage with its name.");
4313
+ invalid5(known.name, "The catalog has another stage with its name.");
3557
4314
  }
3558
4315
  byName[known.name] = factory;
3559
4316
  }
@@ -3564,7 +4321,7 @@ function stageCatalogOf2(...stages) {
3564
4321
  resolve(name, options) {
3565
4322
  const factory = Object.hasOwn(byName, name) ? byName[name] : undefined;
3566
4323
  if (!factory)
3567
- invalid4("catalog", `Unknown stage "${name}".`);
4324
+ invalid5("catalog", `Unknown stage "${name}".`);
3568
4325
  return factory(options);
3569
4326
  }
3570
4327
  });
@@ -3574,38 +4331,38 @@ function stageCatalogOf2(...stages) {
3574
4331
  function isApplicationLike(value) {
3575
4332
  return typeof value === "object" && value !== null && value[Kind2] === "application";
3576
4333
  }
3577
- function invalid5(source, reason) {
4334
+ function invalid6(source, reason) {
3578
4335
  throw new InvalidOutputStage2({ source, details: { reason } });
3579
4336
  }
3580
4337
  var PATCHABLE = ["meta", "tags", "description", "deprecated", "invoke"];
3581
4338
  function operationOf2(operation, patch) {
3582
4339
  const source = operation?.id ?? "operationOf";
3583
4340
  if (operation?.kind !== "operation" || operation.version !== 1) {
3584
- invalid5(source, "Expected an operation descriptor.");
4341
+ invalid6(source, "Expected an operation descriptor.");
3585
4342
  }
3586
4343
  if (typeof patch !== "object" || patch === null || Array.isArray(patch)) {
3587
- invalid5(source, "Expected the patch to be an object.");
4344
+ invalid6(source, "Expected the patch to be an object.");
3588
4345
  }
3589
4346
  for (const key of Reflect.ownKeys(patch)) {
3590
4347
  if (typeof key !== "string" || !PATCHABLE.includes(key)) {
3591
- invalid5(source, `"${String(key)}" cannot be patched; only ${PATCHABLE.join(", ")} can.`);
4348
+ invalid6(source, `"${String(key)}" cannot be patched; only ${PATCHABLE.join(", ")} can.`);
3592
4349
  }
3593
4350
  }
3594
4351
  const { meta, tags, description, deprecated, invoke } = patch;
3595
4352
  if ("meta" in patch && (typeof meta !== "object" || meta === null || Array.isArray(meta))) {
3596
- invalid5(source, "Expected meta to be an object.");
4353
+ invalid6(source, "Expected meta to be an object.");
3597
4354
  }
3598
4355
  if ("tags" in patch && (!Array.isArray(tags) || tags.some((tag) => typeof tag !== "string"))) {
3599
- invalid5(source, "Expected tags to be a list of strings.");
4356
+ invalid6(source, "Expected tags to be a list of strings.");
3600
4357
  }
3601
4358
  if ("description" in patch && description !== undefined && typeof description !== "string") {
3602
- invalid5(source, "Expected description to be a string.");
4359
+ invalid6(source, "Expected description to be a string.");
3603
4360
  }
3604
4361
  if ("deprecated" in patch && typeof deprecated !== "boolean") {
3605
- invalid5(source, "Expected deprecated to be a boolean.");
4362
+ invalid6(source, "Expected deprecated to be a boolean.");
3606
4363
  }
3607
4364
  if ("invoke" in patch && typeof invoke !== "function") {
3608
- invalid5(source, "Expected invoke to be a function.");
4365
+ invalid6(source, "Expected invoke to be a function.");
3609
4366
  }
3610
4367
  return Object.freeze({
3611
4368
  ...operation,
@@ -3646,25 +4403,25 @@ function statusesOf(name, statuses) {
3646
4403
  if (statuses === undefined)
3647
4404
  return Object.freeze({});
3648
4405
  if (typeof statuses !== "object" || statuses === null || Array.isArray(statuses)) {
3649
- invalid5(name, "Expected statuses to be a map of exception names to statuses.");
4406
+ invalid6(name, "Expected statuses to be a map of exception names to statuses.");
3650
4407
  }
3651
4408
  for (const [exception, status] of Object.entries(statuses)) {
3652
4409
  if (!exception || typeof status !== "number" || !Number.isInteger(status) || status < 400 || status > 599) {
3653
- invalid5(name, `Expected the status of "${exception}" to be an integer between 400 and 599.`);
4410
+ invalid6(name, `Expected the status of "${exception}" to be an integer between 400 and 599.`);
3654
4411
  }
3655
4412
  }
3656
4413
  return Object.freeze({ ...statuses });
3657
4414
  }
3658
4415
  function checkPipe(name, pipe) {
3659
4416
  if (!Array.isArray(pipe))
3660
- invalid5(name, "Expected pipe to be a list of stages.");
4417
+ invalid6(name, "Expected pipe to be a list of stages.");
3661
4418
  const stages = pipe;
3662
4419
  stages.forEach((stage, index) => {
3663
4420
  if (!isStage(stage)) {
3664
- invalid5(name, `Expected pipe[${index}] to be a transform or sink stage.`);
4421
+ invalid6(name, `Expected pipe[${index}] to be a transform or sink stage.`);
3665
4422
  }
3666
4423
  if (stage.kind === "sink" && index !== stages.length - 1) {
3667
- invalid5(name, `The sink at pipe[${index}] must be the last stage.`);
4424
+ invalid6(name, `The sink at pipe[${index}] must be the last stage.`);
3668
4425
  }
3669
4426
  });
3670
4427
  return Object.freeze([...stages]);
@@ -3680,7 +4437,7 @@ function transformed(name, operations, transforms, context) {
3680
4437
  if (result === null)
3681
4438
  continue;
3682
4439
  if (typeof result !== "object" || result?.kind !== "operation" || result.id !== operation.id) {
3683
- invalid5(name, `A transform must return the operation "${operation.id}" or null.`);
4440
+ invalid6(name, `A transform must return the operation "${operation.id}" or null.`);
3684
4441
  }
3685
4442
  next.push(result);
3686
4443
  }
@@ -3705,7 +4462,7 @@ function make(name, commands, operations, stages, statuses) {
3705
4462
  return built.value;
3706
4463
  const sink = stages.at(-1);
3707
4464
  if (sink?.kind !== "sink") {
3708
- invalid5(name || "output", "The pipe has no sink to build.");
4465
+ invalid6(name || "output", "The pipe has no sink to build.");
3709
4466
  }
3710
4467
  built = { value: sink.build(operations, context) };
3711
4468
  return built.value;
@@ -3714,7 +4471,7 @@ function make(name, commands, operations, stages, statuses) {
3714
4471
  const next = checkPipe(name || "output", more);
3715
4472
  const last = next.at(-1);
3716
4473
  if (stages.at(-1)?.kind === "sink") {
3717
- invalid5(name || "output", "Cannot add stages after the sink.");
4474
+ invalid6(name || "output", "Cannot add stages after the sink.");
3718
4475
  }
3719
4476
  const ops = transformed(name, operations, next, context);
3720
4477
  if (last?.kind === "sink") {
@@ -3728,11 +4485,11 @@ function make(name, commands, operations, stages, statuses) {
3728
4485
  function outputOf2(settings) {
3729
4486
  const { name, commands } = settings ?? {};
3730
4487
  if (typeof name !== "string" || !name.trim()) {
3731
- invalid5("outputOf", "Expected name to be a non-empty string.");
4488
+ invalid6("outputOf", "Expected name to be a non-empty string.");
3732
4489
  }
3733
4490
  const sources = Array.isArray(commands) ? commands : [commands];
3734
4491
  if (!Array.isArray(commands) && isCommand2(commands) || sources.some((entry) => !isCommand2(entry) && !isApplicationLike(entry))) {
3735
- invalid5(name, "Expected commands to be a list of commands made by commandOf or applications.");
4492
+ invalid6(name, "Expected commands to be a list of commands made by commandOf or applications.");
3736
4493
  }
3737
4494
  const flat = [
3738
4495
  ...new Set(sources.flatMap((entry) => isCommand2(entry) ? [entry] : Object.values(entry.commands)))
@@ -3852,7 +4609,7 @@ function deferredProvider(factory, application) {
3852
4609
  return provider;
3853
4610
  };
3854
4611
  const proxy = {};
3855
- for (const method of METHODS) {
4612
+ for (const method of [...METHODS, "increment"]) {
3856
4613
  proxy[method] = (...args) => real(method)[method]?.(...args);
3857
4614
  }
3858
4615
  proxy.transaction = (run) => real("transaction").transaction(run);
@@ -3913,6 +4670,16 @@ function checkTransactions(source, provider, commands) {
3913
4670
  });
3914
4671
  }
3915
4672
  }
4673
+ function checkCapabilities(source, provider, repositories) {
4674
+ if (repositories.length === 0 || provider.capabilities?.keyset === true)
4675
+ return;
4676
+ throw new InvalidApplicationDeclaration2({
4677
+ source,
4678
+ details: {
4679
+ reason: `The provider made on start does not read by cursor (capabilities.keyset), which the findCursor of ${repositories.join(", ")} needs.`
4680
+ }
4681
+ });
4682
+ }
3916
4683
 
3917
4684
  // src/core/application/providers.ts
3918
4685
  var slots = new WeakMap;
@@ -3927,7 +4694,7 @@ var METHODS2 = [
3927
4694
  ];
3928
4695
  var KEY = /^[A-Za-z_$][A-Za-z0-9_$]*$/;
3929
4696
  var RESERVED = new Set(["inMemory", "__proto__", "constructor", "prototype"]);
3930
- function invalid6(reason) {
4697
+ function invalid7(reason) {
3931
4698
  throw new InvalidApplicationDeclaration2({
3932
4699
  source: "providersOf",
3933
4700
  details: { reason }
@@ -3948,6 +4715,7 @@ function slotOf(key, storage, value) {
3948
4715
  for (const method of METHODS2)
3949
4716
  slot[method] = unbound;
3950
4717
  slot.transaction = unbound;
4718
+ slot.increment = unbound;
3951
4719
  Object.defineProperty(slot, "capabilities", {
3952
4720
  get: () => {
3953
4721
  return;
@@ -3956,7 +4724,7 @@ function slotOf(key, storage, value) {
3956
4724
  });
3957
4725
  } else {
3958
4726
  const target = value;
3959
- for (const method of [...METHODS2, "transaction", "close"]) {
4727
+ for (const method of [...METHODS2, "increment", "transaction", "close"]) {
3960
4728
  if (typeof target[method] === "function") {
3961
4729
  slot[method] = (...args) => target[method]?.(...args);
3962
4730
  }
@@ -3972,7 +4740,7 @@ function slotOf(key, storage, value) {
3972
4740
  }
3973
4741
  function providersOf2(entries) {
3974
4742
  if (typeof entries !== "object" || entries === null || Array.isArray(entries) || Object.keys(entries).length === 0) {
3975
- invalid6("Expected an object of providers by key.");
4743
+ invalid7("Expected an object of providers by key.");
3976
4744
  }
3977
4745
  const storage = Object.create(null);
3978
4746
  storage[Kind2] = "provider-storage";
@@ -3980,17 +4748,17 @@ function providersOf2(entries) {
3980
4748
  const list = [];
3981
4749
  for (const [key, value] of Object.entries(entries)) {
3982
4750
  if (!KEY.test(key) || RESERVED.has(key)) {
3983
- invalid6(key === "inMemory" ? `"inMemory" is the built-in provider; choose another key.` : `Expected "${key}" to be an identifier that is not reserved.`);
4751
+ invalid7(key === "inMemory" ? `"inMemory" is the built-in provider; choose another key.` : `Expected "${key}" to be an identifier that is not reserved.`);
3984
4752
  }
3985
4753
  if (isProviderSlot2(value)) {
3986
- invalid6(`"${key}" holds a provider of another storage; a storage holds providers, not slots.`);
4754
+ invalid7(`"${key}" holds a provider of another storage; a storage holds providers, not slots.`);
3987
4755
  }
3988
4756
  if (!isProviderFactory2(value) && !isProviderObject(value)) {
3989
- invalid6(`Expected "${key}" to be a repository provider or a factory of providerFrom.`);
4757
+ invalid7(`Expected "${key}" to be a repository provider or a factory of providerFrom.`);
3990
4758
  }
3991
4759
  const other = seen.get(value);
3992
4760
  if (other !== undefined) {
3993
- invalid6(`"${other}" and "${key}" hold the same provider.`);
4761
+ invalid7(`"${other}" and "${key}" hold the same provider.`);
3994
4762
  }
3995
4763
  seen.set(value, key);
3996
4764
  const slot = slotOf(key, storage, value);
@@ -4049,9 +4817,34 @@ var KIND_OF = {
4049
4817
  function isApplication2(value) {
4050
4818
  return typeof value === "object" && value !== null && value[Kind2] === "application";
4051
4819
  }
4052
- function invalid7(source, reason) {
4820
+ function invalid8(source, reason) {
4053
4821
  throw new InvalidApplicationDeclaration2({ source, details: { reason } });
4054
4822
  }
4823
+ var publisherOrigins = new WeakMap;
4824
+ function publishersOf(name, declared, extended) {
4825
+ const merged = {};
4826
+ const origins = {};
4827
+ for (const other of extended) {
4828
+ Object.assign(merged, other[ApplicationPublishersKey]);
4829
+ Object.assign(origins, publisherOrigins.get(other));
4830
+ }
4831
+ if (declared !== undefined) {
4832
+ if (typeof declared !== "object" || declared === null || Array.isArray(declared)) {
4833
+ invalid8(name, "Expected `publishers` to be an object of publishers by key.");
4834
+ }
4835
+ for (const [key, publisher] of Object.entries(declared)) {
4836
+ if (!/^[A-Za-z_$][\w$]*$/.test(key)) {
4837
+ invalid8(name, `Expected the publisher key "${key}" to be an identifier.`);
4838
+ }
4839
+ if (!isEventPublisher(publisher)) {
4840
+ invalid8(name, `Expected the publisher "${key}" to be an object with a publish(topic, event) method.`);
4841
+ }
4842
+ merged[key] = publisher;
4843
+ origins[key] = name;
4844
+ }
4845
+ }
4846
+ return { publishers: Object.freeze(merged), origins };
4847
+ }
4055
4848
  function isOfKind(collection, value) {
4056
4849
  switch (collection) {
4057
4850
  case "properties":
@@ -4134,7 +4927,7 @@ function resolve(settings) {
4134
4927
  for (const other of settings.extends ?? []) {
4135
4928
  const resolved = internals.get(other);
4136
4929
  if (!resolved)
4137
- invalid7(source, "Expected `extends` to hold applications.");
4930
+ invalid8(source, "Expected `extends` to hold applications.");
4138
4931
  for (const key of KEYS) {
4139
4932
  for (const entry of resolved[key].values())
4140
4933
  add(entry);
@@ -4145,15 +4938,15 @@ function resolve(settings) {
4145
4938
  if (declared === undefined)
4146
4939
  continue;
4147
4940
  if (typeof declared !== "object" || declared === null || Array.isArray(declared)) {
4148
- invalid7(source, `Expected ${collection} to be an object of declarations.`);
4941
+ invalid8(source, `Expected ${collection} to be an object of declarations.`);
4149
4942
  }
4150
4943
  for (const [name, value] of Object.entries(declared)) {
4151
4944
  if (!isOfKind(collection, value)) {
4152
- invalid7(source, `${collection}.${name} is not a ${KIND_OF[collection]} (${collection} takes ${KIND_OF[collection]} declarations).`);
4945
+ invalid8(source, `${collection}.${name} is not a ${KIND_OF[collection]} (${collection} takes ${KIND_OF[collection]} declarations).`);
4153
4946
  }
4154
4947
  const declaredName = collection === "useCases" ? value.useCaseName : collection === "commands" ? value.commandName : collection === "outputs" ? value.outputName : collection === "exceptions" ? value.name : collection === "records" || collection === "entities" ? declaredRecordNameOf(value) ?? name : name;
4155
4948
  if (declaredName !== name) {
4156
- invalid7(source, `${collection}.${name} must be keyed by its name, "${declaredName}".`);
4949
+ invalid8(source, `${collection}.${name} must be keyed by its name, "${declaredName}".`);
4157
4950
  }
4158
4951
  add({
4159
4952
  collection,
@@ -4288,7 +5081,7 @@ function validateOutputs(source, registry) {
4288
5081
  for (const entry of registry.outputs.values()) {
4289
5082
  const output = entry.value;
4290
5083
  if (!output.outputName) {
4291
- invalid7(source, `outputs.${entry.name} has no name (an output made by pipe cannot be registered).`);
5084
+ invalid8(source, `outputs.${entry.name} has no name (an output made by pipe cannot be registered).`);
4292
5085
  }
4293
5086
  for (const command of output.commands) {
4294
5087
  if (commands.has(command))
@@ -4469,8 +5262,9 @@ function recordDescriptionOf(entry, scope) {
4469
5262
  const Record = entry.value;
4470
5263
  const plugins = pluginsOf2(Record).map((plugin) => plugin.toJSON());
4471
5264
  const skip = new Set(plugins.flatMap((plugin) => plugin.keys));
5265
+ const expires = entry.collection === "entities" ? Record[EntitySettings2]?.expires : undefined;
4472
5266
  if (entry.collection === "entities") {
4473
- for (const key of ENTITY_IDENTITY_KEYS2)
5267
+ for (const key of identityKeysOf(Record))
4474
5268
  skip.add(key);
4475
5269
  }
4476
5270
  const own = (keys, flag) => keys.filter((key) => {
@@ -4479,6 +5273,14 @@ function recordDescriptionOf(entry, scope) {
4479
5273
  const field = Record.blueprint.properties[key];
4480
5274
  return !(flag && isProperty2(field) && field.type[flag] === true);
4481
5275
  });
5276
+ const lifecycle = entry.collection === "entities" ? lifecycleEventsOf(Record) : {};
5277
+ const events = Object.fromEntries(Object.entries(lifecycle).map(([cycle, list]) => [
5278
+ cycle,
5279
+ list.map((event) => ({
5280
+ name: event.name,
5281
+ handler: eventHasHandler(event)
5282
+ }))
5283
+ ]));
4482
5284
  return {
4483
5285
  from: entry.from,
4484
5286
  fields: fieldsDescriptionOf(Record.blueprint.properties, scope, skip),
@@ -4487,10 +5289,13 @@ function recordDescriptionOf(entry, scope) {
4487
5289
  unique: own(Record.options.unique, "unique"),
4488
5290
  change: own(Record.options.change)
4489
5291
  },
4490
- plugins
5292
+ plugins,
5293
+ ...Object.keys(events).length > 0 ? { events } : {},
5294
+ ...expires === undefined ? {} : { expires },
5295
+ ...entry.collection === "entities" && isVersionedEntityClass(Record) ? { versioned: true } : {}
4491
5296
  };
4492
5297
  }
4493
- function describeRegistry(name, extended, registry, provider, named, log) {
5298
+ function describeRegistry(name, extended, registry, provider, named, log, publishers) {
4494
5299
  const scope = {
4495
5300
  properties: referencesOf(registry, "properties"),
4496
5301
  blueprints: referencesOf(registry, "blueprints"),
@@ -4515,6 +5320,15 @@ function describeRegistry(name, extended, registry, provider, named, log) {
4515
5320
  };
4516
5321
  };
4517
5322
  const describeOption = (value) => {
5323
+ if (isLoggerDeclaration(value))
5324
+ return "Logger";
5325
+ if (isUseCaseClass2(value))
5326
+ return useCaseRefs.get(value);
5327
+ if (isEnvironment2(value))
5328
+ return environmentRefs.get(value);
5329
+ if (isRepository2(value)) {
5330
+ return repositories.get(inspectRepository(value).state);
5331
+ }
4518
5332
  if (typeof value === "function") {
4519
5333
  if (isDeclaredException(value)) {
4520
5334
  return exceptionRefs.get(value) ?? { core: value.name };
@@ -4582,6 +5396,9 @@ function describeRegistry(name, extended, registry, provider, named, log) {
4582
5396
  })
4583
5397
  }
4584
5398
  },
5399
+ ...Object.keys(publishers).length === 0 ? {} : {
5400
+ publishers: Object.fromEntries(Object.entries(publishers).map(([key, from]) => [key, { from }]))
5401
+ },
4585
5402
  properties: each("properties", (entry) => ({
4586
5403
  from: entry.from,
4587
5404
  ...propertyDescriptionOf(entry.value, scope)
@@ -4603,6 +5420,8 @@ function describeRegistry(name, extended, registry, provider, named, log) {
4603
5420
  entity: scope.entities.get(info.entity),
4604
5421
  actions: info.actions,
4605
5422
  deletion: info.deletion,
5423
+ indexes: info.indexes,
5424
+ scans: info.scans,
4606
5425
  relations: info.relations.map((relation) => repositories.get(relation)),
4607
5426
  environments: info.environments.map((environment) => environmentRefs.get(environment)),
4608
5427
  ...info.provider === undefined ? {} : {
@@ -4668,7 +5487,7 @@ function describeRegistry(name, extended, registry, provider, named, log) {
4668
5487
  ...command.environments.length === 0 ? {} : {
4669
5488
  environments: command.environments.map((environment) => environmentRefs.get(environment))
4670
5489
  },
4671
- events: typeof command.events === "function" ? { kind: "opaque" } : command.events,
5490
+ events: typeof command.events === "function" ? eventPolicyDescriptionOf2(command.events) ?? { kind: "opaque" } : command.events,
4672
5491
  ...command.onEventError === undefined ? {} : { onEventError: { kind: "opaque" } }
4673
5492
  };
4674
5493
  }),
@@ -4696,11 +5515,11 @@ function describeRegistry(name, extended, registry, provider, named, log) {
4696
5515
  function applicationOf2(settings) {
4697
5516
  const { name } = settings ?? {};
4698
5517
  if (typeof name !== "string" || name === "") {
4699
- invalid7("applicationOf", "Expected a name.");
5518
+ invalid8("applicationOf", "Expected a name.");
4700
5519
  }
4701
5520
  const extended = settings.extends ?? [];
4702
5521
  if (!Array.isArray(extended) || !extended.every(isApplication2)) {
4703
- invalid7(name, "Expected `extends` to be a list of applications.");
5522
+ invalid8(name, "Expected `extends` to be a list of applications.");
4704
5523
  }
4705
5524
  const registry = resolve(settings);
4706
5525
  validate(name, registry);
@@ -4708,7 +5527,7 @@ function applicationOf2(settings) {
4708
5527
  validateCommands(name, registry);
4709
5528
  validateOutputs(name, registry);
4710
5529
  if (settings.providers !== undefined && !isProviderStorage2(settings.providers)) {
4711
- invalid7(name, "Expected `providers` to be a storage made by providersOf.");
5530
+ invalid8(name, "Expected `providers` to be a storage made by providersOf.");
4712
5531
  }
4713
5532
  const named = new Map;
4714
5533
  const addNamed = (entry) => {
@@ -4765,7 +5584,7 @@ function applicationOf2(settings) {
4765
5584
  const inheritedFrom = settings.provider === undefined ? [...extended].reverse().find((other) => other.provider) : undefined;
4766
5585
  const declaredProvider = settings.provider ?? inheritedFrom?.provider;
4767
5586
  if (declaredProvider !== undefined && typeof declaredProvider !== "object") {
4768
- invalid7(name, "Expected `provider` to be a repository provider.");
5587
+ invalid8(name, "Expected `provider` to be a repository provider.");
4769
5588
  }
4770
5589
  const defaultSlot = isProviderSlot2(declaredProvider) ? declaredProvider : inheritedFrom === undefined ? undefined : defaultSlots.get(inheritedFrom);
4771
5590
  const provider = defaultSlot !== undefined ? instanceOf(defaultSlot, "the application") : isProviderFactory2(declaredProvider) ? deferredProvider(declaredProvider, name) : declaredProvider;
@@ -4798,14 +5617,33 @@ function applicationOf2(settings) {
4798
5617
  log?.set(checkLogSettings(declaredLog));
4799
5618
  }
4800
5619
  const logger = log?.logger;
5620
+ const { publishers, origins } = publishersOf(name, settings.publishers, extended);
5621
+ const hasPublishers = Object.keys(publishers).length > 0;
5622
+ for (const entry of registry.commands.values()) {
5623
+ for (const key of eventPolicyPublishersOf(entry.value.events)) {
5624
+ if (!Object.hasOwn(publishers, key)) {
5625
+ throw new UnregisteredApplicationReference2({
5626
+ source: name,
5627
+ details: {
5628
+ holder: `command "${entry.name}"`,
5629
+ reference: `publisher "${key}"`
5630
+ }
5631
+ });
5632
+ }
5633
+ }
5634
+ }
4801
5635
  const bindsProviders = provider !== undefined || instances.size > 0;
4802
- const rebinds = bindsProviders || logger !== undefined;
5636
+ const rebinds = bindsProviders || logger !== undefined || hasPublishers;
4803
5637
  const collection = (key) => Object.freeze(Object.fromEntries([...registry[key].values()].map((entry) => [entry.name, entry.value])));
4804
5638
  const bound = new Map;
4805
5639
  const boundByState = new Map;
4806
5640
  const repositories = {};
4807
5641
  for (const entry of registry.repositories.values()) {
4808
- const repository = bindsProviders ? bindRepository(entry.value, provider, instances) : entry.value;
5642
+ const repository = bindsProviders ? bindRepository(entry.value, provider, instances, {
5643
+ application: name,
5644
+ key: entry.name,
5645
+ logger
5646
+ }) : entry.value;
4809
5647
  bound.set(entry.value, repository);
4810
5648
  boundByState.set(entry.key, repository);
4811
5649
  repositories[entry.name] = repository;
@@ -4861,6 +5699,8 @@ function applicationOf2(settings) {
4861
5699
  });
4862
5700
  if (bindsProviders)
4863
5701
  verifyCommand(rebuilt);
5702
+ if (hasPublishers)
5703
+ bindPublishers(rebuilt, publishers);
4864
5704
  boundCommandOf.set(entry.value, rebuilt);
4865
5705
  commands[entry.name] = rebuilt;
4866
5706
  }
@@ -4925,6 +5765,7 @@ function applicationOf2(settings) {
4925
5765
  continue;
4926
5766
  const made = await startDeferred(instance, context);
4927
5767
  checkTransactions(name, made, Object.entries(commands).filter(([, command]) => command.transaction && commandTransactionProvider(command) === instance).map(([key]) => key));
5768
+ checkCapabilities(name, made, Object.entries(repositories).filter(([, repository]) => providerOfRepository(repository) === instance && inspectRepository(repository).actions.includes("findCursor")).map(([key]) => key));
4928
5769
  }
4929
5770
  started = true;
4930
5771
  };
@@ -4936,6 +5777,7 @@ function applicationOf2(settings) {
4936
5777
  logger: logger ?? silentLogger,
4937
5778
  context,
4938
5779
  ...logger ? { [ApplicationLoggerKey]: logger } : {},
5780
+ ...hasPublishers ? { [ApplicationPublishersKey]: publishers } : {},
4939
5781
  properties: collection("properties"),
4940
5782
  blueprints: collection("blueprints"),
4941
5783
  records: collection("records"),
@@ -4948,13 +5790,13 @@ function applicationOf2(settings) {
4948
5790
  exceptions: collection("exceptions"),
4949
5791
  useCase(useCase) {
4950
5792
  if (!boundUseCaseOf.has(useCase)) {
4951
- invalid7(name, "Expected a use case registered in the application.");
5793
+ invalid8(name, "Expected a use case registered in the application.");
4952
5794
  }
4953
5795
  return boundUseCaseOf.get(useCase);
4954
5796
  },
4955
5797
  async run(useCase, input = {}, options) {
4956
5798
  if (!boundUseCaseOf.has(useCase)) {
4957
- invalid7(name, "Expected a use case registered in the application.");
5799
+ invalid8(name, "Expected a use case registered in the application.");
4958
5800
  }
4959
5801
  const Bound = boundUseCaseOf.get(useCase);
4960
5802
  const instance = options?.context === undefined ? new Bound(input) : new Bound(input, { context: options.context });
@@ -4962,19 +5804,19 @@ function applicationOf2(settings) {
4962
5804
  },
4963
5805
  command(command) {
4964
5806
  if (!boundCommandOf.has(command)) {
4965
- invalid7(name, "Expected a command registered in the application.");
5807
+ invalid8(name, "Expected a command registered in the application.");
4966
5808
  }
4967
5809
  return boundCommandOf.get(command);
4968
5810
  },
4969
5811
  output(output) {
4970
5812
  if (!boundOutputOf.has(output)) {
4971
- invalid7(name, "Expected an output registered in the application.");
5813
+ invalid8(name, "Expected an output registered in the application.");
4972
5814
  }
4973
5815
  return boundOutputOf.get(output);
4974
5816
  },
4975
5817
  repository(repository) {
4976
5818
  if (!bound.has(repository)) {
4977
- invalid7(name, "Expected a repository registered in the application.");
5819
+ invalid8(name, "Expected a repository registered in the application.");
4978
5820
  }
4979
5821
  return bound.get(repository);
4980
5822
  },
@@ -4986,7 +5828,7 @@ function applicationOf2(settings) {
4986
5828
  ...declaredLog === undefined ? {} : { log: declaredLog }
4987
5829
  });
4988
5830
  },
4989
- describe: () => describeRegistry(name, extended, registry, defaultSlot !== undefined ? referenceOf(defaultSlot) : provider === undefined ? undefined : providerDescriptionOf2(provider), named, isLogFactory2(declaredLog) ? "opaque" : log?.settings),
5831
+ describe: () => describeRegistry(name, extended, registry, defaultSlot !== undefined ? referenceOf(defaultSlot) : provider === undefined ? undefined : providerDescriptionOf2(provider), named, isLogFactory2(declaredLog) ? "opaque" : log?.settings, origins),
4990
5832
  get started() {
4991
5833
  return started;
4992
5834
  },
@@ -5039,7 +5881,7 @@ function applicationOf2(settings) {
5039
5881
  get(declaration) {
5040
5882
  if (isEnvironment2(declaration)) {
5041
5883
  if (!registeredEnvironments.has(declaration)) {
5042
- invalid7(name, "Expected an environment registered in the application.");
5884
+ invalid8(name, "Expected an environment registered in the application.");
5043
5885
  }
5044
5886
  return declaration.load();
5045
5887
  }
@@ -5055,11 +5897,11 @@ function applicationOf2(settings) {
5055
5897
  if (isProviderSlot2(declaration)) {
5056
5898
  const instance = instances.get(declaration);
5057
5899
  if (instance === undefined) {
5058
- invalid7(name, "Expected a provider of a storage registered in the application.");
5900
+ invalid8(name, "Expected a provider of a storage registered in the application.");
5059
5901
  }
5060
5902
  return instance;
5061
5903
  }
5062
- return invalid7(name, "Expected a repository, use case, command, output, environment or provider registered in the application.");
5904
+ return invalid8(name, "Expected a repository, use case, command, output, environment or provider registered in the application.");
5063
5905
  }
5064
5906
  });
5065
5907
  internals.set(application, registry);
@@ -5068,6 +5910,7 @@ function applicationOf2(settings) {
5068
5910
  defaultSlots.set(application, defaultSlot);
5069
5911
  if (declaredLog !== undefined)
5070
5912
  logSettings.set(application, declaredLog);
5913
+ publisherOrigins.set(application, Object.freeze(origins));
5071
5914
  return application;
5072
5915
  }
5073
5916
  // src/core/blueprint/blueprint-plugin.ts
@@ -5078,6 +5921,140 @@ function blueprintPluginOf2(name, properties, factories = {}) {
5078
5921
  factories: Object.freeze({ ...factories })
5079
5922
  });
5080
5923
  }
5924
+ // src/core/event-policy/publish.ts
5925
+ var KEYS2 = ["publisher", "topic", "events", "handle", "onPublishError"];
5926
+ var IDENTIFIER3 = /^[A-Za-z_$][\w$]*$/;
5927
+ var PLACEHOLDER = /\{(event|command|operation|id)\}/g;
5928
+ function validate2(options) {
5929
+ const given = options;
5930
+ for (const key of Object.keys(given)) {
5931
+ if (!KEYS2.includes(key))
5932
+ return `publishEvents has no option "${key}".`;
5933
+ }
5934
+ const { publisher, topic, events, handle, onPublishError } = given;
5935
+ if (typeof publisher !== "string" || !IDENTIFIER3.test(publisher)) {
5936
+ return "Expected publishEvents.publisher to be the key of a publisher of the application.";
5937
+ }
5938
+ if (typeof topic !== "string" || topic === "") {
5939
+ return 'Expected publishEvents.topic to be a non-empty string, such as "posts/{id}".';
5940
+ }
5941
+ const unknown = topic.replaceAll(PLACEHOLDER, "").match(/\{[^}]*\}/);
5942
+ if (unknown) {
5943
+ return `publishEvents.topic has an unknown placeholder ${unknown[0]}: use {event}, {command}, {operation} or {id}.`;
5944
+ }
5945
+ if (events !== undefined && (!Array.isArray(events) || events.some((name) => typeof name !== "string" || name === ""))) {
5946
+ return "Expected publishEvents.events to be a list of event names.";
5947
+ }
5948
+ if (handle !== undefined && typeof handle !== "boolean") {
5949
+ return "Expected publishEvents.handle to be a boolean.";
5950
+ }
5951
+ if (onPublishError !== undefined && onPublishError !== "log" && onPublishError !== "throw") {
5952
+ return 'Expected publishEvents.onPublishError to be "log" or "throw".';
5953
+ }
5954
+ return;
5955
+ }
5956
+ function entityJson(event) {
5957
+ const text = JSON.stringify(event.entity);
5958
+ return text === undefined ? null : JSON.parse(text);
5959
+ }
5960
+ var publishEvents2 = eventPolicyOf2({
5961
+ name: "publishEvents",
5962
+ validate: validate2,
5963
+ publishers: (options) => [options.publisher],
5964
+ handle: async (events, info, options) => {
5965
+ const { command, operation, logger } = info;
5966
+ const commandName = command.commandName;
5967
+ const source = `${commandName}.${operation}`;
5968
+ const only = options.events ? new Set(options.events) : undefined;
5969
+ const failed = (cause, event, index) => new CommandEventFailed2({
5970
+ source,
5971
+ cause,
5972
+ details: {
5973
+ operation,
5974
+ event: event.name,
5975
+ handled: index,
5976
+ remaining: events.length - index - 1
5977
+ }
5978
+ });
5979
+ for (const [index, event] of events.entries()) {
5980
+ if (options.handle !== false) {
5981
+ try {
5982
+ await event.handle({ logger });
5983
+ } catch (cause) {
5984
+ throw failed(cause, event, index);
5985
+ }
5986
+ }
5987
+ if (only && !only.has(event.name))
5988
+ continue;
5989
+ let publisher;
5990
+ try {
5991
+ publisher = info.publisher(options.publisher);
5992
+ } catch (cause) {
5993
+ throw failed(cause, event, index);
5994
+ }
5995
+ const entity = entityJson(event);
5996
+ const id = entity?.id;
5997
+ const topic = options.topic.replaceAll(PLACEHOLDER, (_, key) => key === "event" ? event.name : key === "command" ? commandName : key === "operation" ? operation : id === undefined ? "" : String(id));
5998
+ const published = Object.freeze({
5999
+ name: event.name,
6000
+ command: commandName,
6001
+ operation,
6002
+ entity
6003
+ });
6004
+ try {
6005
+ await publisher.publish(topic, published);
6006
+ } catch (cause) {
6007
+ if (options.onPublishError === "throw")
6008
+ throw failed(cause, event, index);
6009
+ logFailure(logger, "event-publish-failed", {
6010
+ command: commandName,
6011
+ operation,
6012
+ event: event.name,
6013
+ publisher: options.publisher,
6014
+ topic
6015
+ }, cause);
6016
+ }
6017
+ }
6018
+ }
6019
+ });
6020
+ // src/core/output/coerce.ts
6021
+ var scalarTypes = new WeakMap;
6022
+ function scalarTypesOf(blueprint) {
6023
+ let types = scalarTypes.get(blueprint);
6024
+ if (!types) {
6025
+ const schema = toOpenAPI2(blueprint);
6026
+ const found = {};
6027
+ for (const [key, property] of Object.entries(schema.properties ?? {})) {
6028
+ found[key] = [property.type ?? []].flat();
6029
+ }
6030
+ types = Object.freeze(found);
6031
+ scalarTypes.set(blueprint, types);
6032
+ }
6033
+ return types;
6034
+ }
6035
+ function coerce(text, types) {
6036
+ if (!types || types.includes("string"))
6037
+ return text;
6038
+ const trimmed = text.trim();
6039
+ if (types.includes("boolean") && (trimmed === "true" || trimmed === "false")) {
6040
+ return trimmed === "true";
6041
+ }
6042
+ if (types.includes("integer") || types.includes("number")) {
6043
+ const number = trimmed === "" ? Number.NaN : Number(trimmed);
6044
+ if (Number.isFinite(number))
6045
+ return number;
6046
+ }
6047
+ return text;
6048
+ }
6049
+ function coerceTextValues2(blueprint, texts) {
6050
+ const types = scalarTypesOf(blueprint);
6051
+ const out = {};
6052
+ for (const [key, value] of Object.entries(texts)) {
6053
+ if (value !== undefined)
6054
+ out[key] = coerce(value, types[key]);
6055
+ }
6056
+ return out;
6057
+ }
5081
6058
  // src/core/output/status.ts
5082
6059
  var INVALID_INPUT = new Set([
5083
6060
  "InvalidUseCaseInput",
@@ -5092,6 +6069,7 @@ var NOT_FOUND = new Set([
5092
6069
  ]);
5093
6070
  var CONFLICT = new Set([
5094
6071
  "RepositoryUniqueViolation",
6072
+ "RepositoryConcurrencyConflict",
5095
6073
  "UseCaseEntityConflict"
5096
6074
  ]);
5097
6075
  function raisedOf(error) {
@@ -5159,6 +6137,8 @@ function outputValueOf2(value) {
5159
6137
 
5160
6138
  // src/core/output/openapi-sink.ts
5161
6139
  var METHODS3 = ["get", "post", "put", "patch", "delete"];
6140
+ var SUCCESS_STATUSES = [200, 201, 202, 204];
6141
+ var REQUEST_ID_HEADER = "x-request-id";
5162
6142
  function isObject(value) {
5163
6143
  return typeof value === "object" && value !== null && !Array.isArray(value);
5164
6144
  }
@@ -5169,36 +6149,199 @@ function onlyKeys(value, allowed, where) {
5169
6149
  }
5170
6150
  return;
5171
6151
  }
5172
- function isPath(value) {
5173
- return typeof value === "string" && value.startsWith("/") && !/[{}?#\s]/.test(value);
6152
+ var PARAMETER = /^\{([A-Za-z_][A-Za-z0-9_]*)\}$/;
6153
+ var HEADER_NAME = /^[A-Za-z0-9!#$%&'*+.^_`|~-]+$/;
6154
+ var COMPONENT_NAME = /^[A-Za-z0-9._-]+$/;
6155
+ function isPath(value, parameters = true) {
6156
+ if (typeof value !== "string" || !value.startsWith("/"))
6157
+ return false;
6158
+ return value.split("/").every((segment) => parameters && PARAMETER.test(segment) || !/[{}?#\s:*]/.test(segment));
6159
+ }
6160
+ function parametersOfPath(path) {
6161
+ const names = [];
6162
+ for (const segment of path.split("/")) {
6163
+ const name = PARAMETER.exec(segment)?.[1];
6164
+ if (name)
6165
+ names.push(name);
6166
+ }
6167
+ return names;
6168
+ }
6169
+ function isHeaderValue(value) {
6170
+ if (typeof value !== "string")
6171
+ return false;
6172
+ for (let i = 0;i < value.length; i++) {
6173
+ const code = value.charCodeAt(i);
6174
+ if (code < 32 && code !== 9 || code === 127)
6175
+ return false;
6176
+ }
6177
+ return true;
6178
+ }
6179
+ function isScopes(value) {
6180
+ return value === undefined || Array.isArray(value) && value.every((scope) => typeof scope === "string" && scope !== "");
6181
+ }
6182
+ var SCHEME_KEYS = {
6183
+ http: ["scheme", "bearerFormat"],
6184
+ apiKey: ["name", "in"],
6185
+ oauth2: ["flows"],
6186
+ openIdConnect: ["openIdConnectUrl"],
6187
+ mutualTLS: []
6188
+ };
6189
+ function schemeIssue(scheme, where) {
6190
+ if (!isObject(scheme) || typeof scheme.type !== "string") {
6191
+ return `Expected ${where} to be a security scheme with a "type".`;
6192
+ }
6193
+ const keys = Object.hasOwn(SCHEME_KEYS, scheme.type) ? SCHEME_KEYS[scheme.type] : undefined;
6194
+ if (!keys) {
6195
+ return `Expected ${where}.type to be one of ${Object.keys(SCHEME_KEYS).join(", ")}.`;
6196
+ }
6197
+ const bad = onlyKeys(scheme, ["type", "description", ...keys], where);
6198
+ if (bad)
6199
+ return bad;
6200
+ if (scheme.description !== undefined && typeof scheme.description !== "string") {
6201
+ return `Expected ${where}.description to be a string.`;
6202
+ }
6203
+ switch (scheme.type) {
6204
+ case "http":
6205
+ if (typeof scheme.scheme !== "string" || !scheme.scheme) {
6206
+ return `Expected ${where}.scheme to be a scheme such as "bearer".`;
6207
+ }
6208
+ if (scheme.bearerFormat !== undefined && typeof scheme.bearerFormat !== "string") {
6209
+ return `Expected ${where}.bearerFormat to be a string.`;
6210
+ }
6211
+ return;
6212
+ case "apiKey":
6213
+ if (typeof scheme.name !== "string" || !scheme.name) {
6214
+ return `Expected ${where}.name to be a non-empty string.`;
6215
+ }
6216
+ if (!["header", "query", "cookie"].includes(scheme.in)) {
6217
+ return `Expected ${where}.in to be "header", "query" or "cookie".`;
6218
+ }
6219
+ return;
6220
+ case "oauth2":
6221
+ return isObject(scheme.flows) ? undefined : `Expected ${where}.flows to be an object.`;
6222
+ case "openIdConnect":
6223
+ return typeof scheme.openIdConnectUrl === "string" && scheme.openIdConnectUrl ? undefined : `Expected ${where}.openIdConnectUrl to be a URL.`;
6224
+ default:
6225
+ return;
6226
+ }
6227
+ }
6228
+ function requirementIssue(requirement, schemes, where) {
6229
+ if (!isObject(requirement) || onlyKeys(requirement, ["scheme", "scopes"], where) || typeof requirement.scheme !== "string" || !isScopes(requirement.scopes)) {
6230
+ return `Expected ${where} to be { scheme, scopes? }.`;
6231
+ }
6232
+ if (!Object.hasOwn(schemes, requirement.scheme)) {
6233
+ return `${where}.scheme names the unknown security scheme "${requirement.scheme}".`;
6234
+ }
6235
+ return;
6236
+ }
6237
+ function securityIssue(security, where) {
6238
+ if (!isObject(security))
6239
+ return `Expected ${where} to be an object.`;
6240
+ const bad = onlyKeys(security, ["schemes", "operations", "default"], where);
6241
+ if (bad)
6242
+ return bad;
6243
+ const { schemes, operations } = security;
6244
+ if (!isObject(schemes))
6245
+ return `Expected ${where}.schemes to be an object.`;
6246
+ for (const [name, scheme] of Object.entries(schemes)) {
6247
+ if (!COMPONENT_NAME.test(name)) {
6248
+ return `Expected the security scheme "${name}" to be named with letters, digits, ".", "_" or "-".`;
6249
+ }
6250
+ const schemeBad = schemeIssue(scheme, `${where}.schemes["${name}"]`);
6251
+ if (schemeBad)
6252
+ return schemeBad;
6253
+ }
6254
+ if (operations !== undefined) {
6255
+ if (!isObject(operations)) {
6256
+ return `Expected ${where}.operations to be an object.`;
6257
+ }
6258
+ for (const [id, requirement] of Object.entries(operations)) {
6259
+ if (requirement === false)
6260
+ continue;
6261
+ const requirementBad = requirementIssue(requirement, schemes, `${where}.operations["${id}"]`);
6262
+ if (requirementBad)
6263
+ return requirementBad;
6264
+ }
6265
+ }
6266
+ if (security.default !== undefined) {
6267
+ return requirementIssue(security.default, schemes, `${where}.default`);
6268
+ }
6269
+ return;
5174
6270
  }
6271
+ var ROUTE_OPTION_KEYS2 = [
6272
+ "basePath",
6273
+ "route",
6274
+ "responses",
6275
+ "requestId",
6276
+ "security"
6277
+ ];
5175
6278
  function routeOptionsIssue2(options, where) {
5176
- const { basePath, route } = options;
5177
- if (basePath !== undefined && (typeof basePath !== "string" || basePath !== "" && !isPath(basePath))) {
6279
+ const { basePath, route, responses, requestId, security } = options;
6280
+ if (basePath !== undefined && (typeof basePath !== "string" || basePath !== "" && !isPath(basePath, false))) {
5178
6281
  return `Expected ${where}.basePath to be "" or a path such as "/api".`;
5179
6282
  }
5180
6283
  if (route !== undefined) {
5181
6284
  if (!isObject(route))
5182
6285
  return `Expected ${where}.route to be an object.`;
5183
6286
  for (const [id, entry] of Object.entries(route)) {
5184
- if (!isObject(entry) || onlyKeys(entry, ["method", "path"], "route")) {
5185
- return `Expected ${where}.route["${id}"] to be { method?, path? }.`;
6287
+ if (!isObject(entry) || onlyKeys(entry, ["method", "path", "status"], "route")) {
6288
+ return `Expected ${where}.route["${id}"] to be { method?, path?, status? }.`;
5186
6289
  }
5187
6290
  if (entry.method !== undefined && !METHODS3.includes(entry.method)) {
5188
6291
  return `Expected ${where}.route["${id}"].method to be one of ${METHODS3.join(", ")}.`;
5189
6292
  }
5190
- if (entry.path !== undefined && !isPath(entry.path)) {
5191
- return `Expected ${where}.route["${id}"].path to start with "/" and have no parameters or query.`;
6293
+ if (entry.path !== undefined) {
6294
+ if (!isPath(entry.path)) {
6295
+ return `Expected ${where}.route["${id}"].path to start with "/", with parameters only as whole "{name}" segments and no query.`;
6296
+ }
6297
+ const names = parametersOfPath(entry.path);
6298
+ if (new Set(names).size !== names.length) {
6299
+ return `Expected ${where}.route["${id}"].path to name each parameter once.`;
6300
+ }
6301
+ }
6302
+ if (entry.status !== undefined && !SUCCESS_STATUSES.includes(entry.status)) {
6303
+ return `Expected ${where}.route["${id}"].status to be one of ${SUCCESS_STATUSES.join(", ")}.`;
6304
+ }
6305
+ }
6306
+ }
6307
+ if (responses !== undefined) {
6308
+ if (!isObject(responses)) {
6309
+ return `Expected ${where}.responses to be an object.`;
6310
+ }
6311
+ for (const [id, entry] of Object.entries(responses)) {
6312
+ const at = `${where}.responses["${id}"]`;
6313
+ if (!isObject(entry) || onlyKeys(entry, ["headers"], at) || entry.headers !== undefined && !isObject(entry.headers)) {
6314
+ return `Expected ${at} to be { headers?: { name: value } }.`;
6315
+ }
6316
+ for (const [name, value] of Object.entries(entry.headers ?? {})) {
6317
+ if (!HEADER_NAME.test(name)) {
6318
+ return `Expected ${at}.headers to be named by header names, not "${name}".`;
6319
+ }
6320
+ if (name.toLowerCase() === "content-type") {
6321
+ return `${at}.headers cannot set "content-type": the sink sets it.`;
6322
+ }
6323
+ if (!isHeaderValue(value)) {
6324
+ return `Expected ${at}.headers["${name}"] to be text without line breaks.`;
6325
+ }
5192
6326
  }
5193
6327
  }
5194
6328
  }
6329
+ if (requestId !== undefined && requestId !== true && (typeof requestId !== "string" || !HEADER_NAME.test(requestId))) {
6330
+ return `Expected ${where}.requestId to be true or a header name.`;
6331
+ }
6332
+ if (security !== undefined) {
6333
+ return securityIssue(security, `${where}.security`);
6334
+ }
5195
6335
  return;
5196
6336
  }
5197
- function validate2(options) {
5198
- const bad = onlyKeys(options, ["info", "servers", "basePath", "route"], "openapi");
6337
+ function requestIdHeaderOf2(options) {
6338
+ return options.requestId === true ? REQUEST_ID_HEADER : options.requestId ?? undefined;
6339
+ }
6340
+ function validate3(options) {
6341
+ const bad = onlyKeys(options, ["info", "servers", ...ROUTE_OPTION_KEYS2], "openapi");
5199
6342
  if (bad)
5200
6343
  return bad;
5201
- const { info, servers, basePath, route } = options;
6344
+ const { info, servers } = options;
5202
6345
  if (!isObject(info))
5203
6346
  return "Expected openapi.info to be an object.";
5204
6347
  const infoBad = onlyKeys(info, ["title", "version", "description"], "info");
@@ -5221,21 +6364,38 @@ function validate2(options) {
5221
6364
  }
5222
6365
  }
5223
6366
  }
5224
- return routeOptionsIssue2({ basePath, route }, "openapi");
6367
+ return routeOptionsIssue2(options, "openapi");
5225
6368
  }
5226
- function invalid8(reason, source = "openapi") {
6369
+ function invalid9(reason, source = "openapi") {
5227
6370
  throw new InvalidOutputStage2({ source, details: { reason } });
5228
6371
  }
5229
6372
  function kebab(text) {
5230
6373
  return text.replace(/([a-z0-9])([A-Z])/g, "$1-$2").replace(/([A-Z]+)([A-Z][a-z])/g, "$1-$2").toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
5231
6374
  }
6375
+ function isScalarSchema(schema) {
6376
+ const types = [schema.type ?? []].flat();
6377
+ return !types.some((type) => type === "object" || type === "array");
6378
+ }
6379
+ function isScalarKey(operation, name) {
6380
+ if (!Object.hasOwn(operation.input.properties, name))
6381
+ return false;
6382
+ const field = operation.input.properties[name];
6383
+ return isProperty2(field) && isScalarSchema(toOpenAPI2(field));
6384
+ }
5232
6385
  function operationRoutes2(operations, options, source = "openapi") {
5233
6386
  const base = options.basePath ?? "";
5234
6387
  const route = options.route ?? {};
6388
+ const responses = options.responses ?? {};
5235
6389
  const ids = new Set(operations.map(({ id }) => id));
5236
- for (const id of Object.keys(route)) {
5237
- if (!ids.has(id)) {
5238
- invalid8(`${source}.route names the unknown operation "${id}".`, source);
6390
+ for (const [where, entries] of [
6391
+ ["route", route],
6392
+ ["responses", responses],
6393
+ ["security.operations", options.security?.operations ?? {}]
6394
+ ]) {
6395
+ for (const id of Object.keys(entries)) {
6396
+ if (!ids.has(id)) {
6397
+ invalid9(`${source}.${where} names the unknown operation "${id}".`, source);
6398
+ }
5239
6399
  }
5240
6400
  }
5241
6401
  const routes = new Map;
@@ -5244,12 +6404,24 @@ function operationRoutes2(operations, options, source = "openapi") {
5244
6404
  const chosen = route[operation.id] ?? {};
5245
6405
  const method = chosen.method ?? "post";
5246
6406
  const path = base + (chosen.path ?? `/${kebab(operation.command)}/${kebab(operation.name)}`);
5247
- const key = `${method} ${path}`;
6407
+ const parameters = parametersOfPath(path);
6408
+ for (const name of parameters) {
6409
+ if (!isScalarKey(operation, name)) {
6410
+ invalid9(`${source}.route["${operation.id}"].path has the parameter "{${name}}", which is not a scalar key of the input of ${operation.id}.`, source);
6411
+ }
6412
+ }
6413
+ const key = `${method} ${path.replace(/\{[^}]*\}/g, "{}")}`;
5248
6414
  if (taken.has(key)) {
5249
- invalid8(`Two operations are routed to ${method.toUpperCase()} ${path}.`, source);
6415
+ invalid9(`Two operations are routed to ${method.toUpperCase()} ${path}.`, source);
5250
6416
  }
5251
6417
  taken.add(key);
5252
- routes.set(operation.id, { method, path });
6418
+ routes.set(operation.id, {
6419
+ method,
6420
+ path,
6421
+ parameters,
6422
+ status: chosen.status ?? 200,
6423
+ headers: { ...responses[operation.id]?.headers ?? {} }
6424
+ });
5253
6425
  }
5254
6426
  return routes;
5255
6427
  }
@@ -5312,7 +6484,13 @@ var STATUS_TEXT = {
5312
6484
  422: "Declared error of the operation.",
5313
6485
  500: "Internal error."
5314
6486
  };
5315
- function responsesOf(operation, statuses, response) {
6487
+ var SUCCESS_TEXT = {
6488
+ 200: "Success.",
6489
+ 201: "Created.",
6490
+ 202: "Accepted.",
6491
+ 204: "No content."
6492
+ };
6493
+ function responsesOf(operation, statuses, response, route, requestIdHeader) {
5316
6494
  const names = new Map([
5317
6495
  [400, []],
5318
6496
  [500, []]
@@ -5330,10 +6508,22 @@ function responsesOf(operation, statuses, response) {
5330
6508
  list.push(Declared.name);
5331
6509
  names.set(status, list);
5332
6510
  }
6511
+ const idHeaders = requestIdHeader ? {
6512
+ [requestIdHeader]: {
6513
+ description: "The id of the request: the one it sent in this header, else a new one.",
6514
+ schema: { type: "string" }
6515
+ }
6516
+ } : {};
6517
+ const successHeaders = { ...idHeaders };
6518
+ for (const [name, value] of Object.entries(route.headers)) {
6519
+ successHeaders[name] = { schema: { type: "string", const: value } };
6520
+ }
6521
+ const withHeaders = (headers) => Object.keys(headers).length > 0 ? { headers } : {};
5333
6522
  const responses = {
5334
- "200": {
5335
- description: "Success.",
5336
- content: { "application/json": { schema: response } }
6523
+ [String(route.status)]: {
6524
+ description: SUCCESS_TEXT[route.status],
6525
+ ...withHeaders(successHeaders),
6526
+ ...route.status === 204 ? {} : { content: { "application/json": { schema: response } } }
5337
6527
  }
5338
6528
  };
5339
6529
  for (const status of [...names.keys()].sort((a, b) => a - b)) {
@@ -5341,34 +6531,55 @@ function responsesOf(operation, statuses, response) {
5341
6531
  const text = STATUS_TEXT[status] ?? `Error ${status}.`;
5342
6532
  responses[String(status)] = {
5343
6533
  description: declared.length > 0 ? `${text} Declared: ${declared.join(", ")}.` : text,
6534
+ ...withHeaders(idHeaders),
5344
6535
  content: problemContent
5345
6536
  };
5346
6537
  }
5347
6538
  return responses;
5348
6539
  }
5349
- function parametersOf(operation, schema) {
6540
+ function parameterOf(name, property, where, required) {
6541
+ const { description, ...rest } = property;
6542
+ return {
6543
+ name,
6544
+ in: where,
6545
+ required: where === "path" || required,
6546
+ ...typeof description === "string" ? { description } : {},
6547
+ schema: rest
6548
+ };
6549
+ }
6550
+ function queryParametersOf(operation, schema) {
5350
6551
  const required = new Set(schema.required ?? []);
5351
6552
  return Object.entries(schema.properties ?? {}).map(([name, property]) => {
5352
- const field = operation.input.properties[name];
5353
- const type = property.type;
5354
- if (!isProperty2(field) || type === "object" || type === "array" || Array.isArray(type) && type.some((t) => t === "object" || t === "array")) {
5355
- invalid8(`${operation.id} uses a ${"get/delete"} route, whose input becomes query parameters, but "${name}" is not a scalar; route it with method "post".`);
6553
+ if (!isProperty2(operation.input.properties[name]) || !isScalarSchema(property)) {
6554
+ invalid9(`${operation.id} uses a get/delete route, whose input becomes query parameters, but "${name}" is not a scalar; route it with method "post".`);
5356
6555
  }
5357
- const { description, ...rest } = property;
5358
- return {
5359
- name,
5360
- in: "query",
5361
- required: required.has(name),
5362
- ...typeof description === "string" ? { description } : {},
5363
- schema: rest
5364
- };
6556
+ return parameterOf(name, property, "query", required.has(name));
5365
6557
  });
5366
6558
  }
6559
+ function withoutKeys(schema, keys) {
6560
+ if (keys.length === 0)
6561
+ return schema;
6562
+ const properties = { ...schema.properties ?? {} };
6563
+ for (const key of keys)
6564
+ delete properties[key];
6565
+ const required = (schema.required ?? []).filter((key) => !keys.includes(key));
6566
+ const { required: _, ...rest } = schema;
6567
+ return {
6568
+ ...rest,
6569
+ properties,
6570
+ ...required.length > 0 ? { required } : {}
6571
+ };
6572
+ }
6573
+ function requirementOf(requirement) {
6574
+ return { [requirement.scheme]: [...requirement.scopes ?? []] };
6575
+ }
5367
6576
  var openapi2 = sinkOf2({
5368
6577
  name: "openapi",
5369
- validate: validate2,
6578
+ validate: validate3,
5370
6579
  build: (operations, options, { output }) => {
5371
6580
  const routes = operationRoutes2(operations, options);
6581
+ const requestIdHeader = requestIdHeaderOf2(options);
6582
+ const security = options.security;
5372
6583
  const records = new Set;
5373
6584
  for (const operation of operations) {
5374
6585
  collectRecords(operation.input.properties, records);
@@ -5378,11 +6589,11 @@ var openapi2 = sinkOf2({
5378
6589
  const classes = new Map;
5379
6590
  for (const record of records) {
5380
6591
  const name = recordNameOf2(record);
5381
- if (!/^[A-Za-z0-9._-]+$/.test(name) || name === "Problem") {
5382
- invalid8(`The record class "${name}" cannot be a component name; give the class a name that is not "Problem".`);
6592
+ if (!COMPONENT_NAME.test(name) || name === "Problem") {
6593
+ invalid9(`The record class "${name}" cannot be a component name; give the class a name that is not "Problem".`);
5383
6594
  }
5384
6595
  if (classes.has(name)) {
5385
- invalid8(`Two record classes are named "${name}"; rename one.`);
6596
+ invalid9(`Two record classes are named "${name}"; rename one.`);
5386
6597
  }
5387
6598
  classes.set(name, record);
5388
6599
  models[name] = record;
@@ -5399,40 +6610,52 @@ var openapi2 = sinkOf2({
5399
6610
  components[name] = schemas[name];
5400
6611
  components.Problem = problemSchema();
5401
6612
  for (const operation of operations) {
5402
- const { method, path } = routes.get(operation.id);
6613
+ const route = routes.get(operation.id);
6614
+ const { method, path } = route;
5403
6615
  const operationId = operation.id.replaceAll(".", "_");
5404
6616
  if (operationIds.has(operationId)) {
5405
- invalid8(`The operationId "${operationId}" is produced twice.`);
6617
+ invalid9(`The operationId "${operationId}" is produced twice.`);
5406
6618
  }
5407
6619
  operationIds.add(operationId);
5408
6620
  const slot = paths[path] ?? {};
5409
6621
  paths[path] = slot;
5410
- const input = schemas[`${operation.id}#input`];
6622
+ const full = schemas[`${operation.id}#input`];
6623
+ const input = withoutKeys(full, route.parameters);
5411
6624
  const response = schemas[`${operation.id}#response`];
5412
6625
  const hasBody = method !== "get" && method !== "delete";
5413
6626
  const hasInput = Object.keys(input.properties ?? {}).length > 0;
6627
+ const parameters = [
6628
+ ...route.parameters.map((name) => parameterOf(name, full.properties?.[name], "path", true)),
6629
+ ...hasBody ? [] : queryParametersOf(operation, input)
6630
+ ];
6631
+ const requirement = security?.operations?.[operation.id];
5414
6632
  slot[method] = {
5415
6633
  operationId,
5416
6634
  summary: operation.id,
5417
6635
  ...operation.description ? { description: operation.description } : {},
5418
6636
  ...operation.tags.length > 0 ? { tags: [...operation.tags] } : {},
5419
6637
  ...operation.deprecated ? { deprecated: true } : {},
5420
- ...!hasBody ? { parameters: parametersOf(operation, input) } : {},
6638
+ ...!hasBody || parameters.length > 0 ? { parameters } : {},
5421
6639
  ...hasBody && hasInput ? {
5422
6640
  requestBody: {
5423
6641
  required: true,
5424
6642
  content: { "application/json": { schema: input } }
5425
6643
  }
5426
6644
  } : {},
5427
- responses: responsesOf(operation, output.statuses, response)
6645
+ responses: responsesOf(operation, output.statuses, response, route, requestIdHeader),
6646
+ ...requirement === false ? { security: [] } : requirement ? { security: [requirementOf(requirement)] } : {}
5428
6647
  };
5429
6648
  }
5430
6649
  return {
5431
6650
  openapi: "3.1.0",
5432
6651
  info: options.info,
5433
6652
  ...options.servers ? { servers: options.servers } : {},
6653
+ ...security?.default ? { security: [requirementOf(security.default)] } : {},
5434
6654
  paths,
5435
- components: { schemas: components }
6655
+ components: {
6656
+ schemas: components,
6657
+ ...security ? { securitySchemes: security.schemes } : {}
6658
+ }
5436
6659
  };
5437
6660
  }
5438
6661
  });
@@ -5464,6 +6687,41 @@ var select2 = transformOf2({
5464
6687
  return operation;
5465
6688
  }
5466
6689
  });
6690
+ // src/core/parse/strip.ts
6691
+ function collectionOf(properties, value) {
6692
+ if (!isPlainObject(value))
6693
+ return value;
6694
+ const kept = {};
6695
+ for (const [name, field] of fieldsOf(properties)) {
6696
+ if (!Object.hasOwn(value, name))
6697
+ continue;
6698
+ const item = fieldOf2(field, value[name]);
6699
+ if (item === undefined)
6700
+ continue;
6701
+ Object.defineProperty(kept, name, {
6702
+ value: item,
6703
+ enumerable: true,
6704
+ writable: true,
6705
+ configurable: true
6706
+ });
6707
+ }
6708
+ return kept;
6709
+ }
6710
+ function fieldOf2(field, value) {
6711
+ if (isBlueprintList2(field)) {
6712
+ return Array.isArray(value) ? value.map((item) => fieldOf2(field.item, item)) : value;
6713
+ }
6714
+ if (isBlueprint2(field))
6715
+ return collectionOf(field.properties, value);
6716
+ const record = recordFieldOf2(field);
6717
+ if (record) {
6718
+ return collectionOf(record.record.blueprint.properties, value);
6719
+ }
6720
+ return value;
6721
+ }
6722
+ function stripUndeclared2(shape, value) {
6723
+ return fieldOf2(shape, value);
6724
+ }
5467
6725
  // src/core/record/plugin-catalog.ts
5468
6726
  var LISTS2 = ["keys", "sensitive", "unique", "change", "members"];
5469
6727
  function isDescriptor(value) {
@@ -5574,37 +6832,37 @@ function recordPluginCatalogOf2(...plugins) {
5574
6832
  });
5575
6833
  }
5576
6834
  // src/std/properties/contact.ts
5577
- import { Type } from "typebox";
6835
+ import { Type as Type2 } from "typebox";
5578
6836
  var Email2 = propertyOf2({
5579
6837
  name: "email",
5580
- schema: Type.String({ format: "email", maxLength: 254 }),
6838
+ schema: Type2.String({ format: "email", maxLength: 254 }),
5581
6839
  default: "demo@example.com",
5582
6840
  preHandle: (value) => value.trim().toLowerCase()
5583
6841
  });
5584
6842
  var Phone2 = propertyOf2({
5585
6843
  name: "phone",
5586
- schema: Type.String({ pattern: "^\\+[1-9]\\d{1,14}$" }),
6844
+ schema: Type2.String({ pattern: "^\\+[1-9]\\d{1,14}$" }),
5587
6845
  default: "+5511999999999",
5588
6846
  preHandle: (value) => value.replace(/[\s().-]/g, "")
5589
6847
  });
5590
6848
  var Url2 = propertyOf2({
5591
6849
  name: "url",
5592
- schema: Type.String({ format: "url", maxLength: 2048 }),
6850
+ schema: Type2.String({ format: "url", maxLength: 2048 }),
5593
6851
  default: "https://example.com",
5594
6852
  preHandle: (value) => value.trim()
5595
6853
  });
5596
6854
  var Ip2 = propertyOf2({
5597
6855
  name: "ip",
5598
- schema: Type.Union([
5599
- Type.String({ format: "ipv4" }),
5600
- Type.String({ format: "ipv6" })
6856
+ schema: Type2.Union([
6857
+ Type2.String({ format: "ipv4" }),
6858
+ Type2.String({ format: "ipv6" })
5601
6859
  ]),
5602
6860
  default: "127.0.0.1",
5603
6861
  preHandle: (value) => value.trim()
5604
6862
  });
5605
6863
 
5606
6864
  // src/std/properties/enum.ts
5607
- import { Type as Type2 } from "typebox";
6865
+ import { Type as Type3 } from "typebox";
5608
6866
  function enumOf2(values, {
5609
6867
  name = "enum",
5610
6868
  optional,
@@ -5612,7 +6870,7 @@ function enumOf2(values, {
5612
6870
  } = {}) {
5613
6871
  const Enum = propertyOf2({
5614
6872
  name,
5615
- schema: Type2.Enum(values),
6873
+ schema: Type3.Enum(values),
5616
6874
  optional,
5617
6875
  default: _default
5618
6876
  });
@@ -5630,9 +6888,9 @@ function enumOf2(values, {
5630
6888
  }
5631
6889
 
5632
6890
  // src/std/properties/headless.ts
5633
- import { Type as Type3 } from "typebox";
6891
+ import { Type as Type4 } from "typebox";
5634
6892
  import { Meta } from "typebox/schema";
5635
- var JSON_SCHEMA = Type3.Unsafe({
6893
+ var JSON_SCHEMA = Type4.Unsafe({
5636
6894
  type: "object",
5637
6895
  allOf: [Meta["https://json-schema.org/draft/2020-12/schema"]]
5638
6896
  });
@@ -5640,7 +6898,7 @@ var JSON_SCHEMA = Type3.Unsafe({
5640
6898
  class SchemaProperty2 extends propertyOf2({
5641
6899
  name: "schema",
5642
6900
  schema: JSON_SCHEMA,
5643
- default: Type3.String()
6901
+ default: Type4.String()
5644
6902
  }) {
5645
6903
  compiled;
5646
6904
  static deserialize(json) {
@@ -5675,7 +6933,7 @@ class SchemaProperty2 extends propertyOf2({
5675
6933
 
5676
6934
  // src/std/properties/identity.ts
5677
6935
  import slugify from "slugify";
5678
- import { Type as Type4 } from "typebox";
6936
+ import { Type as Type5 } from "typebox";
5679
6937
 
5680
6938
  // src/std/runtime.ts
5681
6939
  var PasswordHasherRequired2 = exceptionOf2({
@@ -5871,7 +7129,7 @@ var UUID_V7 = "^[0-9a-f]{8}-[0-9a-f]{4}-7[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{
5871
7129
 
5872
7130
  class Id2 extends propertyOf2({
5873
7131
  name: "id",
5874
- schema: Type4.String({ pattern: UUID_V7 }),
7132
+ schema: Type5.String({ pattern: UUID_V7 }),
5875
7133
  default: "01a173d2-051c-77bb-8166-a371e999573e",
5876
7134
  preHandle: (value) => value.toLowerCase()
5877
7135
  }) {
@@ -5882,7 +7140,7 @@ class Id2 extends propertyOf2({
5882
7140
 
5883
7141
  class Slug2 extends propertyOf2({
5884
7142
  name: "slug",
5885
- schema: Type4.String({
7143
+ schema: Type5.String({
5886
7144
  pattern: "^[a-z0-9]+(?:-[a-z0-9]+)*$",
5887
7145
  maxLength: 255
5888
7146
  }),
@@ -5892,8 +7150,183 @@ class Slug2 extends propertyOf2({
5892
7150
  }) {
5893
7151
  }
5894
7152
 
7153
+ // src/std/properties/json.ts
7154
+ import { Type as Type6 } from "typebox";
7155
+ import { Meta as Meta3 } from "typebox/schema";
7156
+ import Value from "typebox/value";
7157
+
7158
+ // src/std/properties/exceptions.ts
7159
+ var InvalidJsonData2 = exceptionOf2({
7160
+ layer: "property" /* Property */,
7161
+ name: "InvalidJsonData",
7162
+ message: ({ issues }, source) => `Invalid JSON data for "${source}": ${issues.map((issue) => `${issue.path.length === 0 ? "/" : `/${issue.path.join("/")}`}: ${issue.message}`).join("; ")}`
7163
+ });
7164
+ var InvalidJsonSchema2 = exceptionOf2({
7165
+ layer: "schema" /* Schema */,
7166
+ name: "InvalidJsonSchema",
7167
+ message: (_, source) => `The schema of "${source}" is not a JSON Schema.`
7168
+ });
7169
+ markExported(InvalidJsonData2, InvalidJsonSchema2);
7170
+
7171
+ // src/std/properties/json.ts
7172
+ function notJson(value, path = [], ancestors = []) {
7173
+ if (value === null)
7174
+ return;
7175
+ switch (typeof value) {
7176
+ case "string":
7177
+ case "boolean":
7178
+ return;
7179
+ case "number":
7180
+ return Number.isFinite(value) ? undefined : { path, message: `is not JSON (${value})` };
7181
+ case "object":
7182
+ break;
7183
+ default:
7184
+ return { path, message: `is not JSON (${typeof value})` };
7185
+ }
7186
+ if (ancestors.includes(value))
7187
+ return { path, message: "is circular" };
7188
+ const inner = [...ancestors, value];
7189
+ if (Array.isArray(value)) {
7190
+ for (let index = 0;index < value.length; index++) {
7191
+ const found = notJson(value[index], [...path, index], inner);
7192
+ if (found)
7193
+ return found;
7194
+ }
7195
+ return;
7196
+ }
7197
+ const prototype = Object.getPrototypeOf(value);
7198
+ if (prototype !== Object.prototype && prototype !== null) {
7199
+ return {
7200
+ path,
7201
+ message: `is not JSON (an instance of ${prototype?.constructor?.name ?? "a class"})`
7202
+ };
7203
+ }
7204
+ for (const [key, item] of Object.entries(value)) {
7205
+ const found = notJson(item, [...path, key], inner);
7206
+ if (found)
7207
+ return found;
7208
+ }
7209
+ return;
7210
+ }
7211
+ var JSON_SCHEMA2 = Type6.Unsafe({
7212
+ type: "object",
7213
+ allOf: [Meta3["https://json-schema.org/draft/2020-12/schema"]]
7214
+ });
7215
+ var checked = new WeakSet;
7216
+ function assertJsonSchema(schema, source) {
7217
+ if (typeof schema === "object" && schema !== null && checked.has(schema)) {
7218
+ return schema;
7219
+ }
7220
+ if (!Value.Check(JSON_SCHEMA2, schema)) {
7221
+ throw new InvalidJsonSchema2({ source });
7222
+ }
7223
+ checked.add(schema);
7224
+ return schema;
7225
+ }
7226
+ function pathOf(pointer) {
7227
+ if (pointer === "")
7228
+ return [];
7229
+ return pointer.slice(1).split("/").map((segment) => segment.replaceAll("~1", "/").replaceAll("~0", "~")).map((segment) => /^(?:0|[1-9][0-9]*)$/.test(segment) ? +segment : segment);
7230
+ }
7231
+ function issuesOfErrors(errors, source) {
7232
+ const issues = [];
7233
+ const add = (path, code, message) => {
7234
+ issues.push({ path, code, message, source });
7235
+ };
7236
+ for (const error of errors) {
7237
+ const path = pathOf(error.instancePath);
7238
+ const keys = (name) => Array.isArray(error.params[name]) ? error.params[name] : [];
7239
+ if (error.keyword === "required") {
7240
+ for (const key of keys("requiredProperties")) {
7241
+ add([...path, key], "required", "is required");
7242
+ }
7243
+ } else if (error.keyword === "additionalProperties") {
7244
+ for (const key of keys("additionalProperties")) {
7245
+ add([...path, key], "undeclared_key", "is not declared");
7246
+ }
7247
+ } else if (!(error.keyword === "boolean" && error.schemaPath.endsWith("/additionalProperties"))) {
7248
+ add(path, error.keyword === "type" ? "invalid_type" : "invalid_value", error.message);
7249
+ }
7250
+ }
7251
+ return issues;
7252
+ }
7253
+ function jsonSchemaIssues2(schema, value, { source = "json" } = {}) {
7254
+ const checkedSchema = assertJsonSchema(schema, source);
7255
+ const found = notJson(value);
7256
+ if (found) {
7257
+ return [
7258
+ {
7259
+ path: found.path,
7260
+ code: "invalid_value",
7261
+ message: found.message,
7262
+ source
7263
+ }
7264
+ ];
7265
+ }
7266
+ if (Value.Check(checkedSchema, value))
7267
+ return [];
7268
+ return issuesOfErrors(Value.Errors(checkedSchema, value), source);
7269
+ }
7270
+ function checkJsonSchema2(schema, value, options = {}) {
7271
+ const issues = jsonSchemaIssues2(schema, value, options);
7272
+ if (issues.length > 0) {
7273
+ throw new InvalidJsonData2({
7274
+ source: options.source ?? "json",
7275
+ details: { issues }
7276
+ });
7277
+ }
7278
+ return value;
7279
+ }
7280
+ function demoOf(schema) {
7281
+ try {
7282
+ return Value.Create(schema);
7283
+ } catch {
7284
+ return;
7285
+ }
7286
+ }
7287
+ function assertJson(value) {
7288
+ const found = notJson(value);
7289
+ if (found) {
7290
+ throw new Error(`${found.path.length === 0 ? "" : `/${found.path.join("/")}: `}${found.message}`);
7291
+ }
7292
+ return value;
7293
+ }
7294
+ var JsonProperty2 = propertyOf2({
7295
+ name: "json",
7296
+ schema: Type6.Unsafe({}),
7297
+ default: {},
7298
+ preHandle: assertJson
7299
+ });
7300
+ function jsonOf2({
7301
+ schema,
7302
+ name = "json",
7303
+ optional,
7304
+ default: _default
7305
+ }) {
7306
+ assertJsonSchema(schema, name);
7307
+ const made = _default ?? demoOf(schema);
7308
+ const Json = propertyOf2({
7309
+ name,
7310
+ schema,
7311
+ optional,
7312
+ ...made !== undefined && Value.Check(schema, made) ? { default: made } : {},
7313
+ preHandle: assertJson
7314
+ });
7315
+ recordPropertyLineage(Json, {
7316
+ kind: "factory",
7317
+ factory: "jsonOf",
7318
+ args: Object.freeze({
7319
+ schema,
7320
+ ...name === "json" ? {} : { name },
7321
+ ...optional ? { optional: true } : {},
7322
+ ..._default === undefined ? {} : { default: _default }
7323
+ })
7324
+ });
7325
+ return Json;
7326
+ }
7327
+
5895
7328
  // src/std/properties/password.ts
5896
- import { Type as Type5 } from "typebox";
7329
+ import { Type as Type7 } from "typebox";
5897
7330
  var PASSWORD_MIN_LENGTH2 = 7;
5898
7331
  var PASSWORD_MAX_LENGTH2 = 128;
5899
7332
  var PASSWORD_SPECIAL_CHARACTERS2 = "!@#$%^&*()-_=+[]{}|\\;:'\",.<>/?`~";
@@ -5925,7 +7358,7 @@ var escaped = PASSWORD_SPECIAL_CHARACTERS2.replace(/[\\\]^-]/g, "\\$&");
5925
7358
 
5926
7359
  class PlainPassword2 extends propertyOf2({
5927
7360
  name: "plainPassword",
5928
- schema: Type5.String({
7361
+ schema: Type7.String({
5929
7362
  minLength: PASSWORD_MIN_LENGTH2,
5930
7363
  maxLength: PASSWORD_MAX_LENGTH2,
5931
7364
  pattern: `^(?=.*[a-z])(?=.*[A-Z])(?=.*\\d)(?=.*[${escaped}])`
@@ -5937,7 +7370,7 @@ class PlainPassword2 extends propertyOf2({
5937
7370
 
5938
7371
  class Password2 extends propertyOf2({
5939
7372
  name: "password",
5940
- schema: Type5.String({ pattern: ARGON2_HASH }),
7373
+ schema: Type7.String({ pattern: ARGON2_HASH }),
5941
7374
  default: DEMO_HASH,
5942
7375
  sensitive: true
5943
7376
  }) {
@@ -5970,36 +7403,36 @@ class Password2 extends propertyOf2({
5970
7403
  }
5971
7404
 
5972
7405
  // src/std/properties/primitives.ts
5973
- import { Type as Type6 } from "typebox";
7406
+ import { Type as Type8 } from "typebox";
5974
7407
  var StringProperty2 = propertyOf2({
5975
7408
  name: "string",
5976
- schema: Type6.String(),
7409
+ schema: Type8.String(),
5977
7410
  default: "demo"
5978
7411
  });
5979
7412
  var Text2 = propertyOf2({
5980
7413
  name: "text",
5981
- schema: Type6.String({ minLength: 1 }),
7414
+ schema: Type8.String({ minLength: 1 }),
5982
7415
  default: "Demo",
5983
7416
  preHandle: (value) => value.trim()
5984
7417
  });
5985
7418
  var NumberProperty2 = propertyOf2({
5986
7419
  name: "number",
5987
- schema: Type6.Number(),
7420
+ schema: Type8.Number(),
5988
7421
  default: 0
5989
7422
  });
5990
7423
  var Integer2 = propertyOf2({
5991
7424
  name: "integer",
5992
- schema: Type6.Integer(),
7425
+ schema: Type8.Integer(),
5993
7426
  default: 0
5994
7427
  });
5995
7428
  var BooleanProperty2 = propertyOf2({
5996
7429
  name: "boolean",
5997
- schema: Type6.Boolean(),
7430
+ schema: Type8.Boolean(),
5998
7431
  default: false
5999
7432
  });
6000
7433
 
6001
7434
  // src/std/properties/time.ts
6002
- import { Type as Type7 } from "typebox";
7435
+ import { Type as Type9 } from "typebox";
6003
7436
  var DEMO_DATE_TIME = "2026-01-01T00:00:00.000Z";
6004
7437
  function dateTimeOf({
6005
7438
  name,
@@ -6008,7 +7441,7 @@ function dateTimeOf({
6008
7441
  }) {
6009
7442
  return class extends propertyOf2({
6010
7443
  name,
6011
- schema: Type7.String({ format: "date-time" }),
7444
+ schema: Type9.String({ format: "date-time" }),
6012
7445
  optional,
6013
7446
  default: _default
6014
7447
  }) {
@@ -6025,7 +7458,7 @@ class DateTime2 extends dateTimeOf({
6025
7458
  }
6026
7459
  var DateProperty2 = propertyOf2({
6027
7460
  name: "date",
6028
- schema: Type7.String({ format: "date" }),
7461
+ schema: Type9.String({ format: "date" }),
6029
7462
  default: "2026-01-01"
6030
7463
  });
6031
7464
 
@@ -6048,6 +7481,12 @@ class DeletedAt2 extends dateTimeOf({
6048
7481
  }) {
6049
7482
  }
6050
7483
 
7484
+ class ExpiresAt2 extends dateTimeOf({
7485
+ name: "expiresAt",
7486
+ default: DEMO_DATE_TIME
7487
+ }) {
7488
+ }
7489
+
6051
7490
  // src/std/blueprint-plugin.ts
6052
7491
  var Std2 = blueprintPluginOf2("std", {
6053
7492
  id: Id2,
@@ -6067,14 +7506,15 @@ var Std2 = blueprintPluginOf2("std", {
6067
7506
  createdAt: CreatedAt2,
6068
7507
  updatedAt: UpdatedAt2,
6069
7508
  deletedAt: DeletedAt2,
6070
- schema: SchemaProperty2
6071
- }, { enumOf: enumOf2 });
7509
+ schema: SchemaProperty2,
7510
+ json: JsonProperty2
7511
+ }, { enumOf: enumOf2, jsonOf: jsonOf2 });
6072
7512
 
6073
7513
  // src/std/blueprint.ts
6074
7514
  var blueprintOf2 = blueprintBuilderOf2(Std2);
6075
7515
  // src/std/use-cases/shared.ts
6076
7516
  var fields = blueprintBuilderOf2();
6077
- var IDENTIFIER3 = /^[A-Za-z_][A-Za-z0-9_]*$/;
7517
+ var IDENTIFIER4 = /^[A-Za-z_][A-Za-z0-9_]*$/;
6078
7518
  var capitalize2 = (key) => `${key.charAt(0).toUpperCase()}${key.slice(1)}`;
6079
7519
  function literal(value) {
6080
7520
  if (typeof value === "string") {
@@ -6096,6 +7536,7 @@ function checkOptions(target, options, allowed) {
6096
7536
  }
6097
7537
  }
6098
7538
  var COMMON = ["name", "description", "tags", "deprecated", "errors"];
7539
+ var HOOKS = ["before", "after", "dependencies", "throws"];
6099
7540
  function metaOf(target, options, verb) {
6100
7541
  const { name, description, tags, deprecated } = options;
6101
7542
  if (name !== undefined && (typeof name !== "string" || name === "")) {
@@ -6110,7 +7551,7 @@ function metaOf(target, options, verb) {
6110
7551
  }
6111
7552
  function asOf(target, options) {
6112
7553
  const { as = "item" } = options;
6113
- if (typeof as !== "string" || !IDENTIFIER3.test(as)) {
7554
+ if (typeof as !== "string" || !IDENTIFIER4.test(as)) {
6114
7555
  target.unavailable("the option as must be an identifier");
6115
7556
  }
6116
7557
  return as;
@@ -6157,7 +7598,7 @@ function ownerOf(target, options) {
6157
7598
  const { owner } = options;
6158
7599
  if (owner === undefined)
6159
7600
  return;
6160
- if (!isPlain2(owner) || typeof owner.context !== "string" || !IDENTIFIER3.test(owner.context) || owner.key !== undefined && typeof owner.key !== "string") {
7601
+ if (!isPlain2(owner) || typeof owner.context !== "string" || !IDENTIFIER4.test(owner.context) || owner.key !== undefined && typeof owner.key !== "string") {
6161
7602
  return target.unavailable("the option owner must be { key?, context }, context naming a context key");
6162
7603
  }
6163
7604
  const plan = target.field(owner.key ?? "ownerId");
@@ -6199,32 +7640,103 @@ function loadSteps(owner, notFound) {
6199
7640
  ] : []
6200
7641
  ];
6201
7642
  }
7643
+ function hooksOf(target, options) {
7644
+ const { before, after, dependencies = {}, throws = [] } = options;
7645
+ for (const [key, hook] of [
7646
+ ["before", before],
7647
+ ["after", after]
7648
+ ]) {
7649
+ if (hook !== undefined && typeof hook !== "function") {
7650
+ target.unavailable(`the option ${key} must be a function`);
7651
+ }
7652
+ }
7653
+ if (!isPlain2(dependencies) || Object.keys(dependencies).some((key) => !IDENTIFIER4.test(key))) {
7654
+ target.unavailable("the option dependencies must be an object of repositories, use cases or environments by name");
7655
+ }
7656
+ if (!Array.isArray(throws) || throws.some((error) => !isDeclaredException(error))) {
7657
+ target.unavailable("the option throws must be a list of classes made by exceptionOf");
7658
+ }
7659
+ const input = before === undefined ? "$input" : "$prepared";
7660
+ return {
7661
+ input,
7662
+ dependencies,
7663
+ throws,
7664
+ before: (entity) => before === undefined ? [] : [
7665
+ {
7666
+ run: "before",
7667
+ fn: async (given, ...rest) => {
7668
+ const { context, dependencies } = rest.at(-1);
7669
+ const replaced = await before({
7670
+ input: given,
7671
+ ...entity === undefined ? {} : { entity: rest[0] },
7672
+ dependencies,
7673
+ context
7674
+ });
7675
+ return replaced === undefined ? given : replaced;
7676
+ },
7677
+ args: entity === undefined ? ["$input"] : ["$input", entity],
7678
+ as: "prepared"
7679
+ }
7680
+ ],
7681
+ after: (entity) => after === undefined ? [] : [
7682
+ {
7683
+ run: "after",
7684
+ fn: async (written, given, { context, dependencies }) => {
7685
+ await after({
7686
+ entity: written,
7687
+ input: given,
7688
+ dependencies,
7689
+ context
7690
+ });
7691
+ },
7692
+ args: [entity, input]
7693
+ }
7694
+ ]
7695
+ };
7696
+ }
7697
+ function joinDependencies(target, made, hooks) {
7698
+ for (const key of Object.keys(hooks.dependencies)) {
7699
+ if (Object.hasOwn(made, key)) {
7700
+ target.unavailable(`dependencies.${key} is a name the use case gives its own dependency`);
7701
+ }
7702
+ }
7703
+ return { ...made, ...hooks.dependencies };
7704
+ }
6202
7705
  var idOf2 = (target) => target.field("id").field;
6203
7706
  var distinct = (...errors) => [...new Set(errors)];
6204
- function entityUseCase(target, options, verb, allowed, steps) {
6205
- checkOptions(target, options, ["as", "owner", ...allowed]);
7707
+ function entityUseCase(target, options, verb, allowed, steps, hooks = false) {
7708
+ checkOptions(target, options, [
7709
+ "as",
7710
+ "owner",
7711
+ ...allowed,
7712
+ ...hooks ? HOOKS : []
7713
+ ]);
6206
7714
  const owner = ownerOf(target, options);
6207
7715
  const notFound = errorOf(target, options, "notFound", UseCaseEntityNotFound2);
6208
- const body = steps({ owner });
7716
+ const hooked = hooksOf(target, options);
7717
+ const body = steps({ owner, input: hooked.input });
6209
7718
  return {
6210
7719
  ...metaOf(target, options, verb),
6211
7720
  blueprint: fields(() => ({ id: idOf2(target) })),
6212
7721
  response: fields(() => ({ [asOf(target, options)]: target.entity })),
6213
7722
  ...contextOf2(owner),
6214
- dependencies: { repository: target.repository },
6215
- errors: [notFound],
7723
+ dependencies: joinDependencies(target, { repository: target.repository }, hooked),
7724
+ errors: distinct(notFound, ...hooked.throws),
6216
7725
  steps: [
6217
7726
  ...loadSteps(owner, notFound),
7727
+ ...hooked.before("$item"),
6218
7728
  ...body.steps,
7729
+ ...hooked.after(body.result),
6219
7730
  { return: { [asOf(target, options)]: body.result } }
6220
7731
  ]
6221
7732
  };
6222
7733
  }
6223
7734
 
6224
7735
  // src/std/use-cases/crud.ts
6225
- function createCompilation(target, options, verb, before = [], errors = []) {
6226
- checkOptions(target, options, ["as", "owner", "omit", "values"]);
7736
+ function createCompilation(target, options, verb, checks = () => [], errors = []) {
7737
+ checkOptions(target, options, ["as", "owner", "omit", "values", ...HOOKS]);
6227
7738
  const owner = ownerOf(target, options);
7739
+ const hooks = hooksOf(target, options);
6228
7740
  const omitted = keysOf(target, options, "omit");
6229
7741
  const constants = valuesOf(target, options, "values");
6230
7742
  const notFound = errorOf(target, options, "notFound", UseCaseEntityNotFound2);
@@ -6235,7 +7747,7 @@ function createCompilation(target, options, verb, before = [], errors = []) {
6235
7747
  repository: target.repository
6236
7748
  };
6237
7749
  const steps = [];
6238
- const used = new Set(errors);
7750
+ const used = new Set([...errors, ...hooks.throws]);
6239
7751
  const take = (key, field) => {
6240
7752
  if (Object.hasOwn(input, key)) {
6241
7753
  target.unavailable(`two keys of the entity take the input "${key}"`);
@@ -6273,7 +7785,7 @@ function createCompilation(target, options, verb, before = [], errors = []) {
6273
7785
  continue;
6274
7786
  }
6275
7787
  take(plan.input.key, plan.input.field);
6276
- const given = `$input.${plan.input.key}`;
7788
+ const given = `${hooks.input}.${plan.input.key}`;
6277
7789
  if (plan.kind === "entity") {
6278
7790
  values[key] = load(key, given, notFound);
6279
7791
  } else if (plan.convert) {
@@ -6293,13 +7805,15 @@ function createCompilation(target, options, verb, before = [], errors = []) {
6293
7805
  blueprint: fields(() => input),
6294
7806
  response: fields(() => ({ [asOf(target, options)]: target.entity })),
6295
7807
  ...contextOf2(owner),
6296
- dependencies,
7808
+ dependencies: joinDependencies(target, dependencies, hooks),
6297
7809
  errors: distinct(...used),
6298
7810
  steps: [
6299
- ...before,
7811
+ ...hooks.before(),
7812
+ ...checks(hooks.input),
6300
7813
  ...steps,
6301
7814
  { build: target.entity, values, as: "built" },
6302
7815
  { create: "repository", value: "$built", as: "item" },
7816
+ ...hooks.after("$item"),
6303
7817
  { return: { [asOf(target, options)]: "$item" } }
6304
7818
  ]
6305
7819
  };
@@ -6377,8 +7891,9 @@ var update2 = useCasePluginOf2("update", {
6377
7891
  description: "Changes the change keys the input gives, leaving the others as they are, and stores the entity.",
6378
7892
  requires: { actions: ["findById", "update"] },
6379
7893
  compile: (target, options) => {
6380
- checkOptions(target, options, ["as", "owner"]);
7894
+ checkOptions(target, options, ["as", "owner", ...HOOKS]);
6381
7895
  const owner = ownerOf(target, options);
7896
+ const hooks = hooksOf(target, options);
6382
7897
  const notFound = errorOf(target, options, "notFound", UseCaseEntityNotFound2);
6383
7898
  const plans = target.changeKeys.map((key) => target.field(key)).filter((plan) => plan.kind !== "entity" && plan.kind !== "entity-list");
6384
7899
  if (plans.length === 0) {
@@ -6389,7 +7904,7 @@ var update2 = useCasePluginOf2("update", {
6389
7904
  }
6390
7905
  const steps = [];
6391
7906
  for (const plan of plans) {
6392
- const given = `$input.${plan.input.key}`;
7907
+ const given = `${hooks.input}.${plan.input.key}`;
6393
7908
  let value = given;
6394
7909
  if (plan.convert) {
6395
7910
  steps.push({
@@ -6410,12 +7925,14 @@ var update2 = useCasePluginOf2("update", {
6410
7925
  })),
6411
7926
  response: fields(() => ({ [asOf(target, options)]: target.entity })),
6412
7927
  ...contextOf2(owner),
6413
- dependencies: { repository: target.repository },
6414
- errors: [notFound],
7928
+ dependencies: joinDependencies(target, { repository: target.repository }, hooks),
7929
+ errors: distinct(notFound, ...hooks.throws),
6415
7930
  steps: [
6416
7931
  ...loadSteps(owner, notFound),
7932
+ ...hooks.before("$item"),
6417
7933
  ...steps,
6418
7934
  { update: "repository", value: "$item", as: "saved" },
7935
+ ...hooks.after("$saved"),
6419
7936
  { return: { [asOf(target, options)]: "$saved" } }
6420
7937
  ]
6421
7938
  };
@@ -6427,20 +7944,24 @@ var remove2 = useCasePluginOf2("delete", {
6427
7944
  compile: (target, options) => entityUseCase(target, options, "Delete", [], () => ({
6428
7945
  steps: [{ delete: "repository", value: "$item", as: "deleted" }],
6429
7946
  result: "$deleted"
6430
- }))
7947
+ }), true)
6431
7948
  });
6432
7949
  var restore2 = useCasePluginOf2("restore", {
6433
7950
  description: "Brings back the soft-deleted entity of an id; for a repository with soft deletion and the restore action.",
6434
7951
  requires: { actions: ["restore"] },
6435
7952
  compile: (target, options) => {
6436
- checkOptions(target, options, ["as"]);
7953
+ checkOptions(target, options, ["as", ...HOOKS]);
7954
+ const hooks = hooksOf(target, options);
6437
7955
  return {
6438
7956
  ...metaOf(target, options, "Restore"),
6439
7957
  blueprint: fields(() => ({ id: idOf2(target) })),
6440
7958
  response: fields(() => ({ [asOf(target, options)]: target.entity })),
6441
- dependencies: { repository: target.repository },
7959
+ dependencies: joinDependencies(target, { repository: target.repository }, hooks),
7960
+ errors: distinct(...hooks.throws),
6442
7961
  steps: [
6443
- { restore: "repository", value: "$input.id", as: "item" },
7962
+ ...hooks.before(),
7963
+ { restore: "repository", value: `${hooks.input}.id`, as: "item" },
7964
+ ...hooks.after("$item"),
6444
7965
  { return: { [asOf(target, options)]: "$item" } }
6445
7966
  ]
6446
7967
  };
@@ -6456,10 +7977,10 @@ var register = useCasePluginOf2("register", {
6456
7977
  ...authenticatable(["findByEmail", "create"]),
6457
7978
  compile: (target, options) => {
6458
7979
  const conflict = errorOf(target, options, "conflict", UseCaseEntityConflict2);
6459
- return createCompilation(target, options, "Register", [
7980
+ return createCompilation(target, options, "Register", (input) => [
6460
7981
  {
6461
7982
  find: "repository.findByEmail",
6462
- args: ["$input.email"],
7983
+ args: [`${input}.email`],
6463
7984
  as: "existing"
6464
7985
  },
6465
7986
  { ensure: "absent", value: "$existing", error: conflict }
@@ -6513,7 +8034,7 @@ function invoking(target, options, verb, method) {
6513
8034
  { update: "repository", value: "$item", as: "saved" }
6514
8035
  ],
6515
8036
  result: "$saved"
6516
- }));
8037
+ }), true);
6517
8038
  }
6518
8039
  var lifecycle = (plugin) => ({
6519
8040
  requires: { actions: ["findById", "update"], plugins: [plugin] }
@@ -6539,8 +8060,9 @@ var unarchive = useCasePluginOf2("unarchive", {
6539
8060
  compile: (target, options) => invoking(target, options, "Unarchive", "restore")
6540
8061
  });
6541
8062
  function tagging(target, options, verb, method) {
6542
- checkOptions(target, options, ["as", "owner"]);
8063
+ checkOptions(target, options, ["as", "owner", ...HOOKS]);
6543
8064
  const owner = ownerOf(target, options);
8065
+ const hooks = hooksOf(target, options);
6544
8066
  const notFound = errorOf(target, options, "notFound", UseCaseEntityNotFound2);
6545
8067
  const { item } = target.field("tags").field;
6546
8068
  return {
@@ -6551,12 +8073,14 @@ function tagging(target, options, verb, method) {
6551
8073
  })),
6552
8074
  response: fields(() => ({ [asOf(target, options)]: target.entity })),
6553
8075
  ...contextOf2(owner),
6554
- dependencies: { repository: target.repository },
6555
- errors: [notFound],
8076
+ dependencies: joinDependencies(target, { repository: target.repository }, hooks),
8077
+ errors: distinct(notFound, ...hooks.throws),
6556
8078
  steps: [
6557
8079
  ...loadSteps(owner, notFound),
6558
- { invoke: "$item", method, args: "$input.tags" },
8080
+ ...hooks.before("$item"),
8081
+ { invoke: "$item", method, args: `${hooks.input}.tags` },
6559
8082
  { update: "repository", value: "$item", as: "saved" },
8083
+ ...hooks.after("$saved"),
6560
8084
  { return: { [asOf(target, options)]: "$saved" } }
6561
8085
  ]
6562
8086
  };
@@ -6658,10 +8182,13 @@ var entityOf2 = entityBuilderOf2({
6658
8182
  identity: Identity,
6659
8183
  generateId,
6660
8184
  now: () => new Date().toISOString(),
8185
+ expiresAt: ExpiresAt2,
6661
8186
  plugins: [StdRecordPlugins2]
6662
8187
  });
8188
+ // src/std/event-policies.ts
8189
+ var StdEventPolicies2 = eventPolicyCatalogOf2(publishEvents2);
6663
8190
  // src/std/record.ts
6664
8191
  var recordOf2 = recordBuilderOf2(StdRecordPlugins2);
6665
8192
  // src/std/stages.ts
6666
8193
  var StdStages2 = stageCatalogOf2(select2, openapi2);
6667
- export { InvalidEnvironment2, environmentOf2, isEnvironment2, guard2, InvalidApplicationDeclaration2, DuplicateApplicationDeclaration2, ApplicationConflict2, UnregisteredApplicationReference2, UnregisteredRepositoryDependency2, UnregisteredUseCaseDependency2, ApplicationNotStarted2, ProviderNotBound2, ApplicationAlreadyStarted2, InvalidApplicationEnvironment2, useApplication2, resetApplication2, InvalidLogDeclaration2, LogLevels2, logProviderOf2, consoleLogProvider2, jsonLogProvider2, memoryLogProvider2, Logger2, ReservedEntityKey2, ImmutableEntityKey2, DeletedEntity2, entityBuilderOf2, isEntityClass2, isEntity2, eventOf2, ENTITY_IDENTITY_KEYS2, RESERVED_ENTITY_KEYS2, EntityUniqueKeys2, AbstractEntity2, InvalidRepositoryQuery2, InvalidRepositoryCursor2, InvalidRepositoryDeclaration2, InvalidRepositoryEntity2, InvalidRepositoryData2, RepositoryUniqueViolation2, RepositoryEntityNotFound2, providerOf2, providerDescriptionOf2, inMemoryProvider2, AbstractRepository2, repositoryOf2, RepositoryOf2, isRepository2, InvalidUseCaseStep2, InvalidUseCasePlugin2, UnknownUseCasePlugin2, IncompatibleUseCasePlugin2, UseCaseUnavailable2, UseCaseEntityNotFound2, UseCaseEntityConflict2, UseCaseInvalidCredentials2, PropertyInput2, propertyInputOf2, useCaseTargetOf2, InvalidUseCaseDeclaration2, UseCaseAlreadyExecuted2, InvalidUseCaseDependencies2, AbstractUseCase2, useCaseOf2, isUseCaseClass2, isUseCase2, isUseCasePlugin2, useCaseOriginOf2, useCasePluginOf2, isUseCaseCatalog2, useCaseCatalogOf2, InvalidCommandDeclaration2, UnknownCommandOperation2, InvalidCommandContext2, InvalidCommandEnvironment2, CommandEventFailed2, commandBuilderOf2, isCommand2, InvalidOpenAPIModel2, InvalidOutputStage2, DuplicateOperation2, toOpenAPI2, transformOf2, sinkOf2, stageDescriptionOf2, stageKindOf2, stageCatalogOf2, operationOf2, outputOf2, isOutput2, providerFrom2, logFrom2, isLogFactory2, isProviderFactory2, providersOf2, isProviderSlot2, isProviderStorage2, isApplication2, applicationOf2, blueprintPluginOf2, statusOf2, PROBLEM_MEDIA_TYPE2, problemOf2, outputValueOf2, routeOptionsIssue2, operationRoutes2, openapi2, select2, recordPluginCatalogOf2, Email2, Phone2, Url2, Ip2, enumOf2, SchemaProperty2, PasswordHasherRequired2, SyncVerifyUnsupported2, configure2, resetRuntime2, Id2, Slug2, PASSWORD_MIN_LENGTH2, PASSWORD_MAX_LENGTH2, PASSWORD_SPECIAL_CHARACTERS2, isPasswordHash2, passwordIssues2, PlainPassword2, Password2, StringProperty2, Text2, NumberProperty2, Integer2, BooleanProperty2, DateTime2, DateProperty2, CreatedAt2, UpdatedAt2, DeletedAt2, Std2, blueprintOf2, StdUseCases2, commandOf2, Publishable2, Archivable2, Taggable2, Ownable2, Authenticatable2, StdRecordPlugins2, entityOf2, recordOf2, StdStages2 };
8194
+ export { InvalidEnvironment2, environmentOf2, isEnvironment2, InvalidApplicationDeclaration2, DuplicateApplicationDeclaration2, ApplicationConflict2, UnregisteredApplicationReference2, UnregisteredRepositoryDependency2, UnregisteredUseCaseDependency2, ApplicationNotStarted2, ProviderNotBound2, ApplicationAlreadyStarted2, InvalidApplicationEnvironment2, useApplication2, resetApplication2, InvalidEventPolicy2, PublisherNotBound2, guard2, InvalidLogDeclaration2, LogLevels2, logProviderOf2, consoleLogProvider2, jsonLogProvider2, memoryLogProvider2, Logger2, ReservedEntityKey2, ImmutableEntityKey2, DeletedEntity2, InvalidEntityExpiry2, EntityVersion2, entityBuilderOf2, isEntityClass2, isEntity2, eventOf2, ENTITY_IDENTITY_KEYS2, RESERVED_ENTITY_KEYS2, EntityUniqueKeys2, EntitySettings2, AbstractEntity2, InvalidRepositoryQuery2, InvalidRepositoryCursor2, InvalidRepositoryDeclaration2, InvalidRepositoryEntity2, InvalidRepositoryData2, RepositoryUniqueViolation2, RepositoryUsedOutsideApplication2, RepositoryEntityNotFound2, RepositoryConcurrencyConflict2, InvalidRepositoryIncrement2, providerOf2, providerDescriptionOf2, inMemoryProvider2, AbstractRepository2, repositoryOf2, RepositoryOf2, isRepository2, InvalidUseCaseStep2, InvalidUseCasePlugin2, UnknownUseCasePlugin2, IncompatibleUseCasePlugin2, UseCaseUnavailable2, UseCaseEntityNotFound2, UseCaseEntityConflict2, UseCaseInvalidCredentials2, PropertyInput2, propertyInputOf2, useCaseTargetOf2, InvalidUseCaseDeclaration2, UseCaseAlreadyExecuted2, InvalidUseCaseDependencies2, AbstractUseCase2, useCaseOf2, isUseCaseClass2, isUseCase2, isUseCasePlugin2, useCaseOriginOf2, useCasePluginOf2, isUseCaseCatalog2, useCaseCatalogOf2, InvalidCommandDeclaration2, UnknownCommandOperation2, InvalidCommandContext2, InvalidCommandEnvironment2, CommandEventFailed2, commandBuilderOf2, isCommand2, eventPolicyOf2, eventPolicyDescriptionOf2, eventPolicyCatalogOf2, InvalidOpenAPIModel2, InvalidOutputStage2, DuplicateOperation2, toOpenAPI2, transformOf2, sinkOf2, stageDescriptionOf2, stageKindOf2, stageCatalogOf2, operationOf2, outputOf2, isOutput2, providerFrom2, logFrom2, isLogFactory2, isProviderFactory2, providersOf2, isProviderSlot2, isProviderStorage2, isApplication2, applicationOf2, blueprintPluginOf2, publishEvents2, coerceTextValues2, statusOf2, PROBLEM_MEDIA_TYPE2, problemOf2, outputValueOf2, ROUTE_OPTION_KEYS2, routeOptionsIssue2, requestIdHeaderOf2, operationRoutes2, openapi2, select2, stripUndeclared2, recordPluginCatalogOf2, Email2, Phone2, Url2, Ip2, enumOf2, SchemaProperty2, PasswordHasherRequired2, SyncVerifyUnsupported2, configure2, resetRuntime2, Id2, Slug2, InvalidJsonData2, InvalidJsonSchema2, jsonSchemaIssues2, checkJsonSchema2, JsonProperty2, jsonOf2, PASSWORD_MIN_LENGTH2, PASSWORD_MAX_LENGTH2, PASSWORD_SPECIAL_CHARACTERS2, isPasswordHash2, passwordIssues2, PlainPassword2, Password2, StringProperty2, Text2, NumberProperty2, Integer2, BooleanProperty2, DateTime2, DateProperty2, CreatedAt2, UpdatedAt2, DeletedAt2, ExpiresAt2, Std2, blueprintOf2, StdUseCases2, commandOf2, Publishable2, Archivable2, Taggable2, Ownable2, Authenticatable2, StdRecordPlugins2, entityOf2, StdEventPolicies2, recordOf2, StdStages2 };