@cosmicdrift/kumiko-framework 0.356.0 → 0.358.0
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/dist/crypto/pii-field-encryption.d.ts +1 -0
- package/dist/crypto/pii-field-encryption.js +1 -0
- package/dist/crypto/subject-resolver.d.ts +2 -0
- package/dist/crypto/subject-resolver.js +9 -6
- package/dist/db/connection.d.ts +2 -0
- package/dist/db/connection.js +10 -0
- package/dist/db/event-store-executor-context.d.ts +4 -1
- package/dist/db/event-store-executor-context.js +4 -2
- package/dist/db/event-store-executor-write.js +6 -1
- package/dist/db/index.d.ts +1 -1
- package/dist/db/index.js +1 -1
- package/dist/db/queries/backfill-pii.js +58 -34
- package/dist/engine/boot-validator/card-meta-overflow.d.ts +10 -0
- package/dist/engine/boot-validator/card-meta-overflow.js +95 -0
- package/dist/engine/boot-validator/entity-handler.js +3 -2
- package/dist/engine/boot-validator/index.d.ts +1 -0
- package/dist/engine/boot-validator/index.js +4 -1
- package/dist/engine/boot-validator/pii-retention.js +7 -0
- package/dist/engine/boot-validator/query-refs.js +3 -0
- package/dist/engine/boot-validator/screens.js +7 -1
- package/dist/engine/boot-validator.d.ts +1 -1
- package/dist/engine/boot-validator.js +1 -1
- package/dist/engine/factories.js +23 -19
- package/dist/engine/feature-ast/extractors/resolve-import.d.ts +3 -1
- package/dist/engine/feature-ast/extractors/resolve-import.js +9 -8
- package/dist/engine/feature-ast/extractors/round3.js +3 -2
- package/dist/engine/feature-ast/extractors/shared.d.ts +12 -2
- package/dist/engine/feature-ast/extractors/shared.js +31 -11
- package/dist/engine/index.d.ts +1 -1
- package/dist/engine/index.js +1 -1
- package/dist/files/file-ref-entity.d.ts +6 -0
- package/dist/files/file-ref-table.d.ts +6 -0
- package/dist/i18n/required-surface-keys.js +1 -0
- package/dist/jobs/job-runner.d.ts +1 -0
- package/dist/jobs/job-runner.js +51 -13
- package/dist/pipeline/dispatch-shared.d.ts +1 -0
- package/dist/pipeline/dispatch-shared.js +1 -1
- package/dist/pipeline/dispatcher.d.ts +2 -0
- package/dist/pipeline/dispatcher.js +1 -0
- package/dist/pipeline/system-hooks.js +7 -3
- package/dist/stack/db.d.ts +3 -0
- package/dist/stack/db.js +7 -1
- package/dist/stack/test-stack.d.ts +4 -0
- package/dist/stack/test-stack.js +5 -0
- package/dist/testing/shared-entities.d.ts +10 -0
- package/dist/ui-types/card-column-roles.d.ts +14 -0
- package/dist/ui-types/card-column-roles.js +9 -0
- package/dist/ui-types/index.d.ts +2 -0
- package/dist/ui-types/index.js +1 -0
- package/package.json +4 -4
- package/src/changes.json +38 -0
|
@@ -5,7 +5,8 @@ const MODULE_RESOLUTION_OPTIONS = {
|
|
|
5
5
|
target: ts.ScriptTarget.ESNext,
|
|
6
6
|
};
|
|
7
7
|
const MAX_RESOLUTION_DEPTH = 24;
|
|
8
|
-
|
|
8
|
+
const DECLARATION_FILES_OK = { allowDeclarationFiles: true };
|
|
9
|
+
export function resolveModuleFile(fromFile, specifier, { allowDeclarationFiles = false } = {}) {
|
|
9
10
|
const project = fromFile.getProject();
|
|
10
11
|
// The project's own host, not ts.sys: it honours an in-memory file system
|
|
11
12
|
// (Designer) and exists outside Node.
|
|
@@ -13,10 +14,10 @@ export function resolveModuleFile(fromFile, specifier) {
|
|
|
13
14
|
const resolvedFileName = resolved.resolvedModule?.resolvedFileName;
|
|
14
15
|
if (!resolvedFileName)
|
|
15
16
|
return undefined;
|
|
16
|
-
// A declaration file has no initializers to read.
|
|
17
|
-
//
|
|
18
|
-
//
|
|
19
|
-
if (resolvedFileName.endsWith(".d.ts"))
|
|
17
|
+
// A declaration file has no initializers to read. Callers that only need
|
|
18
|
+
// `export declare const X: "literal"` types opt in (installed packages
|
|
19
|
+
// resolve to dist/*.d.ts through their publishConfig exports).
|
|
20
|
+
if (!allowDeclarationFiles && resolvedFileName.endsWith(".d.ts"))
|
|
20
21
|
return undefined;
|
|
21
22
|
return fromFile.getProject().addSourceFileAtPathIfExists(resolvedFileName);
|
|
22
23
|
}
|
|
@@ -71,7 +72,7 @@ function resolveViaNamedExportSpecifier(file, exportedName, visited, depth) {
|
|
|
71
72
|
const moduleSpecifierValue = exportDecl.getModuleSpecifierValue();
|
|
72
73
|
if (!moduleSpecifierValue)
|
|
73
74
|
continue;
|
|
74
|
-
const target = resolveModuleFile(file, moduleSpecifierValue);
|
|
75
|
+
const target = resolveModuleFile(file, moduleSpecifierValue, DECLARATION_FILES_OK);
|
|
75
76
|
if (!target)
|
|
76
77
|
continue;
|
|
77
78
|
const resolved = resolveExportedVariable(target, originalName, visited, depth + 1);
|
|
@@ -87,7 +88,7 @@ function resolveLocalExportBinding(file, originalName, visited, depth) {
|
|
|
87
88
|
const binding = findImportBindingForLocalName(file, originalName);
|
|
88
89
|
if (!binding)
|
|
89
90
|
return undefined;
|
|
90
|
-
const target = resolveModuleFile(file, binding.moduleSpecifier);
|
|
91
|
+
const target = resolveModuleFile(file, binding.moduleSpecifier, DECLARATION_FILES_OK);
|
|
91
92
|
if (!target)
|
|
92
93
|
return undefined;
|
|
93
94
|
return resolveExportedVariable(target, binding.importedName, visited, depth + 1);
|
|
@@ -103,7 +104,7 @@ function resolveViaStarExport(file, exportedName, visited, depth) {
|
|
|
103
104
|
const moduleSpecifierValue = exportDecl.getModuleSpecifierValue();
|
|
104
105
|
if (!moduleSpecifierValue)
|
|
105
106
|
continue;
|
|
106
|
-
const target = resolveModuleFile(file, moduleSpecifierValue);
|
|
107
|
+
const target = resolveModuleFile(file, moduleSpecifierValue, DECLARATION_FILES_OK);
|
|
107
108
|
if (!target)
|
|
108
109
|
continue;
|
|
109
110
|
const resolved = resolveExportedVariable(target, exportedName, visited, depth + 1);
|
|
@@ -212,6 +212,7 @@ export function extractReferenceData(call, sourceFile) {
|
|
|
212
212
|
...(upsertKey !== undefined && { upsertKey }),
|
|
213
213
|
});
|
|
214
214
|
}
|
|
215
|
+
const USE_EXTENSION_READ_OPTIONS = { functionValuesAsRaw: true };
|
|
215
216
|
export function extractUseExtension(call, sourceFile) {
|
|
216
217
|
const args = call.getArguments();
|
|
217
218
|
const first = args[0];
|
|
@@ -245,7 +246,7 @@ export function extractUseExtension(call, sourceFile) {
|
|
|
245
246
|
?.asKind(SyntaxKind.PropertyAssignment)
|
|
246
247
|
?.getInitializer();
|
|
247
248
|
if (optionsInit) {
|
|
248
|
-
const parsed = readDataLiteralNode(optionsInit);
|
|
249
|
+
const parsed = readDataLiteralNode(optionsInit, USE_EXTENSION_READ_OPTIONS);
|
|
249
250
|
if (!isPlainObject(parsed)) {
|
|
250
251
|
return fail("useExtension", sourceLocationFromNode(call, sourceFile), "options could not be read as a plain object");
|
|
251
252
|
}
|
|
@@ -274,7 +275,7 @@ export function extractUseExtension(call, sourceFile) {
|
|
|
274
275
|
const optionsArg = args[2];
|
|
275
276
|
let options;
|
|
276
277
|
if (optionsArg) {
|
|
277
|
-
const parsed = readDataLiteralNode(optionsArg);
|
|
278
|
+
const parsed = readDataLiteralNode(optionsArg, USE_EXTENSION_READ_OPTIONS);
|
|
278
279
|
if (!isPlainObject(parsed)) {
|
|
279
280
|
return fail("useExtension", sourceLocationFromNode(call, sourceFile), "options could not be read as a plain object");
|
|
280
281
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import type { CallExpression,
|
|
1
|
+
import type { CallExpression, ObjectLiteralExpression } from "ts-morph";
|
|
2
|
+
import { Node } from "ts-morph";
|
|
2
3
|
import type { ParseError } from "../parse.js";
|
|
3
4
|
export type ExtractOutput<TPattern> = {
|
|
4
5
|
readonly kind: "pattern";
|
|
@@ -26,7 +27,16 @@ export type RawRefSentinel = {
|
|
|
26
27
|
};
|
|
27
28
|
export declare function isRawRefSentinel(value: unknown): value is RawRefSentinel;
|
|
28
29
|
export declare function containsRawRefSentinel(value: unknown): boolean;
|
|
29
|
-
export
|
|
30
|
+
export type ReadDataLiteralOptions = {
|
|
31
|
+
/**
|
|
32
|
+
* Keep arrow/function-expression values as `__raw` sentinels instead of
|
|
33
|
+
* failing the whole read. Only for pattern options that are code by
|
|
34
|
+
* nature (useExtension); data-only definitions (entity, nav) must still
|
|
35
|
+
* reject functions.
|
|
36
|
+
*/
|
|
37
|
+
readonly functionValuesAsRaw?: boolean;
|
|
38
|
+
};
|
|
39
|
+
export declare function readDataLiteralNode(node: Node, readOptions?: ReadDataLiteralOptions): unknown;
|
|
30
40
|
/**
|
|
31
41
|
* A node's literal string value, or the raw-ref sentinel when it resolves to
|
|
32
42
|
* an unresolvable identifier/factory-call/member-access (see
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { SyntaxKind } from "ts-morph";
|
|
1
|
+
import { Node, SyntaxKind } from "ts-morph";
|
|
2
2
|
import { isPlainObject } from "../../../utils/is-plain-object.js";
|
|
3
3
|
import { findImportBindingForLocalName, findScopedVariableDeclaration, resolveExportedVariable, resolveModuleFile, } from "./resolve-import.js";
|
|
4
4
|
export function ok(pattern) {
|
|
@@ -55,7 +55,7 @@ export function containsRawRefSentinel(value) {
|
|
|
55
55
|
return Object.values(value).some(containsRawRefSentinel);
|
|
56
56
|
return false;
|
|
57
57
|
}
|
|
58
|
-
export function readDataLiteralNode(node) {
|
|
58
|
+
export function readDataLiteralNode(node, readOptions = {}) {
|
|
59
59
|
const kind = node.getKind();
|
|
60
60
|
switch (kind) {
|
|
61
61
|
case SyntaxKind.StringLiteral:
|
|
@@ -74,7 +74,7 @@ export function readDataLiteralNode(node) {
|
|
|
74
74
|
const expr = node.asKindOrThrow(SyntaxKind.PrefixUnaryExpression);
|
|
75
75
|
if (expr.getOperatorToken() !== SyntaxKind.MinusToken)
|
|
76
76
|
return undefined;
|
|
77
|
-
const inner = readDataLiteralNode(expr.getOperand());
|
|
77
|
+
const inner = readDataLiteralNode(expr.getOperand(), readOptions);
|
|
78
78
|
if (typeof inner !== "number")
|
|
79
79
|
return undefined;
|
|
80
80
|
return -inner;
|
|
@@ -83,7 +83,7 @@ export function readDataLiteralNode(node) {
|
|
|
83
83
|
const arr = node.asKindOrThrow(SyntaxKind.ArrayLiteralExpression);
|
|
84
84
|
const out = [];
|
|
85
85
|
for (const el of arr.getElements()) {
|
|
86
|
-
const value = readDataLiteralNode(el);
|
|
86
|
+
const value = readDataLiteralNode(el, readOptions);
|
|
87
87
|
if (value === undefined)
|
|
88
88
|
return undefined;
|
|
89
89
|
out.push(value);
|
|
@@ -100,7 +100,7 @@ export function readDataLiteralNode(node) {
|
|
|
100
100
|
const initializer = propAssign.getInitializer();
|
|
101
101
|
if (!initializer)
|
|
102
102
|
return undefined;
|
|
103
|
-
const value = readDataLiteralNode(initializer);
|
|
103
|
+
const value = readDataLiteralNode(initializer, readOptions);
|
|
104
104
|
if (value === undefined)
|
|
105
105
|
return undefined;
|
|
106
106
|
out[readPropertyKey(propAssign)] = value;
|
|
@@ -108,11 +108,11 @@ export function readDataLiteralNode(node) {
|
|
|
108
108
|
return out;
|
|
109
109
|
}
|
|
110
110
|
case SyntaxKind.AsExpression:
|
|
111
|
-
return readDataLiteralNode(node.asKindOrThrow(SyntaxKind.AsExpression).getExpression());
|
|
111
|
+
return readDataLiteralNode(node.asKindOrThrow(SyntaxKind.AsExpression).getExpression(), readOptions);
|
|
112
112
|
case SyntaxKind.SatisfiesExpression:
|
|
113
|
-
return readDataLiteralNode(node.asKindOrThrow(SyntaxKind.SatisfiesExpression).getExpression());
|
|
113
|
+
return readDataLiteralNode(node.asKindOrThrow(SyntaxKind.SatisfiesExpression).getExpression(), readOptions);
|
|
114
114
|
case SyntaxKind.ParenthesizedExpression:
|
|
115
|
-
return readDataLiteralNode(node.asKindOrThrow(SyntaxKind.ParenthesizedExpression).getExpression());
|
|
115
|
+
return readDataLiteralNode(node.asKindOrThrow(SyntaxKind.ParenthesizedExpression).getExpression(), readOptions);
|
|
116
116
|
case SyntaxKind.Identifier:
|
|
117
117
|
case SyntaxKind.CallExpression:
|
|
118
118
|
case SyntaxKind.PropertyAccessExpression:
|
|
@@ -121,6 +121,11 @@ export function readDataLiteralNode(node) {
|
|
|
121
121
|
// resolving/inlining it would silently rewrite the source on the next
|
|
122
122
|
// parse->render roundtrip (see readDataLiteralNode doc above).
|
|
123
123
|
return { __raw: node.getText() };
|
|
124
|
+
case SyntaxKind.ArrowFunction:
|
|
125
|
+
case SyntaxKind.FunctionExpression:
|
|
126
|
+
return readOptions.functionValuesAsRaw
|
|
127
|
+
? { __raw: node.getText() }
|
|
128
|
+
: undefined;
|
|
124
129
|
default:
|
|
125
130
|
return undefined;
|
|
126
131
|
}
|
|
@@ -178,6 +183,15 @@ function unwrapLiteralInitializer(node) {
|
|
|
178
183
|
return unwrapLiteralInitializer(paren.getExpression());
|
|
179
184
|
return undefined;
|
|
180
185
|
}
|
|
186
|
+
// `export declare const NAME: "value"` in a .d.ts has no initializer, only a
|
|
187
|
+
// string-literal type.
|
|
188
|
+
function readDeclaredStringLiteralType(decl) {
|
|
189
|
+
const typeNode = decl.getTypeNode();
|
|
190
|
+
if (!typeNode || !Node.isLiteralTypeNode(typeNode))
|
|
191
|
+
return undefined;
|
|
192
|
+
const literal = typeNode.getLiteral();
|
|
193
|
+
return Node.isStringLiteral(literal) ? literal.getLiteralValue() : undefined;
|
|
194
|
+
}
|
|
181
195
|
/**
|
|
182
196
|
* Resolves a bare Identifier to the string value of its declaration's
|
|
183
197
|
* initializer (`export const EXT_TENANT_DATA = "tenant-data" as const`),
|
|
@@ -200,12 +214,18 @@ function resolveIdentifierToStringLiteral(identifier) {
|
|
|
200
214
|
const binding = findImportBindingForLocalName(id.getSourceFile(), name);
|
|
201
215
|
if (!binding)
|
|
202
216
|
return undefined;
|
|
203
|
-
const targetFile = resolveModuleFile(id.getSourceFile(), binding.moduleSpecifier
|
|
217
|
+
const targetFile = resolveModuleFile(id.getSourceFile(), binding.moduleSpecifier, {
|
|
218
|
+
allowDeclarationFiles: true,
|
|
219
|
+
});
|
|
204
220
|
if (!targetFile)
|
|
205
221
|
return undefined;
|
|
206
222
|
const decl = resolveExportedVariable(targetFile, binding.importedName);
|
|
207
|
-
|
|
208
|
-
|
|
223
|
+
if (!decl)
|
|
224
|
+
return undefined;
|
|
225
|
+
const init = decl.getInitializer();
|
|
226
|
+
if (init)
|
|
227
|
+
return unwrapLiteralInitializer(init);
|
|
228
|
+
return readDeclaredStringLiteralType(decl);
|
|
209
229
|
}
|
|
210
230
|
/**
|
|
211
231
|
* Like `readNameLiteral`, but for callers that must preserve an
|
package/dist/engine/index.d.ts
CHANGED
|
@@ -3,7 +3,7 @@ export { hasAccess } from "./access.js";
|
|
|
3
3
|
export { isPrincipalStatusPlugin, isTenantLifecycleStatusPlugin, type PrincipalProfile, type PrincipalStatus, type PrincipalStatusPlugin, TENANT_TEARDOWN_STATUSES, type TenantLifecycleStatusPlugin, } from "./active-membership.js";
|
|
4
4
|
export { PII_DIRECT_NAME_HINTS, PII_USER_OWNED_NAME_HINTS, PII_USER_REFERENCE_NAME_HINTS, } from "./boot-validator/entity-handler.js";
|
|
5
5
|
export { entityHasAnonymizableSubjectField } from "./boot-validator/pii-retention.js";
|
|
6
|
-
export { collectWriteHandlerQns, MAX_TRANSFER_DEPTH, SECURITY_BASELINE_FEATURE_NAMES, type ValidateBootOptions, validateAppCustomScreenWriteQns, validateBoot, } from "./boot-validator.js";
|
|
6
|
+
export { type CardMetaOverflow, collectCardMetaOverflow, collectWriteHandlerQns, MAX_TRANSFER_DEPTH, SECURITY_BASELINE_FEATURE_NAMES, type ValidateBootOptions, validateAppCustomScreenWriteQns, validateBoot, } from "./boot-validator.js";
|
|
7
7
|
export { type BuildAppSchemaOptions, buildAppSchema } from "./build-app-schema.js";
|
|
8
8
|
export type { ConfigFeatureSchema } from "./build-config-feature-schema.js";
|
|
9
9
|
export { buildConfigFeatureSchema, SETTINGS_HUB_FEATURE, SETTINGS_HUB_WORKSPACE, } from "./build-config-feature-schema.js";
|
package/dist/engine/index.js
CHANGED
|
@@ -4,7 +4,7 @@ export { hasAccess } from "./access.js";
|
|
|
4
4
|
export { isPrincipalStatusPlugin, isTenantLifecycleStatusPlugin, TENANT_TEARDOWN_STATUSES, } from "./active-membership.js";
|
|
5
5
|
export { PII_DIRECT_NAME_HINTS, PII_USER_OWNED_NAME_HINTS, PII_USER_REFERENCE_NAME_HINTS, } from "./boot-validator/entity-handler.js";
|
|
6
6
|
export { entityHasAnonymizableSubjectField } from "./boot-validator/pii-retention.js";
|
|
7
|
-
export { collectWriteHandlerQns, MAX_TRANSFER_DEPTH, SECURITY_BASELINE_FEATURE_NAMES, validateAppCustomScreenWriteQns, validateBoot, } from "./boot-validator.js";
|
|
7
|
+
export { collectCardMetaOverflow, collectWriteHandlerQns, MAX_TRANSFER_DEPTH, SECURITY_BASELINE_FEATURE_NAMES, validateAppCustomScreenWriteQns, validateBoot, } from "./boot-validator.js";
|
|
8
8
|
export { buildAppSchema } from "./build-app-schema.js";
|
|
9
9
|
export { buildConfigFeatureSchema, SETTINGS_HUB_FEATURE, SETTINGS_HUB_WORKSPACE, } from "./build-config-feature-schema.js";
|
|
10
10
|
export { buildTarget } from "./build-target.js";
|
|
@@ -4,6 +4,7 @@ export declare const fileRefEntity: import("@cosmicdrift/kumiko-types/fields").E
|
|
|
4
4
|
readonly description?: string;
|
|
5
5
|
readonly maxLength?: number;
|
|
6
6
|
readonly minLength?: number;
|
|
7
|
+
readonly autoComplete?: string;
|
|
7
8
|
readonly required?: boolean;
|
|
8
9
|
readonly searchable?: boolean;
|
|
9
10
|
readonly sortable?: boolean;
|
|
@@ -26,6 +27,7 @@ export declare const fileRefEntity: import("@cosmicdrift/kumiko-types/fields").E
|
|
|
26
27
|
readonly description?: string;
|
|
27
28
|
readonly maxLength?: number;
|
|
28
29
|
readonly minLength?: number;
|
|
30
|
+
readonly autoComplete?: string;
|
|
29
31
|
readonly required?: boolean;
|
|
30
32
|
readonly searchable?: boolean;
|
|
31
33
|
readonly sortable?: boolean;
|
|
@@ -48,6 +50,7 @@ export declare const fileRefEntity: import("@cosmicdrift/kumiko-types/fields").E
|
|
|
48
50
|
readonly description?: string;
|
|
49
51
|
readonly maxLength?: number;
|
|
50
52
|
readonly minLength?: number;
|
|
53
|
+
readonly autoComplete?: string;
|
|
51
54
|
readonly required?: boolean;
|
|
52
55
|
readonly searchable?: boolean;
|
|
53
56
|
readonly sortable?: boolean;
|
|
@@ -82,6 +85,7 @@ export declare const fileRefEntity: import("@cosmicdrift/kumiko-types/fields").E
|
|
|
82
85
|
readonly description?: string;
|
|
83
86
|
readonly maxLength?: number;
|
|
84
87
|
readonly minLength?: number;
|
|
88
|
+
readonly autoComplete?: string;
|
|
85
89
|
readonly required?: boolean;
|
|
86
90
|
readonly searchable?: boolean;
|
|
87
91
|
readonly sortable?: boolean;
|
|
@@ -104,6 +108,7 @@ export declare const fileRefEntity: import("@cosmicdrift/kumiko-types/fields").E
|
|
|
104
108
|
readonly description?: string;
|
|
105
109
|
readonly maxLength?: number;
|
|
106
110
|
readonly minLength?: number;
|
|
111
|
+
readonly autoComplete?: string;
|
|
107
112
|
readonly required?: boolean;
|
|
108
113
|
readonly searchable?: boolean;
|
|
109
114
|
readonly sortable?: boolean;
|
|
@@ -126,6 +131,7 @@ export declare const fileRefEntity: import("@cosmicdrift/kumiko-types/fields").E
|
|
|
126
131
|
readonly description?: string;
|
|
127
132
|
readonly maxLength?: number;
|
|
128
133
|
readonly minLength?: number;
|
|
134
|
+
readonly autoComplete?: string;
|
|
129
135
|
readonly required?: boolean;
|
|
130
136
|
readonly searchable?: boolean;
|
|
131
137
|
readonly sortable?: boolean;
|
|
@@ -4,6 +4,7 @@ export declare const fileRefsTable: import("../db/table-builder.js").EntityTable
|
|
|
4
4
|
readonly description?: string;
|
|
5
5
|
readonly maxLength?: number;
|
|
6
6
|
readonly minLength?: number;
|
|
7
|
+
readonly autoComplete?: string;
|
|
7
8
|
readonly required?: boolean;
|
|
8
9
|
readonly searchable?: boolean;
|
|
9
10
|
readonly sortable?: boolean;
|
|
@@ -26,6 +27,7 @@ export declare const fileRefsTable: import("../db/table-builder.js").EntityTable
|
|
|
26
27
|
readonly description?: string;
|
|
27
28
|
readonly maxLength?: number;
|
|
28
29
|
readonly minLength?: number;
|
|
30
|
+
readonly autoComplete?: string;
|
|
29
31
|
readonly required?: boolean;
|
|
30
32
|
readonly searchable?: boolean;
|
|
31
33
|
readonly sortable?: boolean;
|
|
@@ -48,6 +50,7 @@ export declare const fileRefsTable: import("../db/table-builder.js").EntityTable
|
|
|
48
50
|
readonly description?: string;
|
|
49
51
|
readonly maxLength?: number;
|
|
50
52
|
readonly minLength?: number;
|
|
53
|
+
readonly autoComplete?: string;
|
|
51
54
|
readonly required?: boolean;
|
|
52
55
|
readonly searchable?: boolean;
|
|
53
56
|
readonly sortable?: boolean;
|
|
@@ -82,6 +85,7 @@ export declare const fileRefsTable: import("../db/table-builder.js").EntityTable
|
|
|
82
85
|
readonly description?: string;
|
|
83
86
|
readonly maxLength?: number;
|
|
84
87
|
readonly minLength?: number;
|
|
88
|
+
readonly autoComplete?: string;
|
|
85
89
|
readonly required?: boolean;
|
|
86
90
|
readonly searchable?: boolean;
|
|
87
91
|
readonly sortable?: boolean;
|
|
@@ -104,6 +108,7 @@ export declare const fileRefsTable: import("../db/table-builder.js").EntityTable
|
|
|
104
108
|
readonly description?: string;
|
|
105
109
|
readonly maxLength?: number;
|
|
106
110
|
readonly minLength?: number;
|
|
111
|
+
readonly autoComplete?: string;
|
|
107
112
|
readonly required?: boolean;
|
|
108
113
|
readonly searchable?: boolean;
|
|
109
114
|
readonly sortable?: boolean;
|
|
@@ -126,6 +131,7 @@ export declare const fileRefsTable: import("../db/table-builder.js").EntityTable
|
|
|
126
131
|
readonly description?: string;
|
|
127
132
|
readonly maxLength?: number;
|
|
128
133
|
readonly minLength?: number;
|
|
134
|
+
readonly autoComplete?: string;
|
|
129
135
|
readonly required?: boolean;
|
|
130
136
|
readonly searchable?: boolean;
|
|
131
137
|
readonly sortable?: boolean;
|
|
@@ -283,6 +283,7 @@ export function requiredKeysFromScreen(featureName, screen, options = {}) {
|
|
|
283
283
|
pushKey(out, mint.reveal.title);
|
|
284
284
|
pushKey(out, mint.reveal.warning);
|
|
285
285
|
pushKey(out, mint.reveal.confirmLabel);
|
|
286
|
+
pushKey(out, mint.reveal.acknowledge);
|
|
286
287
|
for (const revealField of mint.reveal.fields) {
|
|
287
288
|
pushKey(out, revealField.label);
|
|
288
289
|
}
|
|
@@ -9,6 +9,7 @@ import { type AppContext, type DispatchWriteRef, type JobRunIn, type JobTriggerE
|
|
|
9
9
|
*/
|
|
10
10
|
export declare function schedulerIdForJobName(jobName: string): string;
|
|
11
11
|
export declare function bootJobIdForJobName(jobName: string): string;
|
|
12
|
+
export declare const ORPHAN_SCHEDULER_GRACE_MS: number;
|
|
12
13
|
export type JobLogEntry = {
|
|
13
14
|
level: "info" | "warn" | "error";
|
|
14
15
|
message: string;
|
package/dist/jobs/job-runner.js
CHANGED
|
@@ -73,9 +73,48 @@ export function schedulerIdForJobName(jobName) {
|
|
|
73
73
|
export function bootJobIdForJobName(jobName) {
|
|
74
74
|
return `boot-${jobName.replace(/[.:]/g, "-")}`;
|
|
75
75
|
}
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
76
|
+
const SCHEDULER_PAGE_SIZE = 100;
|
|
77
|
+
// Rolling deploys run old and new pods against the same Redis for a while; an
|
|
78
|
+
// old runner restarting in that window must not delete the scheduler of a
|
|
79
|
+
// cron job the new version just added.
|
|
80
|
+
export const ORPHAN_SCHEDULER_GRACE_MS = 60 * 60 * 1000;
|
|
81
|
+
function schedulerSeenKey(queue) {
|
|
82
|
+
return queue.toKey("kumiko-scheduler-seen");
|
|
83
|
+
}
|
|
84
|
+
// A cron job dropped from the app (or an older scheduler-id format) leaves its
|
|
85
|
+
// scheduler in Redis, where it keeps firing jobs the worker no longer knows
|
|
86
|
+
// ("Unknown job"). Best-effort: a Redis hiccup here must not stop the start.
|
|
87
|
+
async function removeOrphanJobSchedulers(queue, expectedSchedulerIds, lane, log) {
|
|
88
|
+
try {
|
|
89
|
+
// Collect first: removing while paging would shift the indices.
|
|
90
|
+
const existingKeys = [];
|
|
91
|
+
for (let start = 0;; start += SCHEDULER_PAGE_SIZE) {
|
|
92
|
+
const page = await queue.getJobSchedulers(start, start + SCHEDULER_PAGE_SIZE - 1);
|
|
93
|
+
for (const scheduler of page)
|
|
94
|
+
existingKeys.push(scheduler.key);
|
|
95
|
+
if (page.length < SCHEDULER_PAGE_SIZE)
|
|
96
|
+
break;
|
|
97
|
+
}
|
|
98
|
+
const client = await queue.client;
|
|
99
|
+
const seenAt = await client.hgetall(schedulerSeenKey(queue));
|
|
100
|
+
const now = Date.now();
|
|
101
|
+
for (const key of existingKeys) {
|
|
102
|
+
if (expectedSchedulerIds.has(key))
|
|
103
|
+
continue;
|
|
104
|
+
const lastSeen = Number(seenAt[key]);
|
|
105
|
+
if (key in seenAt && now - lastSeen < ORPHAN_SCHEDULER_GRACE_MS)
|
|
106
|
+
continue;
|
|
107
|
+
await queue.removeJobScheduler(key);
|
|
108
|
+
await client.hdel(schedulerSeenKey(queue), key);
|
|
109
|
+
log.info("removed orphan job scheduler", { schedulerId: key, lane });
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
catch (err) {
|
|
113
|
+
log.warn("orphan job scheduler cleanup failed", {
|
|
114
|
+
lane,
|
|
115
|
+
error: err instanceof Error ? err.message : String(err),
|
|
116
|
+
});
|
|
117
|
+
}
|
|
79
118
|
}
|
|
80
119
|
// Deterministic per-tenant child job id, derived from the perTenant
|
|
81
120
|
// wrapper's own BullMQ job id. A cron tick's wrapper id is
|
|
@@ -1108,21 +1147,13 @@ export function createJobRunner(options) {
|
|
|
1108
1147
|
// Only schedule cron + boot for jobs that belong to this lane. Jobs
|
|
1109
1148
|
// assigned to the other lane get their cron/boot wiring from the
|
|
1110
1149
|
// runner running on that lane. Running both here would double-fire.
|
|
1150
|
+
const expectedSchedulerIds = new Set();
|
|
1111
1151
|
for (const [name, jobDef] of allJobs) {
|
|
1112
1152
|
if (laneForJob(jobDef) !== consumerLane)
|
|
1113
1153
|
continue;
|
|
1114
1154
|
if ("cron" in jobDef.trigger) {
|
|
1115
1155
|
const schedulerId = schedulerIdForJobName(name);
|
|
1116
|
-
|
|
1117
|
-
// Drop pre-sanitize scheduler ids so colon-form ghosts stop firing.
|
|
1118
|
-
if (legacyId !== schedulerId) {
|
|
1119
|
-
try {
|
|
1120
|
-
await consumerQueue.removeJobScheduler(legacyId);
|
|
1121
|
-
}
|
|
1122
|
-
catch {
|
|
1123
|
-
// skip: legacy scheduler absent (fresh install / already purged)
|
|
1124
|
-
}
|
|
1125
|
-
}
|
|
1156
|
+
expectedSchedulerIds.add(schedulerId);
|
|
1126
1157
|
await consumerQueue.upsertJobScheduler(schedulerId, { pattern: jobDef.trigger.cron }, {
|
|
1127
1158
|
name: jobDef.perTenant ? `_perTenant:${name}` : name,
|
|
1128
1159
|
data: {},
|
|
@@ -1133,8 +1164,15 @@ export function createJobRunner(options) {
|
|
|
1133
1164
|
...buildRetryBullOpts(jobDef),
|
|
1134
1165
|
},
|
|
1135
1166
|
});
|
|
1167
|
+
await (await consumerQueue.client).hset(schedulerSeenKey(consumerQueue), {
|
|
1168
|
+
[schedulerId]: String(Date.now()),
|
|
1169
|
+
});
|
|
1136
1170
|
}
|
|
1137
1171
|
}
|
|
1172
|
+
await removeOrphanJobSchedulers(consumerQueue, expectedSchedulerIds, consumerLane, {
|
|
1173
|
+
info: (msg, data) => context.log ? context.log.info(msg, data) : errorLogger.debug(msg, data),
|
|
1174
|
+
warn: errorLogger.warn,
|
|
1175
|
+
});
|
|
1138
1176
|
// Persistent marker outside the swept completed/failed sets: a plain
|
|
1139
1177
|
// Redis hash at one key per consumer queue, fields = boot job ids
|
|
1140
1178
|
// already enqueued (BullMQ's IRedisClient has no set commands, only
|
|
@@ -47,6 +47,7 @@ export type DispatchContext = {
|
|
|
47
47
|
meter: ReturnType<typeof getFallbackMeter>;
|
|
48
48
|
membershipQuery: string;
|
|
49
49
|
memberReadTimeoutMs: number;
|
|
50
|
+
outsideTransactionDb: DbConnection | undefined;
|
|
50
51
|
};
|
|
51
52
|
export declare const DEFAULT_MEMBER_READ_TIMEOUT_MS = 10000;
|
|
52
53
|
export declare function resolveDbSource(ctx: DispatchContext, tx: DbTx | undefined): DbConnection | DbTx | undefined;
|
|
@@ -248,7 +248,7 @@ export async function buildHandlerContext(ctx, type, user, origin, tx, afterComm
|
|
|
248
248
|
// that is meant to outlive the request. Computed before `systemDb` below
|
|
249
249
|
// so its guarded escape hatch (ctx.systemDb.outsideTransaction) can wrap
|
|
250
250
|
// this same TenantDb instead of a second, independently-built one.
|
|
251
|
-
const outsideTxSource = resolveDbSource(ctx, undefined);
|
|
251
|
+
const outsideTxSource = ctx.outsideTransactionDb ?? resolveDbSource(ctx, undefined);
|
|
252
252
|
const rawDbOutsideTransaction = outsideTxSource
|
|
253
253
|
? buildTenantScopedDb(outsideTxSource, undefined)
|
|
254
254
|
: undefined;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { CacheSyncBus } from "@cosmicdrift/kumiko-types/cache-sync-types";
|
|
2
2
|
import type { SseBroker } from "../api/sse-broker.js";
|
|
3
|
+
import type { DbConnection } from "../db/connection.js";
|
|
3
4
|
import type { EffectiveFeaturesResolver } from "../engine/tier-resolver-extension.js";
|
|
4
5
|
import type { TenantId } from "../engine/types/identifiers.js";
|
|
5
6
|
import type { ActiveMembershipResult, AppContext, DispatchWriteRef, JobRunnerRef, MemberReader, Registry, SessionUser, WriteResult } from "../engine/types/index.js";
|
|
@@ -16,6 +17,7 @@ export type DispatcherOptions = {
|
|
|
16
17
|
jobRunner?: JobRunnerRef;
|
|
17
18
|
membershipQuery?: string;
|
|
18
19
|
memberReadTimeoutMs?: number;
|
|
20
|
+
outsideTransactionDb?: DbConnection;
|
|
19
21
|
effectiveFeatures?: EffectiveFeaturesResolver;
|
|
20
22
|
sseBroker?: SseBroker;
|
|
21
23
|
cacheSync?: CacheSyncBus | null;
|
|
@@ -97,6 +97,7 @@ export function createDispatcher(registry, context, options = {}) {
|
|
|
97
97
|
meter: dispatcherMeter,
|
|
98
98
|
membershipQuery,
|
|
99
99
|
memberReadTimeoutMs,
|
|
100
|
+
outsideTransactionDb: options.outsideTransactionDb,
|
|
100
101
|
};
|
|
101
102
|
const dispatcher = {
|
|
102
103
|
async write(typeOrRef, payload, user, requestId) {
|
|
@@ -37,15 +37,19 @@ import { InternalError } from "../errors/index.js";
|
|
|
37
37
|
// call per tenant instead of one round-trip per event; handler stays the
|
|
38
38
|
// per-event fallback the dispatcher falls back to when a batch throws.
|
|
39
39
|
export const SEARCH_CONSUMER_NAME = "system:consumer:search";
|
|
40
|
+
// An executor update with includeDeleted leaves the row deleted; indexing it would make it findable again.
|
|
41
|
+
function isUpdateOfSoftDeletedRow(event, verb) {
|
|
42
|
+
return (verb === "updated" && reconstructStateForSearch(event.payload, "updated")["isDeleted"] === true);
|
|
43
|
+
}
|
|
40
44
|
// Resolves what one event implies for the search index, without touching
|
|
41
45
|
// the adapter — same decision tree the old per-event handler ran inline.
|
|
42
46
|
async function resolveSearchOperation(event, ctx, registry) {
|
|
43
47
|
const entityName = event.aggregateType;
|
|
44
48
|
const verb = event.type.split(".").pop();
|
|
45
49
|
const tenantId = event.tenantId;
|
|
46
|
-
// skip: delete/forgotten
|
|
47
|
-
// makes sense for created/updated/restored
|
|
48
|
-
if (verb === "deleted" || verb === "forgotten") {
|
|
50
|
+
// skip: delete/forgotten (and an update on a still soft-deleted row) remove the
|
|
51
|
+
// index entry — reconstruct only makes sense for live created/updated/restored.
|
|
52
|
+
if (verb === "deleted" || verb === "forgotten" || isUpdateOfSoftDeletedRow(event, verb)) {
|
|
49
53
|
return { kind: "remove", tenantId, entityType: entityName, entityId: event.aggregateId };
|
|
50
54
|
}
|
|
51
55
|
let state;
|
package/dist/stack/db.d.ts
CHANGED
|
@@ -2,6 +2,7 @@ import type { DbConnection } from "@cosmicdrift/kumiko-types/db-connection";
|
|
|
2
2
|
declare function requireEnv(name: string): string;
|
|
3
3
|
export type TestDb = {
|
|
4
4
|
db: DbConnection;
|
|
5
|
+
outsideTransactionDb: DbConnection;
|
|
5
6
|
client: unknown;
|
|
6
7
|
dbName: string;
|
|
7
8
|
cleanup: () => Promise<void>;
|
|
@@ -10,6 +11,8 @@ export type CreateTestDbOptions = {
|
|
|
10
11
|
readonly baseUrl?: string;
|
|
11
12
|
readonly dbName?: string;
|
|
12
13
|
readonly persistent?: boolean;
|
|
14
|
+
readonly poolMax?: number;
|
|
15
|
+
readonly outsideTransactionPoolMax?: number;
|
|
13
16
|
};
|
|
14
17
|
/**
|
|
15
18
|
* Provider-agnostische Test-DB. createConnection liest DB_PROVIDER.
|
package/dist/stack/db.js
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
// Provider-agnostic via createConnection (DB_PROVIDER env).
|
|
3
3
|
// postgres-js = default. DB_PROVIDER=bun = Bun.SQL (experimentell).
|
|
4
4
|
import { createConnection } from "../db/api.js";
|
|
5
|
+
import { DEFAULT_OUTSIDE_TRANSACTION_POOL_MAX } from "../db/connection.js";
|
|
5
6
|
import { createDatabase, databaseExists, dropDatabaseIfExists } from "../db/queries/test-stack.js";
|
|
6
7
|
import { ensureTemporalPolyfill } from "../time/polyfill.js";
|
|
7
8
|
import { generateId } from "../utils/index.js";
|
|
@@ -37,15 +38,20 @@ export async function createTestDb(arg) {
|
|
|
37
38
|
await admin.close();
|
|
38
39
|
}
|
|
39
40
|
const testUrl = url.replace(/\/[^/]+$/, `/${dbName}`);
|
|
40
|
-
const conn = await createConnection(testUrl);
|
|
41
|
+
const conn = await createConnection(testUrl, opts.poolMax !== undefined ? { maxConnections: opts.poolMax } : {});
|
|
42
|
+
const outsideConn = await createConnection(testUrl, {
|
|
43
|
+
maxConnections: opts.outsideTransactionPoolMax ?? DEFAULT_OUTSIDE_TRANSACTION_POOL_MAX,
|
|
44
|
+
});
|
|
41
45
|
const { createEventsTable } = await import("../event-store/index.js");
|
|
42
46
|
await createEventsTable(conn.db);
|
|
43
47
|
return {
|
|
44
48
|
db: conn.db,
|
|
49
|
+
outsideTransactionDb: outsideConn.db,
|
|
45
50
|
client: conn.client,
|
|
46
51
|
dbName,
|
|
47
52
|
cleanup: async () => {
|
|
48
53
|
await conn.close();
|
|
54
|
+
await outsideConn.close();
|
|
49
55
|
if (!opts.persistent) {
|
|
50
56
|
const admin2 = await createConnection(adminUrl, { maxConnections: 1 });
|
|
51
57
|
try {
|
|
@@ -101,6 +101,10 @@ export type TestStackOptions = {
|
|
|
101
101
|
* owns its lifecycle. Default false (test contract). Used by
|
|
102
102
|
* dev-server wiring to survive hot-reloads. */
|
|
103
103
|
persistentDb?: boolean;
|
|
104
|
+
/** Cap on the main app pool (default: driver default). Lets tests saturate it deliberately. */
|
|
105
|
+
dbPoolMax?: number;
|
|
106
|
+
/** Size of the dedicated pool behind ctx.dbOutsideTransaction / ctx.outsideTransaction. */
|
|
107
|
+
outsideTransactionPoolMax?: number;
|
|
104
108
|
/** Forwarded to buildServer — when set, requests without a JWT pass
|
|
105
109
|
* through as anonymous instead of 401. See AnonymousAccessConfig.
|
|
106
110
|
* Akzeptiert entweder einen statischen Config-Object ODER eine Factory
|
package/dist/stack/test-stack.js
CHANGED
|
@@ -44,6 +44,10 @@ export async function setupTestStack(options) {
|
|
|
44
44
|
createTestDb({
|
|
45
45
|
...(options.dbName !== undefined && { dbName: options.dbName }),
|
|
46
46
|
...(options.persistentDb !== undefined && { persistent: options.persistentDb }),
|
|
47
|
+
...(options.dbPoolMax !== undefined && { poolMax: options.dbPoolMax }),
|
|
48
|
+
...(options.outsideTransactionPoolMax !== undefined && {
|
|
49
|
+
outsideTransactionPoolMax: options.outsideTransactionPoolMax,
|
|
50
|
+
}),
|
|
47
51
|
}),
|
|
48
52
|
createTestRedis(options.sharedRedisWith && { borrowKeyPrefix: options.sharedRedisWith.redis.keyPrefix }),
|
|
49
53
|
]);
|
|
@@ -286,6 +290,7 @@ export async function setupTestStack(options) {
|
|
|
286
290
|
dispatcherOptions: {
|
|
287
291
|
idempotency,
|
|
288
292
|
cacheSync,
|
|
293
|
+
outsideTransactionDb: testDb.outsideTransactionDb,
|
|
289
294
|
...(options.effectiveFeatures && { effectiveFeatures: options.effectiveFeatures }),
|
|
290
295
|
...(jobRunner && { jobRunner }),
|
|
291
296
|
},
|