cabloy 5.1.220 → 5.1.222
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 +2 -2
- 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 +22 -0
- package/package.json +4 -3
- package/repo-agent-governance/managed-assets.json +15 -15
- package/repo-agent-governance/policies/repository.md +1 -0
- package/repo-agent-governance/skills/cabloy-backend-scaffold/SKILL.md +5 -1
- package/repo-agent-governance/skills/cabloy-backend-scaffold/references/follow-up-checklist.md +7 -5
- package/repo-agent-governance/skills/cabloy-frontend-scaffold/SKILL.md +5 -1
- package/repo-agent-governance/skills/cabloy-frontend-scaffold/references/follow-up-checklist.md +6 -4
- package/repo-docs/backend/unit-testing.md +5 -1
- package/repo-docs/frontend/openapi-sdk-guide.md +2 -0
- package/repo-docs/frontend/scripts.md +8 -0
- package/repo-docs/reference/repo-scripts.md +5 -1
- package/scripts/generateOpenapiIsolated.ts +18 -1
- package/scripts/runIsolatedTest.test.ts +9 -1
- package/scripts/runIsolatedTest.ts +81 -18
- package/scripts/strictGenerationDiagnostics.test.ts +1009 -0
- package/scripts/strictGenerationDiagnostics.ts +61 -0
- package/vona/package.original.json +3 -2
- package/vona/packages-cli/cabloy-cli/package.json +1 -1
- 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-utils/process-helper/package.json +1 -1
- package/vona/packages-utils/process-helper/src/index.test.ts +62 -0
- package/vona/packages-utils/process-helper/src/index.ts +9 -4
- 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/core/completedReadiness.test.ts +558 -0
- package/vona/packages-vona/vona-core/src/lib/core/completedReadiness.ts +389 -0
- package/vona/packages-vona/vona-core/src/lib/core/context.ts +3 -0
- package/vona/packages-vona/vona-core/src/lib/core/index.ts +11 -0
- package/vona/packages-vona/vona-core/src/lib/core/logger/appLogger.ts +10 -2
- package/vona/packages-vona/vona-core/src/lib/core/logger/diagnosticLogger.test.ts +268 -68
- package/vona/packages-vona/vona-core/src/lib/core/meta.ts +5 -0
- package/vona/packages-vona/vona-core/src/lib/core/testFreshness.ts +3 -9
- 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 +10 -0
- package/vona/packages-vona/vona-core/src/lib/framework/start.ts +8 -0
- package/vona/packages-vona/vona-mock/package.json +1 -1
- package/vona/pnpm-lock.yaml +542 -236
- 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 +124 -4
- package/vona/src/suite-vendor/a-vona/modules/a-executor/src/types/executor.ts +7 -0
- package/vona/src/suite-vendor/a-vona/modules/a-executor/test/readyExisting.test.ts +423 -0
- package/vona/src/suite-vendor/a-vona/modules/a-executor/test/readyExistingCommit.test.ts +174 -0
- package/vona/src/suite-vendor/a-vona/modules/a-instance/package.json +1 -1
- package/vona/src/suite-vendor/a-vona/modules/a-instance/src/bean/bean.instance.ts +3 -1
- package/vona/src/suite-vendor/a-vona/modules/a-instance/src/lib/existingReadyAdmission.ts +125 -0
- package/vona/src/suite-vendor/a-vona/modules/a-instance/src/service/instance.ts +72 -25
- package/vona/src/suite-vendor/a-vona/modules/a-instance/test/existingReadyAdmission.test.ts +949 -0
- package/vona/src/suite-vendor/a-vona/modules/a-logger/package.json +1 -1
- package/vona/src/suite-vendor/a-vona/modules/a-logger/test/middlewareSystem.httpLog.test.ts +61 -59
- package/vona/src/suite-vendor/a-vona/modules/a-mail/package.json +1 -1
- package/vona/src/suite-vendor/a-vona/modules/a-mail/src/service/mail.ts +5 -0
- 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/test/devSeed.test.ts +61 -2
- package/vona/src/suite-vendor/a-vona/package.json +1 -1
- package/zova/package.original.json +3 -3
- package/zova/packages-cli/cli/package.json +4 -4
- package/zova/packages-cli/cli-set-front/package.json +2 -2
- package/zova/packages-cli/cli-set-front/src/lib/common/restDependencyVersions.ts +22 -1
- package/zova/packages-cli/cli-set-front/test/restDependencies.test.ts +137 -0
- package/zova/packages-zova/zova/package.json +2 -2
- package/zova/pnpm-lock.yaml +448 -641
- package/zova/src/suite-vendor/a-zova/modules/a-zova/package.json +2 -2
- package/zova/src/suite-vendor/a-zova/package.json +2 -2
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import assert from 'node:assert/strict';
|
|
2
2
|
import { readFileSync } from 'node:fs';
|
|
3
3
|
import { registerHooks } from 'node:module';
|
|
4
|
-
import { test } from 'node:test';
|
|
4
|
+
import { describe, test } from 'node:test';
|
|
5
5
|
import ts from 'typescript';
|
|
6
6
|
|
|
7
7
|
const middlewareUrl = new URL(
|
|
@@ -49,64 +49,66 @@ const hooks = registerHooks({
|
|
|
49
49
|
const { MiddlewareSystemHttpLog } = await import(middlewareUrl);
|
|
50
50
|
hooks.deregister();
|
|
51
51
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
52
|
+
describe('HTTP logger', { concurrency: false }, () => {
|
|
53
|
+
test('HTTP logger skips context and diagnostic access inside a request boundary', async () => {
|
|
54
|
+
const middleware = Object.create(MiddlewareSystemHttpLog.prototype);
|
|
55
|
+
for (const key of ['app', 'ctx', '$loggerChild']) {
|
|
56
|
+
Object.defineProperty(middleware, key, {
|
|
57
|
+
get() {
|
|
58
|
+
assert.fail('protected diagnostic access');
|
|
59
|
+
},
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
let calls = 0;
|
|
63
|
+
setBoundary({ diagnostics: 'suppress' });
|
|
64
|
+
try {
|
|
65
|
+
assert.equal(
|
|
66
|
+
await middleware.execute({}, async () => {
|
|
67
|
+
calls++;
|
|
68
|
+
return 23;
|
|
69
|
+
}),
|
|
70
|
+
23,
|
|
71
|
+
);
|
|
72
|
+
assert.equal(calls, 1);
|
|
73
|
+
} finally {
|
|
74
|
+
setBoundary(undefined);
|
|
75
|
+
}
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
for (const path of ['/health/ready', '/health/test-stop']) {
|
|
79
|
+
test(`HTTP logger redacts request authorization and response proof at ${path} without mutating headers`, async () => {
|
|
80
|
+
const headers = {
|
|
81
|
+
'x-vona-test-challenge': 'secret-challenge',
|
|
82
|
+
'X-Vona-Test-Stop-Authorization': 'secret-authorization',
|
|
83
|
+
'x-vona-test-stop-nonce': 'secret-nonce',
|
|
84
|
+
'accept': 'application/json',
|
|
85
|
+
};
|
|
86
|
+
const responseHeaders = {
|
|
87
|
+
'x-vona-test-proof': 'secret-readiness',
|
|
88
|
+
'X-Vona-Test-Stop-Proof': 'secret-ack',
|
|
89
|
+
'content-type': 'application/json',
|
|
90
|
+
};
|
|
91
|
+
const logs: string[] = [];
|
|
92
|
+
const middleware = Object.create(MiddlewareSystemHttpLog.prototype);
|
|
93
|
+
Object.assign(middleware, {
|
|
94
|
+
app: { meta: { env: { VONA_TEST_RUN_ID: '20261014010203-a1b2c3d4' } } },
|
|
95
|
+
ctx: {
|
|
96
|
+
path,
|
|
97
|
+
req: { url: path, headers },
|
|
98
|
+
res: { statusCode: 200, getHeaders: () => responseHeaders },
|
|
99
|
+
},
|
|
100
|
+
$loggerChild: () => ({
|
|
101
|
+
http: (message: () => string) => logs.push(message()),
|
|
102
|
+
startTimer: () => ({ done: ({ message }: { message: string }) => logs.push(message) }),
|
|
103
|
+
}),
|
|
104
|
+
});
|
|
105
|
+
await middleware.execute({}, async () => {});
|
|
106
|
+
assert.equal(logs.length, 2);
|
|
107
|
+
assert.equal(logs.join('\n').includes('secret-'), false);
|
|
108
|
+
assert.match(logs[0], /application\/json/);
|
|
109
|
+
assert.match(logs[1], /application\/json/);
|
|
110
|
+
assert.equal(headers['X-Vona-Test-Stop-Authorization'], 'secret-authorization');
|
|
111
|
+
assert.equal(responseHeaders['X-Vona-Test-Stop-Proof'], 'secret-ack');
|
|
59
112
|
});
|
|
60
113
|
}
|
|
61
|
-
let calls = 0;
|
|
62
|
-
setBoundary({ diagnostics: 'suppress' });
|
|
63
|
-
try {
|
|
64
|
-
assert.equal(
|
|
65
|
-
await middleware.execute({}, async () => {
|
|
66
|
-
calls++;
|
|
67
|
-
return 23;
|
|
68
|
-
}),
|
|
69
|
-
23,
|
|
70
|
-
);
|
|
71
|
-
assert.equal(calls, 1);
|
|
72
|
-
} finally {
|
|
73
|
-
setBoundary(undefined);
|
|
74
|
-
}
|
|
75
114
|
});
|
|
76
|
-
|
|
77
|
-
for (const path of ['/health/ready', '/health/test-stop']) {
|
|
78
|
-
test(`HTTP logger redacts request authorization and response proof at ${path} without mutating headers`, async () => {
|
|
79
|
-
const headers = {
|
|
80
|
-
'x-vona-test-challenge': 'secret-challenge',
|
|
81
|
-
'X-Vona-Test-Stop-Authorization': 'secret-authorization',
|
|
82
|
-
'x-vona-test-stop-nonce': 'secret-nonce',
|
|
83
|
-
'accept': 'application/json',
|
|
84
|
-
};
|
|
85
|
-
const responseHeaders = {
|
|
86
|
-
'x-vona-test-proof': 'secret-readiness',
|
|
87
|
-
'X-Vona-Test-Stop-Proof': 'secret-ack',
|
|
88
|
-
'content-type': 'application/json',
|
|
89
|
-
};
|
|
90
|
-
const logs: string[] = [];
|
|
91
|
-
const middleware = Object.create(MiddlewareSystemHttpLog.prototype);
|
|
92
|
-
Object.assign(middleware, {
|
|
93
|
-
app: { meta: { env: { VONA_TEST_RUN_ID: '20261014010203-a1b2c3d4' } } },
|
|
94
|
-
ctx: {
|
|
95
|
-
path,
|
|
96
|
-
req: { url: path, headers },
|
|
97
|
-
res: { statusCode: 200, getHeaders: () => responseHeaders },
|
|
98
|
-
},
|
|
99
|
-
$loggerChild: () => ({
|
|
100
|
-
http: (message: () => string) => logs.push(message()),
|
|
101
|
-
startTimer: () => ({ done: ({ message }: { message: string }) => logs.push(message) }),
|
|
102
|
-
}),
|
|
103
|
-
});
|
|
104
|
-
await middleware.execute({}, async () => {});
|
|
105
|
-
assert.equal(logs.length, 2);
|
|
106
|
-
assert.equal(logs.join('\n').includes('secret-'), false);
|
|
107
|
-
assert.match(logs[0], /application\/json/);
|
|
108
|
-
assert.match(logs[1], /application\/json/);
|
|
109
|
-
assert.equal(headers['X-Vona-Test-Stop-Authorization'], 'secret-authorization');
|
|
110
|
-
assert.equal(responseHeaders['X-Vona-Test-Stop-Proof'], 'secret-ack');
|
|
111
|
-
});
|
|
112
|
-
}
|
|
@@ -13,6 +13,11 @@ export class ServiceMail extends BeanBase {
|
|
|
13
13
|
async sendById(id: TableIdentity) {
|
|
14
14
|
const mail = await this.scope.model.mail.getById(id);
|
|
15
15
|
if (!mail) return;
|
|
16
|
+
if (this.app.meta.isTest) {
|
|
17
|
+
await this.scope.model.mail.delete({ id });
|
|
18
|
+
this.$logger.info('Test mode: skipped external mail send.');
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
16
21
|
const clientName = mail.client;
|
|
17
22
|
// client
|
|
18
23
|
let client = this._getClient(clientName);
|
|
@@ -1 +1,12 @@
|
|
|
1
1
|
export * from './.metadata/index.ts';
|
|
2
|
+
export { createProtectedSqliteRegistry } from './lib/protectedSqliteRegistry.ts';
|
|
3
|
+
export type {
|
|
4
|
+
ProtectedSqliteRegistry,
|
|
5
|
+
ProtectedSqliteRegistryCloseResult,
|
|
6
|
+
ProtectedSqliteRegistryLookupOutcome,
|
|
7
|
+
ProtectedSqliteRegistryLookupResult,
|
|
8
|
+
ProtectedSqliteRegistryOptions,
|
|
9
|
+
ProtectedSqliteRegistryPrepareResult,
|
|
10
|
+
ProtectedSqliteRegistryRow,
|
|
11
|
+
ProtectedSqliteRegistrySnapshot,
|
|
12
|
+
} from './lib/protectedSqliteRegistry.ts';
|
|
@@ -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
|
+
}
|
|
@@ -1,8 +1,38 @@
|
|
|
1
1
|
import assert from 'node:assert/strict';
|
|
2
|
+
import { execFile } from 'node:child_process';
|
|
3
|
+
import { registerHooks } from 'node:module';
|
|
2
4
|
import { afterEach, beforeEach, describe, it } from 'node:test';
|
|
5
|
+
import { fileURLToPath } from 'node:url';
|
|
6
|
+
import { promisify } from 'node:util';
|
|
3
7
|
|
|
4
|
-
|
|
5
|
-
|
|
8
|
+
// Only bypass bean registration; exercise real startup methods with local fixtures.
|
|
9
|
+
const serviceUrls = [
|
|
10
|
+
new URL('../../a-instance/src/service/instance.ts', import.meta.url).href,
|
|
11
|
+
new URL('../src/service/startup.ts', import.meta.url).href,
|
|
12
|
+
];
|
|
13
|
+
const hooks = registerHooks({
|
|
14
|
+
resolve(specifier, context, nextResolve) {
|
|
15
|
+
if (serviceUrls.includes(context.parentURL!) && specifier === 'vona-module-a-bean') {
|
|
16
|
+
return {
|
|
17
|
+
url: 'data:text/javascript,export function Service() { return () => {}; }',
|
|
18
|
+
shortCircuit: true,
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
return nextResolve(specifier, context);
|
|
22
|
+
},
|
|
23
|
+
});
|
|
24
|
+
const { ServiceInstance, ServiceStartup } = await (async () => {
|
|
25
|
+
try {
|
|
26
|
+
return {
|
|
27
|
+
...(await import('../../a-instance/src/service/instance.ts')),
|
|
28
|
+
...(await import('../src/service/startup.ts')),
|
|
29
|
+
};
|
|
30
|
+
} finally {
|
|
31
|
+
hooks.deregister();
|
|
32
|
+
}
|
|
33
|
+
})();
|
|
34
|
+
type ServiceInstance = InstanceType<typeof ServiceInstance>;
|
|
35
|
+
type ServiceStartup = InstanceType<typeof ServiceStartup>;
|
|
6
36
|
|
|
7
37
|
function fixture(created: string[] = []) {
|
|
8
38
|
const calls: string[] = [];
|
|
@@ -105,6 +135,35 @@ const isolateCalls = [
|
|
|
105
135
|
];
|
|
106
136
|
|
|
107
137
|
describe('creation-driven dev seed', { concurrency: false }, () => {
|
|
138
|
+
// Keep reset-flag mutations out of the managed runner's concurrent test process.
|
|
139
|
+
if (process.env.VONA_TEST_RUN_ID !== undefined) {
|
|
140
|
+
it('runs dev seed cases in an isolated process', async () => {
|
|
141
|
+
const loader = import.meta.resolve('ts-node-maintained/esm');
|
|
142
|
+
const projectRoot = new URL('../../../../../../', import.meta.url);
|
|
143
|
+
const registerSource = `import { register } from 'node:module'; register(${JSON.stringify(loader)}, ${JSON.stringify(projectRoot.href)});`;
|
|
144
|
+
const env = Object.fromEntries(Object.entries(process.env)) as NodeJS.ProcessEnv;
|
|
145
|
+
delete env.VONA_TEST_RUN_ID;
|
|
146
|
+
delete env.NODE_TEST_CONTEXT;
|
|
147
|
+
delete env.VONA_DB_RESET;
|
|
148
|
+
env.TS_NODE_PROJECT = fileURLToPath(new URL('tsconfig.json', projectRoot));
|
|
149
|
+
const { stdout } = await promisify(execFile)(
|
|
150
|
+
process.execPath,
|
|
151
|
+
[
|
|
152
|
+
'--import',
|
|
153
|
+
`data:text/javascript,${encodeURIComponent(registerSource)}`,
|
|
154
|
+
'--test',
|
|
155
|
+
'--test-reporter=tap',
|
|
156
|
+
'--test-name-pattern=creation-driven dev seed',
|
|
157
|
+
fileURLToPath(import.meta.url),
|
|
158
|
+
],
|
|
159
|
+
{ env, timeout: 30_000 },
|
|
160
|
+
);
|
|
161
|
+
assert.match(stdout, /# fail 0\b/);
|
|
162
|
+
assert.match(stdout, /# pass 16\b/);
|
|
163
|
+
});
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
|
|
108
167
|
let previousReset: string | undefined;
|
|
109
168
|
beforeEach(() => {
|
|
110
169
|
previousReset = process.env.VONA_DB_RESET;
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
"url": "git+https://github.com/zovajs/zova.git"
|
|
15
15
|
},
|
|
16
16
|
"type": "module",
|
|
17
|
-
"packageManager": "pnpm@
|
|
17
|
+
"packageManager": "pnpm@12.14.0",
|
|
18
18
|
"scripts": {
|
|
19
19
|
"zova": "node ./packages-cli/cli/src/bin/zova.ts",
|
|
20
20
|
"dev": "npm run dev:ssr:admin",
|
|
@@ -85,7 +85,6 @@
|
|
|
85
85
|
"typescript": "^5.9.3",
|
|
86
86
|
"vite": "^8.3.4",
|
|
87
87
|
"vite-plugin-fake-server-turbo": "^3.0.18",
|
|
88
|
-
"vue-tsc": "^3.3.1",
|
|
89
88
|
"zova-vite": "workspace:^",
|
|
90
89
|
"textlint": "^15.7.0",
|
|
91
90
|
"textlint-rule-en-no-mixed-period": "^3.0.2",
|
|
@@ -96,6 +95,7 @@
|
|
|
96
95
|
"oxlint": "^1.77.0"
|
|
97
96
|
},
|
|
98
97
|
"engines": {
|
|
99
|
-
"node": ">=
|
|
98
|
+
"node": ">=24.4.0",
|
|
99
|
+
"pnpm": ">=11.5.2"
|
|
100
100
|
}
|
|
101
101
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "zova-cli",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.118",
|
|
4
4
|
"gitHead": "6f675a8cc46d596142c591c28a40cc4d82fcc6cc",
|
|
5
5
|
"description": "zova cli",
|
|
6
6
|
"keywords": [
|
|
@@ -40,11 +40,11 @@
|
|
|
40
40
|
"postpack": "clean-package restore"
|
|
41
41
|
},
|
|
42
42
|
"dependencies": {
|
|
43
|
-
"@cabloy/cli": "^3.1.
|
|
44
|
-
"@cabloy/process-helper": "^3.1.
|
|
43
|
+
"@cabloy/cli": "^3.1.30",
|
|
44
|
+
"@cabloy/process-helper": "^3.1.9",
|
|
45
45
|
"fs-extra": "^11.3.5",
|
|
46
46
|
"semver": "^7.6.2",
|
|
47
|
-
"zova-cli-set-front": "^1.2.
|
|
47
|
+
"zova-cli-set-front": "^1.2.116"
|
|
48
48
|
},
|
|
49
49
|
"devDependencies": {
|
|
50
50
|
"clean-package": "^2.2.0",
|