@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.
- package/README.md +34 -6
- package/dist/core/application/start.d.ts +7 -0
- package/dist/core/application/types/application.interface.d.ts +36 -2
- package/dist/core/command/types/command.interface.d.ts +7 -0
- package/dist/core/entity/abstract-entity.d.ts +28 -1
- package/dist/core/entity/entity.d.ts +1 -1
- package/dist/core/entity/event.d.ts +13 -2
- package/dist/core/entity/exceptions.d.ts +8 -0
- package/dist/core/entity/types/entity.interface.d.ts +66 -2
- package/dist/core/entity/version.d.ts +13 -0
- package/dist/core/event-policy/event-policy.d.ts +25 -0
- package/dist/core/event-policy/exceptions.d.ts +11 -0
- package/dist/core/event-policy/publish.d.ts +17 -0
- package/dist/core/event-policy/publishers.d.ts +18 -0
- package/dist/core/event-policy/types/event-policy.interface.d.ts +72 -0
- package/dist/core/output/coerce.d.ts +9 -0
- package/dist/core/output/openapi-sink.d.ts +138 -38
- package/dist/core/output/status.d.ts +1 -1
- package/dist/core/parse/parse.d.ts +10 -0
- package/dist/core/parse/strip.d.ts +13 -0
- package/dist/core/repository/exceptions.d.ts +30 -0
- package/dist/core/repository/in-memory.d.ts +14 -1
- package/dist/core/repository/plan.d.ts +20 -0
- package/dist/core/repository/provider.d.ts +1 -1
- package/dist/core/repository/query.d.ts +9 -2
- package/dist/core/repository/repository.d.ts +40 -8
- package/dist/core/repository/state.d.ts +30 -2
- package/dist/core/repository/types/query.interface.d.ts +106 -5
- package/dist/core/repository/types/repository.interface.d.ts +106 -7
- package/dist/experimental.js +4 -4
- package/dist/{index-r78dsvtc.js → index-qxfkyd6k.js} +612 -612
- package/dist/{index-q6hggpzw.js → index-t5qwtyj5.js} +1807 -280
- package/dist/index.d.ts +22 -11
- package/dist/index.js +62 -14
- package/dist/std/blueprint-plugin.d.ts +6 -2
- package/dist/std/blueprint.d.ts +3 -0
- package/dist/std/entity.d.ts +2 -1
- package/dist/std/event-policies.d.ts +2 -0
- package/dist/std/properties/exceptions.d.ts +11 -0
- package/dist/std/properties/json.d.ts +54 -0
- package/dist/std/properties/time.d.ts +4 -0
- package/dist/std/use-cases/crud.d.ts +6 -4
- package/dist/std/use-cases/shared.d.ts +32 -4
- package/dist/std/use-cases/types.d.ts +100 -21
- package/dist/testing/index.d.ts +2 -0
- package/dist/testing/index.js +347 -2
- package/dist/testing/memory-publisher.d.ts +16 -0
- package/dist/testing/provider-contract.d.ts +14 -0
- 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
|
-
|
|
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/
|
|
77
|
+
// src/core/event-policy/exceptions.ts
|
|
75
78
|
var exports_exceptions5 = {};
|
|
76
79
|
__export(exports_exceptions5, {
|
|
77
|
-
|
|
80
|
+
InvalidEventPolicy: () => InvalidEventPolicy2,
|
|
81
|
+
PublisherNotBound: () => PublisherNotBound2
|
|
78
82
|
});
|
|
79
83
|
|
|
80
|
-
// src/core/
|
|
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
|
|
90
|
-
__export(
|
|
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
|
|
102
|
-
__export(
|
|
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
|
|
127
|
-
__export(
|
|
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
|
|
137
|
-
__export(
|
|
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
|
|
143
|
-
__export(
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
863
|
+
const created = createdAt === undefined ? clock.now() : createdAt;
|
|
864
|
+
const identity = {
|
|
669
865
|
...value,
|
|
670
866
|
id: id === undefined ? clock.generateId() : id,
|
|
671
|
-
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 (
|
|
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 = [
|
|
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
|
|
971
|
-
|
|
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
|
|
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({
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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, {
|
|
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 = [
|
|
1491
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
return self.
|
|
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:
|
|
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([
|
|
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
|
-
|
|
3338
|
-
|
|
3339
|
-
|
|
4093
|
+
exports_exceptions3,
|
|
4094
|
+
exports_exceptions10,
|
|
4095
|
+
exports_exceptions7,
|
|
3340
4096
|
exports_environment,
|
|
3341
4097
|
exports_exceptions5,
|
|
3342
|
-
|
|
4098
|
+
exports_exceptions6,
|
|
3343
4099
|
exports_exceptions11,
|
|
3344
|
-
|
|
4100
|
+
exports_exceptions12,
|
|
4101
|
+
exports_exceptions2,
|
|
3345
4102
|
exports_exceptions,
|
|
3346
|
-
|
|
4103
|
+
exports_exceptions8,
|
|
3347
4104
|
exports_use_case,
|
|
3348
|
-
|
|
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) ?
|
|
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
|
|
4225
|
+
var factories2 = new WeakMap;
|
|
3469
4226
|
var opaque2 = Object.freeze({ kind: "opaque" });
|
|
3470
|
-
function
|
|
4227
|
+
function invalid5(source, reason) {
|
|
3471
4228
|
throw new InvalidOutputStage2({ source, details: { reason } });
|
|
3472
4229
|
}
|
|
3473
|
-
function
|
|
3474
|
-
const options = frozenJson(given ?? {}, "Stage options", (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
|
-
|
|
4233
|
+
invalid5(name, "Stage options must be an object.");
|
|
3477
4234
|
}
|
|
3478
4235
|
const reason = validate?.(options);
|
|
3479
4236
|
if (reason !== undefined)
|
|
3480
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
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
|
|
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 =
|
|
4308
|
+
const known = factories2.get(factory);
|
|
3552
4309
|
if (!known) {
|
|
3553
|
-
|
|
4310
|
+
invalid5("catalog", "A catalog takes factories made by `transformOf` or `sinkOf`.");
|
|
3554
4311
|
}
|
|
3555
4312
|
if (Object.hasOwn(byName, known.name)) {
|
|
3556
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
4341
|
+
invalid6(source, "Expected an operation descriptor.");
|
|
3585
4342
|
}
|
|
3586
4343
|
if (typeof patch !== "object" || patch === null || Array.isArray(patch)) {
|
|
3587
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
4359
|
+
invalid6(source, "Expected description to be a string.");
|
|
3603
4360
|
}
|
|
3604
4361
|
if ("deprecated" in patch && typeof deprecated !== "boolean") {
|
|
3605
|
-
|
|
4362
|
+
invalid6(source, "Expected deprecated to be a boolean.");
|
|
3606
4363
|
}
|
|
3607
4364
|
if ("invoke" in patch && typeof invoke !== "function") {
|
|
3608
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
5518
|
+
invalid8("applicationOf", "Expected a name.");
|
|
4700
5519
|
}
|
|
4701
5520
|
const extended = settings.extends ?? [];
|
|
4702
5521
|
if (!Array.isArray(extended) || !extended.every(isApplication2)) {
|
|
4703
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
5900
|
+
invalid8(name, "Expected a provider of a storage registered in the application.");
|
|
5059
5901
|
}
|
|
5060
5902
|
return instance;
|
|
5061
5903
|
}
|
|
5062
|
-
return
|
|
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
|
-
|
|
5173
|
-
|
|
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
|
|
5191
|
-
|
|
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
|
|
5198
|
-
|
|
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
|
|
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(
|
|
6367
|
+
return routeOptionsIssue2(options, "openapi");
|
|
5225
6368
|
}
|
|
5226
|
-
function
|
|
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
|
|
5237
|
-
|
|
5238
|
-
|
|
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
|
|
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
|
-
|
|
6415
|
+
invalid9(`Two operations are routed to ${method.toUpperCase()} ${path}.`, source);
|
|
5250
6416
|
}
|
|
5251
6417
|
taken.add(key);
|
|
5252
|
-
routes.set(operation.id, {
|
|
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
|
-
|
|
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
|
-
|
|
5335
|
-
description:
|
|
5336
|
-
|
|
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
|
|
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
|
-
|
|
5353
|
-
|
|
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
|
-
|
|
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:
|
|
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 (
|
|
5382
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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: {
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
5599
|
-
|
|
5600
|
-
|
|
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
|
|
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:
|
|
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
|
|
6891
|
+
import { Type as Type4 } from "typebox";
|
|
5634
6892
|
import { Meta } from "typebox/schema";
|
|
5635
|
-
var JSON_SCHEMA =
|
|
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:
|
|
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
|
|
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:
|
|
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:
|
|
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
|
|
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:
|
|
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:
|
|
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
|
|
7406
|
+
import { Type as Type8 } from "typebox";
|
|
5974
7407
|
var StringProperty2 = propertyOf2({
|
|
5975
7408
|
name: "string",
|
|
5976
|
-
schema:
|
|
7409
|
+
schema: Type8.String(),
|
|
5977
7410
|
default: "demo"
|
|
5978
7411
|
});
|
|
5979
7412
|
var Text2 = propertyOf2({
|
|
5980
7413
|
name: "text",
|
|
5981
|
-
schema:
|
|
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:
|
|
7420
|
+
schema: Type8.Number(),
|
|
5988
7421
|
default: 0
|
|
5989
7422
|
});
|
|
5990
7423
|
var Integer2 = propertyOf2({
|
|
5991
7424
|
name: "integer",
|
|
5992
|
-
schema:
|
|
7425
|
+
schema: Type8.Integer(),
|
|
5993
7426
|
default: 0
|
|
5994
7427
|
});
|
|
5995
7428
|
var BooleanProperty2 = propertyOf2({
|
|
5996
7429
|
name: "boolean",
|
|
5997
|
-
schema:
|
|
7430
|
+
schema: Type8.Boolean(),
|
|
5998
7431
|
default: false
|
|
5999
7432
|
});
|
|
6000
7433
|
|
|
6001
7434
|
// src/std/properties/time.ts
|
|
6002
|
-
import { Type as
|
|
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:
|
|
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:
|
|
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
|
-
|
|
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
|
|
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" || !
|
|
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" || !
|
|
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, [
|
|
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
|
|
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:
|
|
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,
|
|
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:
|
|
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
|
-
|
|
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: [
|
|
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:
|
|
8076
|
+
dependencies: joinDependencies(target, { repository: target.repository }, hooks),
|
|
8077
|
+
errors: distinct(notFound, ...hooks.throws),
|
|
6556
8078
|
steps: [
|
|
6557
8079
|
...loadSteps(owner, notFound),
|
|
6558
|
-
|
|
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,
|
|
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 };
|