@afora/core 0.0.9 → 0.0.11
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 +52 -30
- package/dist/adapters.js +31 -55
- package/dist/core/application/application.d.ts +30 -0
- package/dist/core/application/application.js +1291 -0
- package/dist/core/application/exceptions.js +85 -0
- package/dist/core/application/migrate.js +67 -0
- package/dist/core/application/providers.js +108 -0
- package/dist/core/application/registry.js +23 -0
- package/dist/core/application/schema.js +398 -0
- package/dist/core/application/start.js +162 -0
- package/dist/core/application/types/application.interface.d.ts +23 -3
- package/dist/core/auth/jwt.js +200 -0
- package/dist/core/auth/types/jwt.interface.d.ts +10 -2
- package/dist/core/blueprint/blueprint-plugin.js +7 -0
- package/dist/core/blueprint/blueprint.js +146 -0
- package/dist/core/blueprint/callable.js +13 -0
- package/dist/core/blueprint/exceptions.js +20 -0
- package/dist/core/blueprint/is-blueprint.js +4 -0
- package/dist/core/blueprint/list.js +19 -0
- package/dist/core/blueprint/optional.js +69 -0
- package/dist/core/blueprint/record-field.js +14 -0
- package/dist/core/blueprint/slice.js +127 -0
- package/dist/core/blueprint/types/blueprint.interface.d.ts +16 -0
- package/dist/core/blueprint/variant.js +72 -0
- package/dist/core/command/command.js +428 -0
- package/dist/core/command/exceptions.js +30 -0
- package/dist/core/command/types/command.interface.d.ts +10 -1
- package/dist/core/entity/abstract-entity.d.ts +19 -0
- package/dist/core/entity/abstract-entity.js +181 -0
- package/dist/core/entity/entity.js +209 -0
- package/dist/core/entity/event.d.ts +39 -0
- package/dist/core/entity/event.js +80 -0
- package/dist/core/entity/exceptions.js +27 -0
- package/dist/core/entity/trust.js +7 -0
- package/dist/core/entity/version.js +13 -0
- package/dist/core/environment/environment.d.ts +26 -3
- package/dist/core/environment/environment.js +71 -0
- package/dist/core/event-policy/event-policy.js +76 -0
- package/dist/core/event-policy/exceptions.js +12 -0
- package/dist/core/event-policy/publish.js +98 -0
- package/dist/core/event-policy/publishers.js +18 -0
- package/dist/core/event-policy/types/event-policy.interface.d.ts +8 -2
- package/dist/core/exception/enums/exception-layers.js +15 -0
- package/dist/core/exception/exception.d.ts +15 -0
- package/dist/core/exception/exception.js +128 -0
- package/dist/core/exception/exported.js +49 -0
- package/dist/core/exception/guard.d.ts +20 -1
- package/dist/core/exception/guard.js +16 -0
- package/dist/core/exception/types/exception.interface.d.ts +13 -0
- package/dist/core/json.js +19 -0
- package/dist/core/logger/exceptions.js +7 -0
- package/dist/core/logger/logger.d.ts +14 -1
- package/dist/core/logger/logger.js +178 -0
- package/dist/core/openapi/exceptions.js +7 -0
- package/dist/core/openapi/openapi.js +132 -0
- package/dist/core/openapi/types/openapi.interface.d.ts +7 -1
- package/dist/core/output/access.js +66 -0
- package/dist/core/output/coerce.js +38 -0
- package/dist/core/output/exceptions.js +22 -0
- package/dist/{index-y280ew0k.js → core/output/http.js} +35 -80
- package/dist/core/output/openapi-sink.d.ts +10 -2
- package/dist/{index-hg1z2s2v.js → core/output/openapi-sink.js} +60 -544
- package/dist/core/output/output.js +192 -0
- package/dist/core/output/problem.js +30 -0
- package/dist/core/output/select.js +28 -0
- package/dist/core/output/stage.js +111 -0
- package/dist/core/output/status.js +52 -0
- package/dist/core/output/types/http.interface.d.ts +5 -1
- package/dist/core/output/types/output.interface.d.ts +24 -0
- package/dist/core/parse/issue.js +31 -0
- package/dist/core/parse/parse.d.ts +8 -1
- package/dist/core/parse/parse.js +323 -0
- package/dist/core/parse/strip.js +48 -0
- package/dist/core/property/abstract-property.js +37 -0
- package/dist/core/property/define-property.js +209 -0
- package/dist/core/property/enums/property-states.js +8 -0
- package/dist/core/property/exceptions.js +12 -0
- package/dist/core/property/input.js +10 -0
- package/dist/core/property/is-property.js +3 -0
- package/dist/core/property/lineage.js +8 -0
- package/dist/core/property/property.d.ts +27 -0
- package/dist/core/property/property.js +46 -0
- package/dist/core/property/refine.js +103 -0
- package/dist/core/record/abstract-record.d.ts +2 -1
- package/dist/core/record/abstract-record.js +307 -0
- package/dist/core/record/exceptions.js +49 -0
- package/dist/core/record/marks.js +7 -0
- package/dist/core/record/mask.js +60 -0
- package/dist/core/record/name.js +23 -0
- package/dist/core/record/plugin-catalog.js +115 -0
- package/dist/core/record/plugin.js +198 -0
- package/dist/core/record/record.js +117 -0
- package/dist/core/record/types/record.interface.d.ts +5 -0
- package/dist/core/repository/cursor.js +83 -0
- package/dist/core/repository/exceptions.js +72 -0
- package/dist/core/repository/in-memory.js +196 -0
- package/dist/core/repository/migrate.js +254 -0
- package/dist/core/repository/plan.js +27 -0
- package/dist/core/repository/provider.js +29 -0
- package/dist/core/repository/query.js +153 -0
- package/dist/core/repository/references.js +196 -0
- package/dist/core/repository/repository.d.ts +22 -2
- package/dist/core/repository/repository.js +720 -0
- package/dist/core/repository/state.js +138 -0
- package/dist/core/repository/types/query.interface.d.ts +13 -0
- package/dist/core/repository/types/repository.interface.d.ts +20 -8
- package/dist/core/repository/types/schema.interface.d.ts +8 -2
- package/dist/core/repository/upcast.js +179 -0
- package/dist/core/schema/schema.d.ts +9 -0
- package/dist/core/schema/schema.js +542 -0
- package/dist/core/standard/standard-schema.js +1 -0
- package/dist/core/standard/standard.js +23 -0
- package/dist/core/symbols/demo.d.ts +4 -0
- package/dist/core/symbols/demo.js +1 -0
- package/dist/core/symbols/kind.js +1 -0
- package/dist/core/symbols/meta.js +1 -0
- package/dist/core/symbols/state.d.ts +5 -0
- package/dist/core/symbols/state.js +1 -0
- package/dist/core/tracing/exceptions.d.ts +4 -0
- package/dist/core/tracing/exceptions.js +7 -0
- package/dist/core/tracing/tracing.d.ts +72 -0
- package/dist/core/tracing/tracing.js +101 -0
- package/dist/core/use-case/access.js +56 -0
- package/dist/core/use-case/decorators.js +65 -0
- package/dist/core/use-case/exceptions.d.ts +5 -1
- package/dist/core/use-case/exceptions.js +48 -0
- package/dist/core/use-case/plugin-catalog.js +68 -0
- package/dist/core/use-case/plugin.js +133 -0
- package/dist/core/use-case/route.js +200 -0
- package/dist/core/use-case/scope.d.ts +5 -0
- package/dist/core/use-case/scope.js +4 -0
- package/dist/core/use-case/steps/describe.d.ts +5 -1
- package/dist/core/use-case/steps/describe.js +24 -0
- package/dist/core/use-case/steps/references.js +105 -0
- package/dist/core/use-case/steps/run.js +139 -0
- package/dist/core/use-case/steps/types/check.interface.d.ts +27 -8
- package/dist/core/use-case/steps/types/steps.interface.d.ts +8 -1
- package/dist/core/use-case/steps/validate.js +368 -0
- package/dist/core/use-case/target.js +100 -0
- package/dist/core/use-case/types/route.interface.d.ts +25 -5
- package/dist/core/use-case/use-case.d.ts +77 -2
- package/dist/core/use-case/use-case.js +493 -0
- package/dist/experimental.d.ts +5 -0
- package/dist/experimental.js +18 -161
- package/dist/index.d.ts +7 -5
- package/dist/index.js +213 -367
- package/dist/std/blueprint-plugin.js +42 -0
- package/dist/std/blueprint.d.ts +21 -0
- package/dist/std/blueprint.js +3 -0
- package/dist/std/command.js +3 -0
- package/dist/std/entity.d.ts +20 -1
- package/dist/std/entity.js +30 -0
- package/dist/std/event-policies.js +3 -0
- package/dist/std/properties/contact.d.ts +3 -0
- package/dist/std/properties/contact.js +29 -0
- package/dist/std/properties/enum.js +26 -0
- package/dist/std/properties/exceptions.js +14 -0
- package/dist/std/properties/headless.js +45 -0
- package/dist/std/properties/identity.js +28 -0
- package/dist/std/properties/json.js +161 -0
- package/dist/std/properties/password.js +79 -0
- package/dist/std/properties/primitives.d.ts +4 -0
- package/dist/std/properties/primitives.js +28 -0
- package/dist/std/properties/time.js +55 -0
- package/dist/std/record-plugins.js +73 -0
- package/dist/std/record.d.ts +20 -1
- package/dist/std/record.js +3 -0
- package/dist/std/resource.js +39 -0
- package/dist/std/runtime.js +193 -0
- package/dist/std/stages.js +4 -0
- package/dist/std/use-cases/auth.js +193 -0
- package/dist/std/use-cases/crud.js +261 -0
- package/dist/std/use-cases/index.js +12 -0
- package/dist/std/use-cases/lifecycle.js +97 -0
- package/dist/std/use-cases/shared.js +232 -0
- package/dist/std/use-cases/types.d.ts +5 -1
- package/dist/std/use-cases/types.js +0 -0
- package/dist/testing/http-provider-contract.js +344 -0
- package/dist/testing/index.js +3 -1435
- package/dist/testing/memory-publisher.js +12 -0
- package/dist/testing/provider-contract.d.ts +12 -3
- package/dist/testing/provider-contract.js +1071 -0
- package/package.json +10 -3
- package/dist/index-3aae5jfq.js +0 -2997
- package/dist/index-ny3as79r.js +0 -4567
- package/dist/index-xegv6a1p.js +0 -4291
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { startedApplication } from "../application/registry.js";
|
|
2
|
+
import { Exception, UNEXPECTED_ERROR } from "../exception/exception.js";
|
|
3
|
+
import { InvalidTraceDeclaration } from "./exceptions.js";
|
|
4
|
+
export function checkTracer(value) {
|
|
5
|
+
if (typeof value !== "object" || value === null || typeof value.startSpan !== "function" || value.withSpan !== undefined && typeof value.withSpan !== "function") {
|
|
6
|
+
throw new InvalidTraceDeclaration({
|
|
7
|
+
source: "trace",
|
|
8
|
+
details: {
|
|
9
|
+
reason: "Expected `trace` to be a tracer: { startSpan(name, attributes, parent), withSpan? }."
|
|
10
|
+
}
|
|
11
|
+
});
|
|
12
|
+
}
|
|
13
|
+
return value;
|
|
14
|
+
}
|
|
15
|
+
export const ApplicationTracerKey = Symbol("afora.application.tracer");
|
|
16
|
+
export function resolveTracer(scoped, owner) {
|
|
17
|
+
return scoped ?? owner?.[ApplicationTracerKey] ?? startedApplication()?.[ApplicationTracerKey];
|
|
18
|
+
}
|
|
19
|
+
export function startSpan(tracer, name, attributes, parent) {
|
|
20
|
+
try {
|
|
21
|
+
return tracer.startSpan(name, attributes, parent);
|
|
22
|
+
} catch {
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
export function runInSpan(tracer, span, run) {
|
|
27
|
+
if (tracer.withSpan === undefined)
|
|
28
|
+
return run();
|
|
29
|
+
let ran = "no";
|
|
30
|
+
let result;
|
|
31
|
+
let thrown;
|
|
32
|
+
const once = () => {
|
|
33
|
+
try {
|
|
34
|
+
result = run();
|
|
35
|
+
ran = "returned";
|
|
36
|
+
return result;
|
|
37
|
+
} catch (error) {
|
|
38
|
+
ran = "threw";
|
|
39
|
+
thrown = error;
|
|
40
|
+
throw error;
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
try {
|
|
44
|
+
return tracer.withSpan(span, once);
|
|
45
|
+
} catch {
|
|
46
|
+
if (ran === "threw")
|
|
47
|
+
throw thrown;
|
|
48
|
+
return ran === "returned" ? result : run();
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
const OK = Object.freeze({ "afora.outcome": "ok" });
|
|
52
|
+
export function endSpan(span) {
|
|
53
|
+
if (span === undefined)
|
|
54
|
+
return;
|
|
55
|
+
try {
|
|
56
|
+
span.end("ok", OK);
|
|
57
|
+
} catch {}
|
|
58
|
+
}
|
|
59
|
+
export function failSpan(span, error, layer) {
|
|
60
|
+
if (span === undefined)
|
|
61
|
+
return;
|
|
62
|
+
const origin = error instanceof Exception ? error.origin : undefined;
|
|
63
|
+
const name = origin?.name ?? UNEXPECTED_ERROR;
|
|
64
|
+
try {
|
|
65
|
+
span.end("error", {
|
|
66
|
+
"afora.outcome": "error",
|
|
67
|
+
"afora.exception.layer": origin?.layer ?? layer,
|
|
68
|
+
"afora.exception.name": name,
|
|
69
|
+
"afora.exception.unexpected": name === UNEXPECTED_ERROR
|
|
70
|
+
});
|
|
71
|
+
} catch {}
|
|
72
|
+
}
|
|
73
|
+
export function memoryTracer() {
|
|
74
|
+
const spans = [];
|
|
75
|
+
const recorded = new WeakMap;
|
|
76
|
+
return Object.freeze({
|
|
77
|
+
spans,
|
|
78
|
+
clear: () => {
|
|
79
|
+
spans.length = 0;
|
|
80
|
+
},
|
|
81
|
+
startSpan(name, attributes, parent) {
|
|
82
|
+
const span = {
|
|
83
|
+
name,
|
|
84
|
+
attributes: { ...attributes },
|
|
85
|
+
parent: parent === undefined ? undefined : recorded.get(parent),
|
|
86
|
+
status: undefined
|
|
87
|
+
};
|
|
88
|
+
spans.push(span);
|
|
89
|
+
const handle = {
|
|
90
|
+
end(status, ended) {
|
|
91
|
+
if (span.status !== undefined)
|
|
92
|
+
return;
|
|
93
|
+
span.status = status;
|
|
94
|
+
span.attributes = { ...span.attributes, ...ended };
|
|
95
|
+
}
|
|
96
|
+
};
|
|
97
|
+
recorded.set(handle, span);
|
|
98
|
+
return handle;
|
|
99
|
+
}
|
|
100
|
+
});
|
|
101
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { isScalarKey } from "./route.js";
|
|
2
|
+
function isObject(value) {
|
|
3
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
4
|
+
}
|
|
5
|
+
const ACCESS_KEYS = ["public", "scopes", "owner", "context"];
|
|
6
|
+
const PRINCIPAL_VALUE = /^(subject|scopes|claims\.[^.]+)$/;
|
|
7
|
+
export function isPublicAccess(access) {
|
|
8
|
+
return access.public === true;
|
|
9
|
+
}
|
|
10
|
+
export function accessDeclarationOf(access, blueprint) {
|
|
11
|
+
if (!isObject(access)) {
|
|
12
|
+
return "Expected access to be { public: true } or { scopes?, owner?, context? }.";
|
|
13
|
+
}
|
|
14
|
+
for (const key of Object.keys(access)) {
|
|
15
|
+
if (!ACCESS_KEYS.includes(key))
|
|
16
|
+
return `access has no option "${key}".`;
|
|
17
|
+
}
|
|
18
|
+
const { scopes, owner, context } = access;
|
|
19
|
+
if (access.public !== undefined && typeof access.public !== "boolean") {
|
|
20
|
+
return "Expected access.public to be a boolean.";
|
|
21
|
+
}
|
|
22
|
+
if (access.public === true) {
|
|
23
|
+
if (Object.keys(access).length > 1) {
|
|
24
|
+
return "access.public leaves no room for scopes, owner or context: anyone may call.";
|
|
25
|
+
}
|
|
26
|
+
return Object.freeze({ public: true });
|
|
27
|
+
}
|
|
28
|
+
if (scopes !== undefined) {
|
|
29
|
+
if (!Array.isArray(scopes) || scopes.some((scope) => typeof scope !== "string" || scope === "") || new Set(scopes).size !== scopes.length) {
|
|
30
|
+
return "Expected access.scopes to be distinct, non-empty strings.";
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
if (owner !== undefined) {
|
|
34
|
+
if (!isObject(owner) || Object.keys(owner).some((key) => key !== "input" && key !== "claim") || typeof owner.input !== "string" || owner.claim !== undefined && (typeof owner.claim !== "string" || owner.claim === "")) {
|
|
35
|
+
return "Expected access.owner to be { input, claim? }.";
|
|
36
|
+
}
|
|
37
|
+
if (!isScalarKey(blueprint, owner.input)) {
|
|
38
|
+
return `access.owner compares "${owner.input}", which is not a scalar key of the input.`;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
if (context !== undefined) {
|
|
42
|
+
if (!isObject(context)) {
|
|
43
|
+
return "Expected access.context to map context keys to values of the caller.";
|
|
44
|
+
}
|
|
45
|
+
for (const [key, value] of Object.entries(context)) {
|
|
46
|
+
if (typeof value !== "string" || !PRINCIPAL_VALUE.test(value)) {
|
|
47
|
+
return `Expected access.context["${key}"] to be "subject", "scopes" or "claims.<name>".`;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
return Object.freeze({
|
|
52
|
+
...scopes === undefined ? {} : { scopes: Object.freeze([...scopes]) },
|
|
53
|
+
...owner === undefined ? {} : { owner: Object.freeze({ ...owner }) },
|
|
54
|
+
...context === undefined ? {} : { context: Object.freeze({ ...context }) }
|
|
55
|
+
});
|
|
56
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { Kind } from "../symbols/kind.js";
|
|
2
|
+
import { accessDeclarationOf } from "./access.js";
|
|
3
|
+
import { routeDeclarationOf } from "./route.js";
|
|
4
|
+
import { InvalidUseCaseDeclaration } from "./use-case.js";
|
|
5
|
+
function sameData(a, b) {
|
|
6
|
+
if (a === b)
|
|
7
|
+
return true;
|
|
8
|
+
if (typeof a !== "object" || typeof b !== "object" || a === null || b === null || Array.isArray(a) !== Array.isArray(b)) {
|
|
9
|
+
return false;
|
|
10
|
+
}
|
|
11
|
+
const left = Object.keys(a);
|
|
12
|
+
return left.length === Object.keys(b).length && left.every((key) => Object.hasOwn(b, key) && sameData(a[key], b[key]));
|
|
13
|
+
}
|
|
14
|
+
function decoratedUseCase(value, decorator) {
|
|
15
|
+
if (typeof value !== "function" || value[Kind] !== "use-case") {
|
|
16
|
+
throw new InvalidUseCaseDeclaration({
|
|
17
|
+
source: decorator,
|
|
18
|
+
details: {
|
|
19
|
+
reason: `${decorator} decorates a class that extends useCaseOf(...).`
|
|
20
|
+
}
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
return value;
|
|
24
|
+
}
|
|
25
|
+
export function Route(method, pathOrOptions, options) {
|
|
26
|
+
const settings = typeof pathOrOptions === "string" ? { ...options, method, path: pathOrOptions } : { ...pathOrOptions, method };
|
|
27
|
+
return (value) => {
|
|
28
|
+
const UseCase = decoratedUseCase(value, "@Route");
|
|
29
|
+
const invalid = (reason) => {
|
|
30
|
+
throw new InvalidUseCaseDeclaration({
|
|
31
|
+
source: UseCase.useCaseName,
|
|
32
|
+
details: { reason }
|
|
33
|
+
});
|
|
34
|
+
};
|
|
35
|
+
const declared = routeDeclarationOf(settings, UseCase.blueprint, UseCase.response);
|
|
36
|
+
if (typeof declared === "string")
|
|
37
|
+
return invalid(declared);
|
|
38
|
+
if (UseCase.route !== undefined) {
|
|
39
|
+
if (sameData(UseCase.route, declared))
|
|
40
|
+
return;
|
|
41
|
+
invalid("The use case already has another route: declare it once, with the route setting or with @Route.");
|
|
42
|
+
}
|
|
43
|
+
Object.defineProperty(UseCase, "route", { value: declared });
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
export function Access(access) {
|
|
47
|
+
return (value) => {
|
|
48
|
+
const UseCase = decoratedUseCase(value, "@Access");
|
|
49
|
+
const invalid = (reason) => {
|
|
50
|
+
throw new InvalidUseCaseDeclaration({
|
|
51
|
+
source: UseCase.useCaseName,
|
|
52
|
+
details: { reason }
|
|
53
|
+
});
|
|
54
|
+
};
|
|
55
|
+
const declared = accessDeclarationOf(access, UseCase.blueprint);
|
|
56
|
+
if (typeof declared === "string")
|
|
57
|
+
return invalid(declared);
|
|
58
|
+
if (UseCase.access !== undefined) {
|
|
59
|
+
if (sameData(UseCase.access, declared))
|
|
60
|
+
return;
|
|
61
|
+
invalid("The use case already declares another access: declare it once, with the access setting or with @Access.");
|
|
62
|
+
}
|
|
63
|
+
Object.defineProperty(UseCase, "access", { value: declared });
|
|
64
|
+
};
|
|
65
|
+
}
|
|
@@ -1,12 +1,16 @@
|
|
|
1
|
+
import type { Issue } from "../parse/types/parse.interface.js";
|
|
1
2
|
/**
|
|
2
3
|
* A step of a use case is invalid: at declaration (`useCaseOf({ steps })`),
|
|
3
4
|
* or while the steps run (a `change` or `invoke` on something that is not a
|
|
4
|
-
* record or has no such member
|
|
5
|
+
* record or has no such member, or `args` that a `run`'s `params` reject).
|
|
6
|
+
* `details.index` is the position of the step.
|
|
5
7
|
*/
|
|
6
8
|
export declare const InvalidUseCaseStep: import("../../index.js").DeclaredExceptionClass<{
|
|
7
9
|
readonly index: number;
|
|
8
10
|
readonly reason: string;
|
|
9
11
|
readonly code?: string;
|
|
12
|
+
/** The issues of the `args` a `run` gave its `params` (`code: "invalid_args"`). */
|
|
13
|
+
readonly issues?: readonly Issue[];
|
|
10
14
|
}>;
|
|
11
15
|
/**
|
|
12
16
|
* `useCasePluginOf` or `useCaseCatalogOf` was given what cannot be a plugin
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { ExceptionLayers } from "../exception/enums/exception-layers.js";
|
|
2
|
+
import { exceptionOf } from "../exception/exception.js";
|
|
3
|
+
export const InvalidUseCaseStep = exceptionOf({
|
|
4
|
+
layer: ExceptionLayers.UseCase,
|
|
5
|
+
name: "InvalidUseCaseStep",
|
|
6
|
+
message: ({
|
|
7
|
+
index,
|
|
8
|
+
reason
|
|
9
|
+
}) => `Invalid step ${index}: ${reason}`
|
|
10
|
+
});
|
|
11
|
+
export const InvalidUseCasePlugin = exceptionOf({
|
|
12
|
+
layer: ExceptionLayers.UseCase,
|
|
13
|
+
name: "InvalidUseCasePlugin",
|
|
14
|
+
message: ({ reason }, source) => `Invalid use case plugin "${source}": ${reason}.`
|
|
15
|
+
});
|
|
16
|
+
export const UnknownUseCasePlugin = exceptionOf({
|
|
17
|
+
layer: ExceptionLayers.UseCase,
|
|
18
|
+
name: "UnknownUseCasePlugin",
|
|
19
|
+
message: ({ name }) => `No use case plugin "${name}" in the catalog.`
|
|
20
|
+
});
|
|
21
|
+
export const IncompatibleUseCasePlugin = exceptionOf({
|
|
22
|
+
layer: ExceptionLayers.UseCase,
|
|
23
|
+
name: "IncompatibleUseCasePlugin",
|
|
24
|
+
message: ({ reason }, source) => `Incompatible use case plugin "${source}": ${reason}.`
|
|
25
|
+
});
|
|
26
|
+
export const UseCaseUnavailable = exceptionOf({
|
|
27
|
+
layer: ExceptionLayers.UseCase,
|
|
28
|
+
name: "UseCaseUnavailable",
|
|
29
|
+
message: ({
|
|
30
|
+
repository,
|
|
31
|
+
reason
|
|
32
|
+
}, source) => `The use case "${source}" is not available for "${repository}": ${reason}.`
|
|
33
|
+
});
|
|
34
|
+
export const UseCaseEntityNotFound = exceptionOf({
|
|
35
|
+
layer: ExceptionLayers.UseCase,
|
|
36
|
+
name: "UseCaseEntityNotFound",
|
|
37
|
+
message: "The entity was not found."
|
|
38
|
+
});
|
|
39
|
+
export const UseCaseEntityConflict = exceptionOf({
|
|
40
|
+
layer: ExceptionLayers.UseCase,
|
|
41
|
+
name: "UseCaseEntityConflict",
|
|
42
|
+
message: "The entity already exists."
|
|
43
|
+
});
|
|
44
|
+
export const UseCaseInvalidCredentials = exceptionOf({
|
|
45
|
+
layer: ExceptionLayers.UseCase,
|
|
46
|
+
name: "UseCaseInvalidCredentials",
|
|
47
|
+
message: "Invalid credentials."
|
|
48
|
+
});
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { isVersion, satisfies } from "../record/plugin.js";
|
|
2
|
+
import { Kind } from "../symbols/kind.js";
|
|
3
|
+
import {
|
|
4
|
+
IncompatibleUseCasePlugin,
|
|
5
|
+
InvalidUseCasePlugin,
|
|
6
|
+
UnknownUseCasePlugin
|
|
7
|
+
} from "./exceptions.js";
|
|
8
|
+
import { isUseCasePlugin } from "./plugin.js";
|
|
9
|
+
export function isUseCaseCatalog(value) {
|
|
10
|
+
return typeof value === "object" && value !== null && value[Kind] === "use-case-catalog";
|
|
11
|
+
}
|
|
12
|
+
export function useCaseCatalogOf(...plugins) {
|
|
13
|
+
const byName = {};
|
|
14
|
+
const context = {};
|
|
15
|
+
for (const plugin of plugins) {
|
|
16
|
+
if (!isUseCasePlugin(plugin)) {
|
|
17
|
+
throw new InvalidUseCasePlugin({
|
|
18
|
+
source: "catalog",
|
|
19
|
+
details: {
|
|
20
|
+
reason: "a catalog takes plugins made by `useCasePluginOf`"
|
|
21
|
+
}
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
if (Object.hasOwn(byName, plugin.name)) {
|
|
25
|
+
throw new InvalidUseCasePlugin({
|
|
26
|
+
source: plugin.name,
|
|
27
|
+
details: { reason: "the catalog has another plugin with its name" }
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
byName[plugin.name] = plugin;
|
|
31
|
+
context[plugin.name] = plugin.make;
|
|
32
|
+
}
|
|
33
|
+
const find = (name) => {
|
|
34
|
+
const plugin = Object.hasOwn(byName, name) ? byName[name] : undefined;
|
|
35
|
+
if (!plugin) {
|
|
36
|
+
throw new UnknownUseCasePlugin({ source: "catalog", details: { name } });
|
|
37
|
+
}
|
|
38
|
+
return plugin;
|
|
39
|
+
};
|
|
40
|
+
return Object.freeze({
|
|
41
|
+
[Kind]: "use-case-catalog",
|
|
42
|
+
plugins: Object.freeze(byName),
|
|
43
|
+
list: Object.freeze([...plugins]),
|
|
44
|
+
context: Object.freeze(context),
|
|
45
|
+
resolve(reference) {
|
|
46
|
+
const at = typeof reference === "string" ? reference.lastIndexOf("@") : -1;
|
|
47
|
+
if (at <= 0)
|
|
48
|
+
return find(String(reference));
|
|
49
|
+
const plugin = find(reference.slice(0, at));
|
|
50
|
+
const version = reference.slice(at + 1);
|
|
51
|
+
if (!isVersion(version)) {
|
|
52
|
+
throw new IncompatibleUseCasePlugin({
|
|
53
|
+
source: plugin.name,
|
|
54
|
+
details: { reason: `"${version}" is not a semantic version` }
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
if (!satisfies(plugin.version, version)) {
|
|
58
|
+
throw new IncompatibleUseCasePlugin({
|
|
59
|
+
source: plugin.name,
|
|
60
|
+
details: {
|
|
61
|
+
reason: `version ${plugin.version} does not satisfy ${version}`
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
return plugin;
|
|
66
|
+
}
|
|
67
|
+
});
|
|
68
|
+
}
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import { hasPlugin, isVersion } from "../record/plugin.js";
|
|
2
|
+
import { isRepository } from "../repository/repository.js";
|
|
3
|
+
import { inspectRepository } from "../repository/state.js";
|
|
4
|
+
import { Kind } from "../symbols/kind.js";
|
|
5
|
+
import { InvalidUseCasePlugin, UseCaseUnavailable } from "./exceptions.js";
|
|
6
|
+
import { isPlain } from "./steps/references.js";
|
|
7
|
+
import { useCaseTargetOf } from "./target.js";
|
|
8
|
+
import { useCaseOf } from "./use-case.js";
|
|
9
|
+
const UseCaseOrigin = Symbol("useCaseOrigin");
|
|
10
|
+
const NAME = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
11
|
+
const identities = new WeakMap;
|
|
12
|
+
let nextIdentity = 0;
|
|
13
|
+
function keyOf(value) {
|
|
14
|
+
if (typeof value === "function" || typeof value === "object" && value) {
|
|
15
|
+
if (Array.isArray(value))
|
|
16
|
+
return `[${value.map(keyOf).join(",")}]`;
|
|
17
|
+
if (isPlain(value)) {
|
|
18
|
+
return `{${Object.keys(value).sort().filter((key) => value[key] !== undefined).map((key) => `${JSON.stringify(key)}:${keyOf(value[key])}`).join(",")}}`;
|
|
19
|
+
}
|
|
20
|
+
let identity = identities.get(value);
|
|
21
|
+
if (identity === undefined) {
|
|
22
|
+
identity = nextIdentity++;
|
|
23
|
+
identities.set(value, identity);
|
|
24
|
+
}
|
|
25
|
+
return `#${identity}`;
|
|
26
|
+
}
|
|
27
|
+
return value === undefined ? "undefined" : JSON.stringify(value);
|
|
28
|
+
}
|
|
29
|
+
export function isUseCasePlugin(value) {
|
|
30
|
+
return typeof value === "object" && value !== null && value[Kind] === "use-case-plugin";
|
|
31
|
+
}
|
|
32
|
+
export function useCaseOriginOf(UseCase) {
|
|
33
|
+
return typeof UseCase === "function" ? UseCase[UseCaseOrigin] : undefined;
|
|
34
|
+
}
|
|
35
|
+
export function useCasePluginOf(name, settings) {
|
|
36
|
+
const source = typeof name === "string" ? name : "useCasePluginOf";
|
|
37
|
+
const invalid = (reason) => {
|
|
38
|
+
throw new InvalidUseCasePlugin({ source, details: { reason } });
|
|
39
|
+
};
|
|
40
|
+
if (typeof name !== "string" || !NAME.test(name)) {
|
|
41
|
+
invalid("its name must be an identifier");
|
|
42
|
+
}
|
|
43
|
+
if (!isPlain(settings) || typeof settings.compile !== "function") {
|
|
44
|
+
invalid("it needs a compile function");
|
|
45
|
+
}
|
|
46
|
+
const { version = "1.0.0", description, compile } = settings;
|
|
47
|
+
if (!isVersion(version)) {
|
|
48
|
+
invalid(`"${String(version)}" is not a semantic version`);
|
|
49
|
+
}
|
|
50
|
+
if (description !== undefined && typeof description !== "string") {
|
|
51
|
+
invalid("its description must be a string");
|
|
52
|
+
}
|
|
53
|
+
const names = (list, what) => {
|
|
54
|
+
if (list === undefined)
|
|
55
|
+
return Object.freeze([]);
|
|
56
|
+
if (!Array.isArray(list) || list.some((item) => typeof item !== "string" || item === "")) {
|
|
57
|
+
invalid(`requires.${what} must be a list of names`);
|
|
58
|
+
}
|
|
59
|
+
return Object.freeze([...new Set(list)]);
|
|
60
|
+
};
|
|
61
|
+
if (settings.requires !== undefined && !isPlain(settings.requires)) {
|
|
62
|
+
invalid("requires must be an object of actions and plugins");
|
|
63
|
+
}
|
|
64
|
+
const requires = Object.freeze({
|
|
65
|
+
actions: names(settings.requires?.actions, "actions"),
|
|
66
|
+
plugins: names(settings.requires?.plugins, "plugins")
|
|
67
|
+
});
|
|
68
|
+
const made = new WeakMap;
|
|
69
|
+
const make = (repository, given) => {
|
|
70
|
+
if (!isRepository(repository)) {
|
|
71
|
+
return invalid("it makes a use case for a repository of repositoryOf");
|
|
72
|
+
}
|
|
73
|
+
if (given !== undefined && !isPlain(given)) {
|
|
74
|
+
return invalid("its options must be a plain object");
|
|
75
|
+
}
|
|
76
|
+
const options = Object.freeze({ ...given });
|
|
77
|
+
const info = inspectRepository(repository);
|
|
78
|
+
let classes = made.get(info.state);
|
|
79
|
+
if (!classes) {
|
|
80
|
+
classes = new Map;
|
|
81
|
+
made.set(info.state, classes);
|
|
82
|
+
}
|
|
83
|
+
const key = keyOf(options);
|
|
84
|
+
const known = classes.get(key);
|
|
85
|
+
if (known)
|
|
86
|
+
return known;
|
|
87
|
+
const target = useCaseTargetOf(repository, name);
|
|
88
|
+
const missing = {
|
|
89
|
+
actions: requires.actions.filter((action) => !info.actions.includes(action)),
|
|
90
|
+
plugins: requires.plugins.filter((plugin) => !hasPlugin(info.entity, plugin))
|
|
91
|
+
};
|
|
92
|
+
if (missing.actions.length > 0 || missing.plugins.length > 0) {
|
|
93
|
+
const quoted = (list) => list.map((item) => `"${item}"`).join(", ");
|
|
94
|
+
throw new UseCaseUnavailable({
|
|
95
|
+
source: name,
|
|
96
|
+
details: {
|
|
97
|
+
repository: target.repositoryName,
|
|
98
|
+
reason: [
|
|
99
|
+
missing.actions.length > 0 ? `the repository lacks the action ${quoted(missing.actions)}` : undefined,
|
|
100
|
+
missing.plugins.length > 0 ? `the entity lacks the plugin ${quoted(missing.plugins)}` : undefined
|
|
101
|
+
].filter(Boolean).join(" and "),
|
|
102
|
+
missing
|
|
103
|
+
}
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
const UseCase = useCaseOf(compile(target, options));
|
|
107
|
+
Object.defineProperty(UseCase, UseCaseOrigin, {
|
|
108
|
+
value: Object.freeze({
|
|
109
|
+
plugin,
|
|
110
|
+
repository,
|
|
111
|
+
options
|
|
112
|
+
})
|
|
113
|
+
});
|
|
114
|
+
classes.set(key, UseCase);
|
|
115
|
+
return UseCase;
|
|
116
|
+
};
|
|
117
|
+
const plugin = Object.freeze({
|
|
118
|
+
[Kind]: "use-case-plugin",
|
|
119
|
+
name,
|
|
120
|
+
version,
|
|
121
|
+
...description === undefined ? {} : { description },
|
|
122
|
+
requires,
|
|
123
|
+
make,
|
|
124
|
+
toJSON: () => ({
|
|
125
|
+
kind: "use-case-plugin",
|
|
126
|
+
name,
|
|
127
|
+
version,
|
|
128
|
+
...description === undefined ? {} : { description },
|
|
129
|
+
requires
|
|
130
|
+
})
|
|
131
|
+
});
|
|
132
|
+
return plugin;
|
|
133
|
+
}
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
import { toOpenAPI } from "../openapi/openapi.js";
|
|
2
|
+
import { isProperty } from "../property/is-property.js";
|
|
3
|
+
const ROUTE_METHODS = [
|
|
4
|
+
"get",
|
|
5
|
+
"post",
|
|
6
|
+
"put",
|
|
7
|
+
"patch",
|
|
8
|
+
"delete"
|
|
9
|
+
];
|
|
10
|
+
const ROUTE_SUCCESS_STATUSES = [
|
|
11
|
+
200,
|
|
12
|
+
201,
|
|
13
|
+
202,
|
|
14
|
+
204
|
|
15
|
+
];
|
|
16
|
+
const PARAMETER = /^\{([A-Za-z_][A-Za-z0-9_]*)\}$/;
|
|
17
|
+
export const HEADER_NAME = /^[A-Za-z0-9!#$%&'*+.^_`|~-]+$/;
|
|
18
|
+
export function isRoutePath(value, parameters = true) {
|
|
19
|
+
if (typeof value !== "string" || !value.startsWith("/"))
|
|
20
|
+
return false;
|
|
21
|
+
return value.split("/").every((segment) => parameters && PARAMETER.test(segment) || !/[{}?#\s:*]/.test(segment));
|
|
22
|
+
}
|
|
23
|
+
export function parametersOfPath(path) {
|
|
24
|
+
const names = [];
|
|
25
|
+
for (const segment of path.split("/")) {
|
|
26
|
+
const name = PARAMETER.exec(segment)?.[1];
|
|
27
|
+
if (name)
|
|
28
|
+
names.push(name);
|
|
29
|
+
}
|
|
30
|
+
return names;
|
|
31
|
+
}
|
|
32
|
+
export function isScalarSchema(schema) {
|
|
33
|
+
const types = [schema.type ?? []].flat();
|
|
34
|
+
return !types.some((type) => type === "object" || type === "array");
|
|
35
|
+
}
|
|
36
|
+
export function isScalarKey(blueprint, name) {
|
|
37
|
+
if (!Object.hasOwn(blueprint.properties, name))
|
|
38
|
+
return false;
|
|
39
|
+
const field = blueprint.properties[name];
|
|
40
|
+
return isProperty(field) && isScalarSchema(toOpenAPI(field));
|
|
41
|
+
}
|
|
42
|
+
function isObject(value) {
|
|
43
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
44
|
+
}
|
|
45
|
+
const ROUTE_KEYS = [
|
|
46
|
+
"method",
|
|
47
|
+
"path",
|
|
48
|
+
"status",
|
|
49
|
+
"query",
|
|
50
|
+
"headers",
|
|
51
|
+
"cookie",
|
|
52
|
+
"setCookie"
|
|
53
|
+
];
|
|
54
|
+
const COOKIE_KEYS = [
|
|
55
|
+
"name",
|
|
56
|
+
"httpOnly",
|
|
57
|
+
"secure",
|
|
58
|
+
"sameSite",
|
|
59
|
+
"path",
|
|
60
|
+
"domain",
|
|
61
|
+
"maxAge"
|
|
62
|
+
];
|
|
63
|
+
function cookieIssue(cookie, where) {
|
|
64
|
+
if (!isObject(cookie) || typeof cookie.name !== "string") {
|
|
65
|
+
return `Expected ${where} to be { name, httpOnly?, secure?, sameSite?, path?, domain?, maxAge? }.`;
|
|
66
|
+
}
|
|
67
|
+
for (const key of Object.keys(cookie)) {
|
|
68
|
+
if (!COOKIE_KEYS.includes(key))
|
|
69
|
+
return `${where} has no option "${key}".`;
|
|
70
|
+
}
|
|
71
|
+
if (!HEADER_NAME.test(cookie.name)) {
|
|
72
|
+
return `Expected ${where}.name to be the name of a cookie.`;
|
|
73
|
+
}
|
|
74
|
+
for (const key of ["httpOnly", "secure"]) {
|
|
75
|
+
if (cookie[key] !== undefined && typeof cookie[key] !== "boolean") {
|
|
76
|
+
return `Expected ${where}.${key} to be a boolean.`;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
if (cookie.sameSite !== undefined && !["strict", "lax", "none"].includes(cookie.sameSite)) {
|
|
80
|
+
return `Expected ${where}.sameSite to be "strict", "lax" or "none".`;
|
|
81
|
+
}
|
|
82
|
+
if (cookie.sameSite === "none" && cookie.secure === false) {
|
|
83
|
+
return `${where}.sameSite "none" needs a secure cookie.`;
|
|
84
|
+
}
|
|
85
|
+
for (const key of ["path", "domain"]) {
|
|
86
|
+
const value = cookie[key];
|
|
87
|
+
if (value !== undefined && (typeof value !== "string" || value === "" || /[;\s]/.test(value))) {
|
|
88
|
+
return `Expected ${where}.${key} to be text without ";" or spaces.`;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
if (cookie.maxAge !== undefined && (!Number.isInteger(cookie.maxAge) || cookie.maxAge < 0)) {
|
|
92
|
+
return `Expected ${where}.maxAge to be a whole number of seconds, 0 or more.`;
|
|
93
|
+
}
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
export function routeDeclarationOf(route, blueprint, response) {
|
|
97
|
+
if (!isObject(route)) {
|
|
98
|
+
return "Expected route to be { method, path?, status?, query?, headers?, cookie?, setCookie? }.";
|
|
99
|
+
}
|
|
100
|
+
for (const key of Object.keys(route)) {
|
|
101
|
+
if (!ROUTE_KEYS.includes(key))
|
|
102
|
+
return `route has no option "${key}".`;
|
|
103
|
+
}
|
|
104
|
+
const method = typeof route.method === "string" ? route.method.toLowerCase() : undefined;
|
|
105
|
+
if (!ROUTE_METHODS.includes(method)) {
|
|
106
|
+
return `Expected route.method to be one of ${ROUTE_METHODS.join(", ")}.`;
|
|
107
|
+
}
|
|
108
|
+
const { path, status, query, headers, cookie, setCookie } = route;
|
|
109
|
+
const places = new Map;
|
|
110
|
+
const place = (key, where) => {
|
|
111
|
+
const taken = places.get(key);
|
|
112
|
+
if (taken !== undefined) {
|
|
113
|
+
return `route reads the input key "${key}" from two places: ${taken} and ${where}.`;
|
|
114
|
+
}
|
|
115
|
+
if (!isScalarKey(blueprint, key)) {
|
|
116
|
+
return `route reads "${key}" from ${where}, but it is not a scalar key of the input.`;
|
|
117
|
+
}
|
|
118
|
+
places.set(key, where);
|
|
119
|
+
return;
|
|
120
|
+
};
|
|
121
|
+
if (path !== undefined) {
|
|
122
|
+
if (!isRoutePath(path)) {
|
|
123
|
+
return 'Expected route.path to start with "/", with parameters only as whole "{name}" segments and no query.';
|
|
124
|
+
}
|
|
125
|
+
for (const name of parametersOfPath(path)) {
|
|
126
|
+
const bad = place(name, "the path");
|
|
127
|
+
if (bad)
|
|
128
|
+
return bad;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
if (status !== undefined && !ROUTE_SUCCESS_STATUSES.includes(status)) {
|
|
132
|
+
return `Expected route.status to be one of ${ROUTE_SUCCESS_STATUSES.join(", ")}.`;
|
|
133
|
+
}
|
|
134
|
+
if (query !== undefined) {
|
|
135
|
+
if (!Array.isArray(query) || query.some((key) => typeof key !== "string")) {
|
|
136
|
+
return "Expected route.query to be a list of input keys.";
|
|
137
|
+
}
|
|
138
|
+
for (const key of query) {
|
|
139
|
+
const bad = place(key, "the query");
|
|
140
|
+
if (bad)
|
|
141
|
+
return bad;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
for (const [option, value, where] of [
|
|
145
|
+
["headers", headers, "a header"],
|
|
146
|
+
["cookie", cookie, "a cookie"]
|
|
147
|
+
]) {
|
|
148
|
+
if (value === undefined)
|
|
149
|
+
continue;
|
|
150
|
+
if (!isObject(value)) {
|
|
151
|
+
return `Expected route.${option} to map input keys to names.`;
|
|
152
|
+
}
|
|
153
|
+
const names = new Set;
|
|
154
|
+
for (const [key, name] of Object.entries(value)) {
|
|
155
|
+
if (typeof name !== "string" || !HEADER_NAME.test(name)) {
|
|
156
|
+
return `Expected route.${option}["${key}"] to be the name of ${where}.`;
|
|
157
|
+
}
|
|
158
|
+
const bad = place(key, where);
|
|
159
|
+
if (bad)
|
|
160
|
+
return bad;
|
|
161
|
+
const folded = option === "headers" ? name.toLowerCase() : name;
|
|
162
|
+
if (names.has(folded)) {
|
|
163
|
+
return `route.${option} reads "${name}" into two input keys.`;
|
|
164
|
+
}
|
|
165
|
+
names.add(folded);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
if (setCookie !== undefined) {
|
|
169
|
+
if (!isObject(setCookie)) {
|
|
170
|
+
return "Expected route.setCookie to map response keys to cookies.";
|
|
171
|
+
}
|
|
172
|
+
const names = new Set;
|
|
173
|
+
for (const [key, settings] of Object.entries(setCookie)) {
|
|
174
|
+
if (!isScalarKey(response, key)) {
|
|
175
|
+
return `route.setCookie sends "${key}", which is not a scalar key of the response.`;
|
|
176
|
+
}
|
|
177
|
+
const bad = cookieIssue(settings, `route.setCookie["${key}"]`);
|
|
178
|
+
if (bad)
|
|
179
|
+
return bad;
|
|
180
|
+
const { name } = settings;
|
|
181
|
+
if (names.has(name)) {
|
|
182
|
+
return `route.setCookie sets the cookie "${name}" from two response keys.`;
|
|
183
|
+
}
|
|
184
|
+
names.add(name);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
return Object.freeze({
|
|
188
|
+
method,
|
|
189
|
+
...path === undefined ? {} : { path },
|
|
190
|
+
...status === undefined ? {} : { status },
|
|
191
|
+
...query === undefined ? {} : { query: Object.freeze([...query]) },
|
|
192
|
+
...headers === undefined ? {} : {
|
|
193
|
+
headers: Object.freeze(Object.fromEntries(Object.entries(headers).map(([key, name]) => [key, name.toLowerCase()])))
|
|
194
|
+
},
|
|
195
|
+
...cookie === undefined ? {} : { cookie: Object.freeze({ ...cookie }) },
|
|
196
|
+
...setCookie === undefined ? {} : {
|
|
197
|
+
setCookie: Object.freeze(Object.fromEntries(Object.entries(setCookie).map(([key, settings]) => [key, Object.freeze({ ...settings })])))
|
|
198
|
+
}
|
|
199
|
+
});
|
|
200
|
+
}
|