cabloy 5.1.219 → 5.1.221
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/.cabloy-version +1 -1
- package/.github/workflows/agent-governance.yml +2 -2
- package/.github/workflows/create-cabloy.yml +3 -3
- package/.github/workflows/docs-pages.yml +2 -2
- package/.github/workflows/playwright-e2e.yml +4 -4
- package/.github/workflows/vona-cov-pg.yml +2 -2
- package/.github/workflows/vona-test-crud.yml +2 -2
- package/.github/workflows/vona-test-mysql.yml +2 -2
- package/.github/workflows/vona-test-pg.yml +2 -2
- package/.github/workflows/vona-test-sqlite3.yml +2 -2
- package/.github/workflows/vona-tsc.yml +2 -2
- package/.github/workflows/zova-ui.yml +2 -2
- package/CHANGELOG.md +23 -0
- package/README.md +1 -1
- package/package.json +4 -3
- package/repo-docs/frontend/openapi-sdk-guide.md +2 -0
- package/repo-docs/fullstack/quickstart.md +3 -3
- package/repo-docs/reference/repo-scripts.md +16 -0
- package/scripts/generateOpenapiIsolated.test.ts +25 -0
- package/scripts/generateOpenapiIsolated.ts +46 -12
- package/scripts/runIsolatedTest.test.ts +165 -45
- package/scripts/runIsolatedTest.ts +111 -24
- package/scripts/strictGenerationDiagnostics.test.ts +1009 -0
- package/scripts/strictGenerationDiagnostics.ts +61 -0
- package/scripts/strictGenerationStartup.test.ts +212 -0
- package/vona/package.original.json +3 -2
- package/vona/packages-cli/cli/package.json +1 -1
- package/vona/packages-cli/cli-set-api/package.json +1 -1
- package/vona/packages-cli/cli-set-api/src/lib/bean/cli.bin.dev.ts +14 -4
- package/vona/packages-cli/cli-set-api/src/lib/bean/toolsBin/generateEntryFiles.ts +6 -1
- package/vona/packages-cli/cli-set-api/src/lib/bean/toolsBin/generateVonaMeta.ts +2 -10
- package/vona/packages-cli/cli-set-api/src/lib/bean/toolsBin/validateGenerationPaths.ts +27 -0
- package/vona/packages-cli/cli-set-api/test/binDevLifecycle.test.ts +1 -1
- package/vona/packages-cli/cli-set-api/test/strictGenerationPrivacy.test.ts +439 -0
- package/vona/packages-vona/vona/package.json +1 -1
- package/vona/packages-vona/vona-core/package.json +1 -1
- package/vona/packages-vona/vona-core/src/lib/bean/beanBase.ts +4 -0
- package/vona/packages-vona/vona-core/src/lib/core/asyncLocalStorage.ts +84 -12
- package/vona/packages-vona/vona-core/src/lib/core/index.ts +14 -0
- package/vona/packages-vona/vona-core/src/lib/core/logger/appLogger.ts +19 -3
- package/vona/packages-vona/vona-core/src/lib/core/logger/diagnosticLogger.test.ts +346 -0
- package/vona/packages-vona/vona-core/src/lib/core/logger/diagnosticLogger.ts +138 -0
- package/vona/packages-vona/vona-core/src/lib/core/logger/index.ts +1 -0
- package/vona/packages-vona/vona-core/src/lib/core/logger/utils.ts +25 -5
- package/vona/packages-vona/vona-core/src/lib/core/requestBoundary.ts +89 -0
- package/vona/packages-vona/vona-core/src/lib/core/testFreshness.test.ts +379 -0
- package/vona/packages-vona/vona-core/src/lib/core/testFreshness.ts +232 -0
- package/vona/packages-vona/vona-core/src/lib/core/testStrictMode.ts +9 -0
- package/vona/packages-vona/vona-core/src/lib/framework/start.test.ts +4 -3
- package/vona/packages-vona/vona-core/src/lib/framework/start.ts +2 -0
- package/vona/packages-vona/vona-mock/package.json +1 -1
- package/vona/pnpm-lock.yaml +691 -356
- package/vona/src/suite-vendor/a-vona/modules/a-core/package.json +1 -1
- package/vona/src/suite-vendor/a-vona/modules/a-executor/package.json +1 -1
- package/vona/src/suite-vendor/a-vona/modules/a-executor/src/bean/bean.executor.ts +111 -60
- package/vona/src/suite-vendor/a-vona/modules/a-executor/src/service/executor.ts +6 -6
- package/vona/src/suite-vendor/a-vona/modules/a-executor/src/types/executor.ts +13 -1
- package/vona/src/suite-vendor/a-vona/modules/a-jwt/package.json +1 -1
- package/vona/src/suite-vendor/a-vona/modules/a-jwt/src/lib/jwtClientLogging.test.ts +182 -0
- package/vona/src/suite-vendor/a-vona/modules/a-jwt/src/service/jwtClient.ts +2 -8
- package/vona/src/suite-vendor/a-vona/modules/a-logger/package.json +1 -1
- package/vona/src/suite-vendor/a-vona/modules/a-logger/src/bean/aopMethod.log.ts +4 -1
- package/vona/src/suite-vendor/a-vona/modules/a-logger/src/bean/bean.logger.ts +9 -0
- package/vona/src/suite-vendor/a-vona/modules/a-logger/src/bean/middlewareSystem.httpLog.ts +27 -14
- package/vona/src/suite-vendor/a-vona/modules/a-logger/src/lib/redactHttpLog.test.ts +207 -0
- package/vona/src/suite-vendor/a-vona/modules/a-logger/src/lib/redactHttpLog.ts +30 -0
- package/vona/src/suite-vendor/a-vona/modules/a-logger/test/middlewareSystem.httpLog.test.ts +33 -1
- package/vona/src/suite-vendor/a-vona/modules/a-ormdialect/package.json +1 -1
- package/vona/src/suite-vendor/a-vona/modules/a-ormdialect/src/index.ts +11 -0
- package/vona/src/suite-vendor/a-vona/modules/a-ormdialect/src/lib/protectedSqliteRegistry.ts +356 -0
- package/vona/src/suite-vendor/a-vona/modules/a-startup/package.json +1 -1
- package/vona/src/suite-vendor/a-vona/modules/a-startup/src/service/startup.ts +6 -0
- package/vona/src/suite-vendor/a-vona/modules/a-startup/test/devSeed.test.ts +2 -2
- package/vona/src/suite-vendor/a-vona/modules/a-telemetry/package.json +1 -1
- package/vona/src/suite-vendor/a-vona/modules/a-telemetry/src/bean/bean.telemetry.ts +8 -1
- package/vona/src/suite-vendor/a-vona/modules/a-telemetry/src/bean/middlewareSystem.trace.ts +9 -14
- package/vona/src/suite-vendor/a-vona/modules/a-telemetry/src/lib/spanDiagnostics.ts +70 -0
- package/vona/src/suite-vendor/a-vona/modules/a-telemetry/src/service/telemetry.ts +37 -23
- package/vona/src/suite-vendor/a-vona/modules/a-version/package.json +1 -1
- package/vona/src/suite-vendor/a-vona/modules/a-version/src/service/database.ts +31 -4
- package/vona/src/suite-vendor/a-vona/package.json +2 -2
- package/zova/package.original.json +3 -2
- package/zova/packages-cli/cli/package.json +2 -2
- package/zova/packages-cli/cli-set-front/package.json +1 -1
- package/zova/packages-cli/cli-set-front/src/lib/bean/cli.bin.buildRest.ts +11 -101
- package/zova/packages-cli/cli-set-front/src/lib/common/restDependencies.ts +78 -0
- package/zova/packages-cli/cli-set-front/src/lib/common/restDependencyVersions.ts +252 -0
- package/zova/packages-cli/cli-set-front/test/restDependencies.test.ts +568 -0
- package/zova/pnpm-lock.yaml +442 -564
|
@@ -0,0 +1,356 @@
|
|
|
1
|
+
import { createRequire } from 'node:module';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { getRequestBoundary } from 'vona';
|
|
4
|
+
|
|
5
|
+
const MAX_NAME_BYTES = 256;
|
|
6
|
+
const MAX_CONFIG_BYTES = 65_536;
|
|
7
|
+
const LOOKUP_BUDGET_MS = 1_000;
|
|
8
|
+
const PRIVATE_TIMEOUT_MS = 100;
|
|
9
|
+
|
|
10
|
+
export const PROTECTED_SQLITE_REGISTRY_SQL = `SELECT
|
|
11
|
+
CASE WHEN typeof(id) = 'integer' AND id > 0 AND id <= 9007199254740991 THEN id ELSE NULL END AS id,
|
|
12
|
+
CASE WHEN typeof(name) = 'text' AND length(CAST(name AS BLOB)) <= 256 THEN name ELSE NULL END AS name,
|
|
13
|
+
CASE WHEN typeof(disabled) = 'integer' AND disabled IN (0, 1) THEN disabled ELSE NULL END AS disabled,
|
|
14
|
+
CASE WHEN typeof(isolate) = 'integer' AND isolate IN (0, 1) THEN isolate ELSE NULL END AS isolate,
|
|
15
|
+
CASE
|
|
16
|
+
WHEN config IS NULL THEN NULL
|
|
17
|
+
WHEN typeof(config) = 'text' AND length(CAST(config AS BLOB)) <= 65536 THEN config
|
|
18
|
+
ELSE NULL
|
|
19
|
+
END AS config,
|
|
20
|
+
CASE
|
|
21
|
+
WHEN typeof(id) = 'integer' AND id > 0 AND id <= 9007199254740991
|
|
22
|
+
AND typeof(name) = 'text' AND length(CAST(name AS BLOB)) <= 256
|
|
23
|
+
AND typeof(disabled) = 'integer' AND disabled IN (0, 1)
|
|
24
|
+
AND typeof(isolate) = 'integer' AND isolate IN (0, 1)
|
|
25
|
+
AND (config IS NULL OR (typeof(config) = 'text' AND length(CAST(config AS BLOB)) <= 65536))
|
|
26
|
+
THEN 1
|
|
27
|
+
ELSE 0
|
|
28
|
+
END AS __protected_valid
|
|
29
|
+
FROM aInstance
|
|
30
|
+
WHERE name COLLATE BINARY = ?
|
|
31
|
+
AND deleted = 0
|
|
32
|
+
LIMIT 2`;
|
|
33
|
+
|
|
34
|
+
type ProtectedSqliteState = 'new' | 'ready' | 'faulted' | 'closed';
|
|
35
|
+
|
|
36
|
+
type ProtectedSqlitePrepareOutcome =
|
|
37
|
+
| 'ready'
|
|
38
|
+
| 'already_prepared'
|
|
39
|
+
| 'boundary_required'
|
|
40
|
+
| 'unsupported'
|
|
41
|
+
| 'unavailable';
|
|
42
|
+
|
|
43
|
+
export interface ProtectedSqliteRegistryOptions {
|
|
44
|
+
readonly filename: string;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export interface ProtectedSqliteRegistryRow {
|
|
48
|
+
readonly id: number;
|
|
49
|
+
readonly name: string;
|
|
50
|
+
readonly disabled: boolean;
|
|
51
|
+
readonly isolate: boolean;
|
|
52
|
+
readonly config: string | null;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export interface ProtectedSqliteRegistryPrepareResult {
|
|
56
|
+
readonly outcome: ProtectedSqlitePrepareOutcome;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export type ProtectedSqliteRegistryLookupOutcome =
|
|
60
|
+
| 'found'
|
|
61
|
+
| 'missing'
|
|
62
|
+
| 'duplicate'
|
|
63
|
+
| 'disabled'
|
|
64
|
+
| 'invalid_input'
|
|
65
|
+
| 'expired'
|
|
66
|
+
| 'boundary_required'
|
|
67
|
+
| 'reentrant'
|
|
68
|
+
| 'unavailable';
|
|
69
|
+
|
|
70
|
+
export type ProtectedSqliteRegistryLookupResult =
|
|
71
|
+
| Readonly<{
|
|
72
|
+
readonly outcome: 'found';
|
|
73
|
+
readonly row: ProtectedSqliteRegistryRow;
|
|
74
|
+
}>
|
|
75
|
+
| Readonly<{ readonly outcome: Exclude<ProtectedSqliteRegistryLookupOutcome, 'found'> }>;
|
|
76
|
+
|
|
77
|
+
export interface ProtectedSqliteRegistryCloseResult {
|
|
78
|
+
readonly outcome: 'closed' | 'close_failed';
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export interface ProtectedSqliteRegistrySnapshot {
|
|
82
|
+
readonly state: ProtectedSqliteState;
|
|
83
|
+
readonly active: boolean;
|
|
84
|
+
readonly closing: boolean;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export interface ProtectedSqliteRegistry {
|
|
88
|
+
readonly prepare: () => ProtectedSqliteRegistryPrepareResult;
|
|
89
|
+
readonly lookup: (name: string, deadlineAt?: number) => ProtectedSqliteRegistryLookupResult;
|
|
90
|
+
readonly close: () => Promise<ProtectedSqliteRegistryCloseResult>;
|
|
91
|
+
readonly snapshot: () => ProtectedSqliteRegistrySnapshot;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
interface ProtectedSqliteStatement {
|
|
95
|
+
all(name: string): readonly unknown[];
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
interface ProtectedSqliteDatabase {
|
|
99
|
+
pragma(sql: string, options?: Readonly<{ readonly simple: true }>): unknown;
|
|
100
|
+
prepare(sql: string): ProtectedSqliteStatement;
|
|
101
|
+
close(): void;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
interface ProtectedSqliteOpenOptions {
|
|
105
|
+
readonly readonly: true;
|
|
106
|
+
readonly fileMustExist: true;
|
|
107
|
+
readonly timeout: 100;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
type ProtectedSqliteOpen = (
|
|
111
|
+
filename: string,
|
|
112
|
+
options: ProtectedSqliteOpenOptions,
|
|
113
|
+
) => ProtectedSqliteDatabase;
|
|
114
|
+
|
|
115
|
+
interface ProtectedSqliteDependencies {
|
|
116
|
+
readonly open: ProtectedSqliteOpen;
|
|
117
|
+
readonly now: () => number;
|
|
118
|
+
readonly hasRequestBoundary: () => boolean;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const productionDependencies: ProtectedSqliteDependencies = Object.freeze({
|
|
122
|
+
open: openNativeSqlite,
|
|
123
|
+
now: () => performance.now(),
|
|
124
|
+
hasRequestBoundary: () => getRequestBoundary() !== undefined,
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
export function createProtectedSqliteRegistry(
|
|
128
|
+
options: ProtectedSqliteRegistryOptions,
|
|
129
|
+
): ProtectedSqliteRegistry {
|
|
130
|
+
return createProtectedSqliteRegistryInternal(options, productionDependencies);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// This seam is intentionally not exported by the package barrel. It makes lifecycle and
|
|
134
|
+
// diagnostic-containment tests deterministic without accepting an alternate production driver.
|
|
135
|
+
export function createProtectedSqliteRegistryForTest(
|
|
136
|
+
options: unknown,
|
|
137
|
+
dependencies: Readonly<
|
|
138
|
+
Partial<ProtectedSqliteDependencies> & { readonly open: ProtectedSqliteOpen }
|
|
139
|
+
>,
|
|
140
|
+
): ProtectedSqliteRegistry {
|
|
141
|
+
return createProtectedSqliteRegistryInternal(options, {
|
|
142
|
+
open: dependencies.open,
|
|
143
|
+
now: dependencies.now ?? (() => performance.now()),
|
|
144
|
+
hasRequestBoundary:
|
|
145
|
+
dependencies.hasRequestBoundary ?? (() => getRequestBoundary() !== undefined),
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export function createProtectedSqliteRegistryInternal(
|
|
150
|
+
options: unknown,
|
|
151
|
+
dependencies: ProtectedSqliteDependencies,
|
|
152
|
+
): ProtectedSqliteRegistry {
|
|
153
|
+
const filename = readSupportedFilename(options);
|
|
154
|
+
let state: ProtectedSqliteState = 'new';
|
|
155
|
+
let attempted = false;
|
|
156
|
+
let active = false;
|
|
157
|
+
let closing = false;
|
|
158
|
+
let database: ProtectedSqliteDatabase | undefined;
|
|
159
|
+
let statement: ProtectedSqliteStatement | undefined;
|
|
160
|
+
let closePromise: Promise<ProtectedSqliteRegistryCloseResult> | undefined;
|
|
161
|
+
let closeResolve: ((result: ProtectedSqliteRegistryCloseResult) => void) | undefined;
|
|
162
|
+
|
|
163
|
+
const unavailable = (outcome: Exclude<ProtectedSqliteRegistryLookupOutcome, 'found'>) =>
|
|
164
|
+
Object.freeze({ outcome }) as ProtectedSqliteRegistryLookupResult;
|
|
165
|
+
|
|
166
|
+
const finalizeClose = (): ProtectedSqliteRegistryCloseResult => {
|
|
167
|
+
if (!database) {
|
|
168
|
+
state = 'closed';
|
|
169
|
+
return Object.freeze({ outcome: 'closed' });
|
|
170
|
+
}
|
|
171
|
+
try {
|
|
172
|
+
database.close();
|
|
173
|
+
} catch {
|
|
174
|
+
state = 'faulted';
|
|
175
|
+
return Object.freeze({ outcome: 'close_failed' });
|
|
176
|
+
}
|
|
177
|
+
database = undefined;
|
|
178
|
+
statement = undefined;
|
|
179
|
+
state = 'closed';
|
|
180
|
+
return Object.freeze({ outcome: 'closed' });
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
const settleDeferredClose = () => {
|
|
184
|
+
if (!closeResolve) return;
|
|
185
|
+
const resolve = closeResolve;
|
|
186
|
+
closeResolve = undefined;
|
|
187
|
+
resolve(finalizeClose());
|
|
188
|
+
};
|
|
189
|
+
|
|
190
|
+
const prepare = (): ProtectedSqliteRegistryPrepareResult => {
|
|
191
|
+
if (dependencies.hasRequestBoundary()) return Object.freeze({ outcome: 'boundary_required' });
|
|
192
|
+
if (closing || state === 'closed') return Object.freeze({ outcome: 'unavailable' });
|
|
193
|
+
if (attempted) return Object.freeze({ outcome: 'already_prepared' });
|
|
194
|
+
attempted = true;
|
|
195
|
+
if (!filename) {
|
|
196
|
+
state = 'faulted';
|
|
197
|
+
return Object.freeze({ outcome: 'unsupported' });
|
|
198
|
+
}
|
|
199
|
+
try {
|
|
200
|
+
database = dependencies.open(filename, {
|
|
201
|
+
readonly: true,
|
|
202
|
+
fileMustExist: true,
|
|
203
|
+
timeout: PRIVATE_TIMEOUT_MS,
|
|
204
|
+
});
|
|
205
|
+
const journalMode = database.pragma('journal_mode', { simple: true });
|
|
206
|
+
const busyTimeout = database.pragma('busy_timeout', { simple: true });
|
|
207
|
+
if (journalMode !== 'wal' || busyTimeout !== PRIVATE_TIMEOUT_MS) throw new Error();
|
|
208
|
+
statement = database.prepare(PROTECTED_SQLITE_REGISTRY_SQL);
|
|
209
|
+
const probe = statement.all('');
|
|
210
|
+
if (
|
|
211
|
+
!Array.isArray(probe) ||
|
|
212
|
+
probe.length > 2 ||
|
|
213
|
+
probe.some(row => !isValidProjectionRow(row))
|
|
214
|
+
) {
|
|
215
|
+
throw new Error();
|
|
216
|
+
}
|
|
217
|
+
state = 'ready';
|
|
218
|
+
return Object.freeze({ outcome: 'ready' });
|
|
219
|
+
} catch {
|
|
220
|
+
state = 'faulted';
|
|
221
|
+
return Object.freeze({ outcome: 'unavailable' });
|
|
222
|
+
}
|
|
223
|
+
};
|
|
224
|
+
|
|
225
|
+
const lookup = (name: string, deadlineAt = dependencies.now() + LOOKUP_BUDGET_MS) => {
|
|
226
|
+
if (!dependencies.hasRequestBoundary()) return unavailable('boundary_required');
|
|
227
|
+
if (state !== 'ready' || closing || !database || !statement) return unavailable('unavailable');
|
|
228
|
+
if (!isBoundedText(name, MAX_NAME_BYTES)) {
|
|
229
|
+
return unavailable('invalid_input');
|
|
230
|
+
}
|
|
231
|
+
if (!Number.isFinite(deadlineAt) || dependencies.now() >= deadlineAt) {
|
|
232
|
+
return unavailable('expired');
|
|
233
|
+
}
|
|
234
|
+
if (active) return unavailable('reentrant');
|
|
235
|
+
active = true;
|
|
236
|
+
try {
|
|
237
|
+
const rows = statement.all(name);
|
|
238
|
+
if (closing || dependencies.now() >= deadlineAt) return unavailable('expired');
|
|
239
|
+
if (!Array.isArray(rows)) {
|
|
240
|
+
state = 'faulted';
|
|
241
|
+
return unavailable('unavailable');
|
|
242
|
+
}
|
|
243
|
+
if (rows.length > 2) {
|
|
244
|
+
state = 'faulted';
|
|
245
|
+
return unavailable('unavailable');
|
|
246
|
+
}
|
|
247
|
+
if (rows.length === 0) return unavailable('missing');
|
|
248
|
+
if (rows.length === 2) return unavailable('duplicate');
|
|
249
|
+
const row = rows[0];
|
|
250
|
+
if (!isValidProjectionRow(row)) {
|
|
251
|
+
state = 'faulted';
|
|
252
|
+
return unavailable('unavailable');
|
|
253
|
+
}
|
|
254
|
+
const projected = row as Record<string, unknown>;
|
|
255
|
+
if (projected.disabled === 1) return unavailable('disabled');
|
|
256
|
+
const result = Object.freeze({
|
|
257
|
+
outcome: 'found' as const,
|
|
258
|
+
row: Object.freeze({
|
|
259
|
+
id: projected.id as number,
|
|
260
|
+
name: projected.name as string,
|
|
261
|
+
disabled: projected.disabled === 1,
|
|
262
|
+
isolate: projected.isolate === 1,
|
|
263
|
+
config: projected.config as string | null,
|
|
264
|
+
}),
|
|
265
|
+
});
|
|
266
|
+
return result;
|
|
267
|
+
} catch {
|
|
268
|
+
state = 'faulted';
|
|
269
|
+
return unavailable('unavailable');
|
|
270
|
+
} finally {
|
|
271
|
+
active = false;
|
|
272
|
+
if (closing) settleDeferredClose();
|
|
273
|
+
}
|
|
274
|
+
};
|
|
275
|
+
|
|
276
|
+
const close = () => {
|
|
277
|
+
if (closePromise) return closePromise;
|
|
278
|
+
closing = true;
|
|
279
|
+
if (active) {
|
|
280
|
+
closePromise = new Promise<ProtectedSqliteRegistryCloseResult>(resolve => {
|
|
281
|
+
closeResolve = resolve;
|
|
282
|
+
});
|
|
283
|
+
} else {
|
|
284
|
+
closePromise = Promise.resolve(finalizeClose());
|
|
285
|
+
}
|
|
286
|
+
return closePromise;
|
|
287
|
+
};
|
|
288
|
+
|
|
289
|
+
return Object.freeze({
|
|
290
|
+
prepare,
|
|
291
|
+
lookup,
|
|
292
|
+
close,
|
|
293
|
+
snapshot: () => Object.freeze({ state, active, closing }),
|
|
294
|
+
});
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
function readSupportedFilename(options: unknown): string | undefined {
|
|
298
|
+
try {
|
|
299
|
+
if (!options || typeof options !== 'object' || Array.isArray(options)) return undefined;
|
|
300
|
+
const prototype = Object.getPrototypeOf(options);
|
|
301
|
+
if (prototype !== Object.prototype && prototype !== null) return undefined;
|
|
302
|
+
const keys = Reflect.ownKeys(options);
|
|
303
|
+
if (keys.length !== 1 || keys[0] !== 'filename') return undefined;
|
|
304
|
+
const descriptor = Object.getOwnPropertyDescriptor(options, 'filename');
|
|
305
|
+
if (!descriptor || !Object.hasOwn(descriptor, 'value')) return undefined;
|
|
306
|
+
const filename: unknown = descriptor.value;
|
|
307
|
+
if (
|
|
308
|
+
typeof filename !== 'string' ||
|
|
309
|
+
!filename.isWellFormed() ||
|
|
310
|
+
!path.isAbsolute(filename) ||
|
|
311
|
+
filename.includes('\0')
|
|
312
|
+
) {
|
|
313
|
+
return undefined;
|
|
314
|
+
}
|
|
315
|
+
return filename;
|
|
316
|
+
} catch {
|
|
317
|
+
return undefined;
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
function isBoundedText(value: unknown, maxBytes: number): value is string {
|
|
322
|
+
return (
|
|
323
|
+
typeof value === 'string' &&
|
|
324
|
+
value.isWellFormed() &&
|
|
325
|
+
Buffer.byteLength(value, 'utf8') <= maxBytes
|
|
326
|
+
);
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
function isValidProjectionRow(row: unknown): boolean {
|
|
330
|
+
if (!row || typeof row !== 'object') return false;
|
|
331
|
+
const projected = row as Record<string, unknown>;
|
|
332
|
+
if (projected.__protected_valid !== 1) return false;
|
|
333
|
+
if (
|
|
334
|
+
typeof projected.id !== 'number' ||
|
|
335
|
+
!Number.isSafeInteger(projected.id) ||
|
|
336
|
+
projected.id <= 0
|
|
337
|
+
) {
|
|
338
|
+
return false;
|
|
339
|
+
}
|
|
340
|
+
if (!isBoundedText(projected.name, MAX_NAME_BYTES)) return false;
|
|
341
|
+
if (projected.disabled !== 0 && projected.disabled !== 1) return false;
|
|
342
|
+
if (projected.isolate !== 0 && projected.isolate !== 1) return false;
|
|
343
|
+
return projected.config === null || isBoundedText(projected.config, MAX_CONFIG_BYTES);
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
function openNativeSqlite(
|
|
347
|
+
filename: string,
|
|
348
|
+
options: ProtectedSqliteOpenOptions,
|
|
349
|
+
): ProtectedSqliteDatabase {
|
|
350
|
+
const require = createRequire(import.meta.url);
|
|
351
|
+
const BetterSqlite3 = require('better-sqlite3') as new (
|
|
352
|
+
filename: string,
|
|
353
|
+
options: ProtectedSqliteOpenOptions,
|
|
354
|
+
) => ProtectedSqliteDatabase;
|
|
355
|
+
return new BetterSqlite3(filename, options);
|
|
356
|
+
}
|
|
@@ -5,6 +5,8 @@ import { isNil } from '@cabloy/utils';
|
|
|
5
5
|
import fse from 'fs-extra';
|
|
6
6
|
import {
|
|
7
7
|
assertIsolatedTestConfig,
|
|
8
|
+
assertFreshTestResourcesReady,
|
|
9
|
+
requiresFreshTestResources,
|
|
8
10
|
BeanBase,
|
|
9
11
|
cast,
|
|
10
12
|
getRuntimePathPhysicalRoot,
|
|
@@ -242,6 +244,10 @@ export class ServiceStartup extends BeanBase {
|
|
|
242
244
|
private async _clearResources() {
|
|
243
245
|
if (!this.app.meta.isTest) return;
|
|
244
246
|
assertIsolatedTestConfig(this.app);
|
|
247
|
+
if (requiresFreshTestResources()) {
|
|
248
|
+
assertFreshTestResourcesReady(this.app);
|
|
249
|
+
return;
|
|
250
|
+
}
|
|
245
251
|
// redis
|
|
246
252
|
await this.$scope.redis.service.redis.clearAllData();
|
|
247
253
|
// .app/public
|
|
@@ -217,7 +217,7 @@ describe('creation-driven dev seed', { concurrency: false }, () => {
|
|
|
217
217
|
const { service, app, calls, pending } = fixture(['', 'isolateTest']);
|
|
218
218
|
const runStartup = service.runStartup.bind(service);
|
|
219
219
|
service.runStartup = async (name, instance, options) => {
|
|
220
|
-
if (name === 'a-version:instanceInit' && instance === 'isolateTest') {
|
|
220
|
+
if (name === 'a-version:instanceInit' && String(instance) === 'isolateTest') {
|
|
221
221
|
throw new Error('isolate init failed');
|
|
222
222
|
}
|
|
223
223
|
await runStartup(name, instance, options);
|
|
@@ -239,7 +239,7 @@ describe('creation-driven dev seed', { concurrency: false }, () => {
|
|
|
239
239
|
} else {
|
|
240
240
|
const runStartup = service.runStartup.bind(service);
|
|
241
241
|
service.runStartup = async (name, instance, options) => {
|
|
242
|
-
if (name === 'a-version:instanceInit' && instance === 'isolateTest') {
|
|
242
|
+
if (name === 'a-version:instanceInit' && String(instance) === 'isolateTest') {
|
|
243
243
|
throw new Error('isolate init failed');
|
|
244
244
|
}
|
|
245
245
|
await runStartup(name, instance, options);
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { Span, SpanOptions } from '@opentelemetry/api';
|
|
2
2
|
|
|
3
3
|
import { INVALID_SPAN_CONTEXT, trace } from '@opentelemetry/api';
|
|
4
|
-
import { BeanBase } from 'vona';
|
|
4
|
+
import { BeanBase, getRequestBoundary } from 'vona';
|
|
5
5
|
import { Bean } from 'vona-module-a-bean';
|
|
6
6
|
|
|
7
7
|
const nonRecordingSpan = trace.wrapSpanContext(INVALID_SPAN_CONTEXT);
|
|
@@ -9,25 +9,30 @@ const nonRecordingSpan = trace.wrapSpanContext(INVALID_SPAN_CONTEXT);
|
|
|
9
9
|
@Bean()
|
|
10
10
|
export class BeanTelemetry extends BeanBase {
|
|
11
11
|
get enabled() {
|
|
12
|
+
if (getRequestBoundary()) return false;
|
|
12
13
|
return this.scope.service.telemetry.enabled;
|
|
13
14
|
}
|
|
14
15
|
|
|
15
16
|
startSpan(name: string, options?: SpanOptions): Span {
|
|
17
|
+
if (getRequestBoundary()) return nonRecordingSpan;
|
|
16
18
|
const telemetry = this.scope.service.telemetry;
|
|
17
19
|
return telemetry.enabled ? telemetry.startSpan(name, options) : nonRecordingSpan;
|
|
18
20
|
}
|
|
19
21
|
|
|
20
22
|
withSpan<RESULT>(span: Span, fn: () => RESULT): RESULT {
|
|
23
|
+
if (getRequestBoundary()) return fn();
|
|
21
24
|
const telemetry = this.scope.service.telemetry;
|
|
22
25
|
return telemetry.enabled ? telemetry.withSpan(span, fn) : fn();
|
|
23
26
|
}
|
|
24
27
|
|
|
25
28
|
recordException(span: Span, error: unknown) {
|
|
29
|
+
if (getRequestBoundary()) return;
|
|
26
30
|
const telemetry = this.scope.service.telemetry;
|
|
27
31
|
if (telemetry.enabled) telemetry.recordException(span, error);
|
|
28
32
|
}
|
|
29
33
|
|
|
30
34
|
withNamedSpan<RESULT>(name: string, fn: () => RESULT, options?: SpanOptions): RESULT {
|
|
35
|
+
if (getRequestBoundary()) return fn();
|
|
31
36
|
const span = this.startSpan(name, options);
|
|
32
37
|
try {
|
|
33
38
|
const result = this.withSpan(span, fn);
|
|
@@ -52,6 +57,7 @@ export class BeanTelemetry extends BeanBase {
|
|
|
52
57
|
}
|
|
53
58
|
|
|
54
59
|
private _recordAndEndSpan(span: Span, error: unknown) {
|
|
60
|
+
if (getRequestBoundary()) return;
|
|
55
61
|
try {
|
|
56
62
|
this.recordException(span, error);
|
|
57
63
|
} catch {}
|
|
@@ -59,6 +65,7 @@ export class BeanTelemetry extends BeanBase {
|
|
|
59
65
|
}
|
|
60
66
|
|
|
61
67
|
private _endSpan(span: Span) {
|
|
68
|
+
if (getRequestBoundary()) return;
|
|
62
69
|
try {
|
|
63
70
|
span.end();
|
|
64
71
|
} catch {}
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import type { Span } from '@opentelemetry/api';
|
|
2
1
|
import type { Next } from 'vona';
|
|
3
2
|
import type {
|
|
4
3
|
IDecoratorMiddlewareSystemOptions,
|
|
@@ -6,9 +5,11 @@ import type {
|
|
|
6
5
|
} from 'vona-module-a-aspect';
|
|
7
6
|
|
|
8
7
|
import { context } from '@opentelemetry/api';
|
|
9
|
-
import { BeanBase, uuidv4 } from 'vona';
|
|
8
|
+
import { BeanBase, captureRequestBoundary, getRequestBoundary, uuidv4 } from 'vona';
|
|
10
9
|
import { MiddlewareSystem } from 'vona-module-a-aspect';
|
|
11
10
|
|
|
11
|
+
import { createHttpSpanEnd, recordSpanException } from '../lib/spanDiagnostics.ts';
|
|
12
|
+
|
|
12
13
|
export interface IMiddlewareSystemOptionsTrace extends IDecoratorMiddlewareSystemOptions {}
|
|
13
14
|
|
|
14
15
|
@MiddlewareSystem<IMiddlewareSystemOptionsTrace>({
|
|
@@ -16,6 +17,7 @@ export interface IMiddlewareSystemOptionsTrace extends IDecoratorMiddlewareSyste
|
|
|
16
17
|
})
|
|
17
18
|
export class MiddlewareSystemTrace extends BeanBase implements IMiddlewareSystemExecute {
|
|
18
19
|
async execute(_options: IMiddlewareSystemOptionsTrace, next: Next) {
|
|
20
|
+
if (getRequestBoundary()) return next();
|
|
19
21
|
const telemetry = this.$scope.telemetry.service.telemetry;
|
|
20
22
|
if (!telemetry.enabled || this.ctx.state.telemetry?.internalAction) return next();
|
|
21
23
|
|
|
@@ -37,11 +39,10 @@ export class MiddlewareSystemTrace extends BeanBase implements IMiddlewareSystem
|
|
|
37
39
|
ctx.state.telemetry = { requestId, context: active, serverSpan: span };
|
|
38
40
|
ctx.set(requestIdHeader, requestId);
|
|
39
41
|
|
|
40
|
-
const
|
|
41
|
-
const
|
|
42
|
-
|
|
43
|
-
ctx.res.once('
|
|
44
|
-
ctx.res.once('close', close);
|
|
42
|
+
const capture = captureRequestBoundary();
|
|
43
|
+
const terminal = createHttpSpanEnd(span, ctx.res, capture);
|
|
44
|
+
ctx.res.once('finish', terminal.finish);
|
|
45
|
+
ctx.res.once('close', terminal.close);
|
|
45
46
|
|
|
46
47
|
try {
|
|
47
48
|
const result = await context.with(active, next);
|
|
@@ -55,16 +56,10 @@ export class MiddlewareSystemTrace extends BeanBase implements IMiddlewareSystem
|
|
|
55
56
|
}
|
|
56
57
|
return result;
|
|
57
58
|
} catch (err) {
|
|
58
|
-
|
|
59
|
+
recordSpanException(span, err, capture);
|
|
59
60
|
throw err;
|
|
60
61
|
}
|
|
61
62
|
}
|
|
62
|
-
|
|
63
|
-
private _end(span: Span, statusCode: number, aborted: boolean) {
|
|
64
|
-
if (!span || this.ctx.state.telemetry?.serverSpanEnded) return;
|
|
65
|
-
this.ctx.state.telemetry!.serverSpanEnded = true;
|
|
66
|
-
this.$scope.telemetry.service.telemetry.endHttpSpan(span, statusCode, aborted);
|
|
67
|
-
}
|
|
68
63
|
}
|
|
69
64
|
|
|
70
65
|
function normalizeRequestId(value?: string) {
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import type { Span } from '@opentelemetry/api';
|
|
2
|
+
import type { RequestBoundaryCapture } from 'vona';
|
|
3
|
+
|
|
4
|
+
import { SpanStatusCode } from '@opentelemetry/api';
|
|
5
|
+
import { captureRequestBoundary, getRequestBoundary } from 'vona';
|
|
6
|
+
|
|
7
|
+
export interface HttpSpanResponse {
|
|
8
|
+
readonly statusCode: number;
|
|
9
|
+
readonly writableEnded: boolean;
|
|
10
|
+
removeListener: (event: 'finish' | 'close', listener: () => void) => unknown;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function recordSpanException(
|
|
14
|
+
span: Span,
|
|
15
|
+
error: unknown,
|
|
16
|
+
capture?: RequestBoundaryCapture,
|
|
17
|
+
): void {
|
|
18
|
+
if (capture || getRequestBoundary()) return;
|
|
19
|
+
if (error instanceof Error) {
|
|
20
|
+
span.recordException(error);
|
|
21
|
+
span.setStatus({ code: SpanStatusCode.ERROR, message: error.message });
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
span.setStatus({ code: SpanStatusCode.ERROR, message: String(error) });
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function endHttpSpan(span: Span, statusCode: number, aborted = false): void {
|
|
28
|
+
if (getRequestBoundary()) return;
|
|
29
|
+
if (aborted || statusCode >= 500) {
|
|
30
|
+
span.setStatus({
|
|
31
|
+
code: SpanStatusCode.ERROR,
|
|
32
|
+
message: aborted ? 'client aborted' : undefined,
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
span.setAttribute('http.response.status_code', statusCode);
|
|
36
|
+
if (aborted) span.setAttribute('vona.http.aborted', true);
|
|
37
|
+
span.end();
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function createHttpSpanEnd(
|
|
41
|
+
span: Span,
|
|
42
|
+
response: HttpSpanResponse,
|
|
43
|
+
capture?: RequestBoundaryCapture,
|
|
44
|
+
): Readonly<{ finish(): void; close(): void }> {
|
|
45
|
+
capture ??= captureRequestBoundary();
|
|
46
|
+
const reportFailure = () => capture?.reportFailure('late_callback_failed');
|
|
47
|
+
let ended = false;
|
|
48
|
+
const finish = () => end(false);
|
|
49
|
+
const close = () => end(true);
|
|
50
|
+
function end(closed: boolean) {
|
|
51
|
+
if (ended) return;
|
|
52
|
+
ended = true;
|
|
53
|
+
try {
|
|
54
|
+
response.removeListener(closed ? 'finish' : 'close', closed ? finish : close);
|
|
55
|
+
} catch {
|
|
56
|
+
reportFailure();
|
|
57
|
+
}
|
|
58
|
+
if (capture || getRequestBoundary()) return;
|
|
59
|
+
try {
|
|
60
|
+
endHttpSpan(
|
|
61
|
+
span,
|
|
62
|
+
response.statusCode || (closed ? 499 : 500),
|
|
63
|
+
closed && !response.writableEnded,
|
|
64
|
+
);
|
|
65
|
+
} catch {
|
|
66
|
+
reportFailure();
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
return Object.freeze({ finish, close });
|
|
70
|
+
}
|