@happyvertical/smrt-core 0.47.1 → 0.48.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/AGENTS.md +1 -0
- package/agents/generators.md +12 -0
- package/agents/registry-snapshot.md +28 -0
- package/agents/schema-paths.md +57 -0
- package/agents/system-diagnostics.md +6 -1
- package/dist/browser.js +3 -2
- package/dist/collection.d.ts.map +1 -1
- package/dist/collection.js +3 -1
- package/dist/collection.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -2
- package/dist/manifest/static-manifest.js +1 -1
- package/dist/manifest/static-manifest.js.map +1 -1
- package/dist/manifest/store.js +1 -1
- package/dist/manifest.json +1 -1
- package/dist/migrations/differ.d.ts +88 -4
- package/dist/migrations/differ.d.ts.map +1 -1
- package/dist/migrations/differ.js +363 -5
- package/dist/migrations/differ.js.map +1 -1
- package/dist/migrations/generator.d.ts.map +1 -1
- package/dist/migrations/generator.js +2 -1
- package/dist/migrations/generator.js.map +1 -1
- package/dist/prebuild/index.d.ts.map +1 -1
- package/dist/prebuild/index.js +4 -3
- package/dist/prebuild/index.js.map +1 -1
- package/dist/registry/class-registration.d.ts.map +1 -1
- package/dist/registry/class-registration.js +3 -1
- package/dist/registry/class-registration.js.map +1 -1
- package/dist/registry/schema-builder.js +1 -1
- package/dist/registry.js +1 -1
- package/dist/scanner/manifest-generator.d.ts.map +1 -1
- package/dist/scanner/manifest-generator.js +22 -1
- package/dist/scanner/manifest-generator.js.map +1 -1
- package/dist/schema/foreign-key-ddl.d.ts +26 -0
- package/dist/schema/foreign-key-ddl.d.ts.map +1 -1
- package/dist/schema/foreign-key-ddl.js +26 -2
- package/dist/schema/foreign-key-ddl.js.map +1 -1
- package/dist/schema/foreign-key-orphan-report.d.ts +57 -0
- package/dist/schema/foreign-key-orphan-report.d.ts.map +1 -0
- package/dist/schema/foreign-key-orphan-report.js +159 -0
- package/dist/schema/foreign-key-orphan-report.js.map +1 -0
- package/dist/schema/index.d.ts +2 -1
- package/dist/schema/index.d.ts.map +1 -1
- package/dist/schema/index.js +3 -2
- package/dist/schema/live-parity.d.ts +8 -1
- package/dist/schema/live-parity.d.ts.map +1 -1
- package/dist/schema/live-parity.js +172 -2
- package/dist/schema/live-parity.js.map +1 -1
- package/dist/schema/text-cast-probe.d.ts +66 -0
- package/dist/schema/text-cast-probe.d.ts.map +1 -0
- package/dist/schema/text-cast-probe.js +142 -0
- package/dist/schema/text-cast-probe.js.map +1 -0
- package/dist/schema/types.d.ts +39 -1
- package/dist/schema/types.d.ts.map +1 -1
- package/dist/schema.js +3 -2
- package/dist/smrt-knowledge.json +17 -11
- package/dist/system/diagnostics.d.ts +22 -0
- package/dist/system/diagnostics.d.ts.map +1 -1
- package/dist/system/diagnostics.js +88 -31
- package/dist/system/diagnostics.js.map +1 -1
- package/dist/system/index.d.ts +1 -0
- package/dist/system/index.d.ts.map +1 -1
- package/dist/system/index.js +2 -1
- package/dist/system/registry-snapshot.d.ts +112 -0
- package/dist/system/registry-snapshot.d.ts.map +1 -0
- package/dist/system/registry-snapshot.js +171 -0
- package/dist/system/registry-snapshot.js.map +1 -0
- package/dist/testing/database.d.ts.map +1 -1
- package/dist/testing/database.js +10 -2
- package/dist/testing/database.js.map +1 -1
- package/dist/vite-plugin/dev-plane-route.d.ts +5 -0
- package/dist/vite-plugin/dev-plane-route.d.ts.map +1 -0
- package/dist/vite-plugin/dev-plane-route.js +91 -0
- package/dist/vite-plugin/dev-plane-route.js.map +1 -0
- package/dist/vite-plugin/index.d.ts.map +1 -1
- package/dist/vite-plugin/index.js +4 -3
- package/dist/vite-plugin/index.js.map +1 -1
- package/dist/vite-plugin/sveltekit-generator.d.ts +8 -0
- package/dist/vite-plugin/sveltekit-generator.d.ts.map +1 -1
- package/dist/vite-plugin/sveltekit-generator.js +2 -0
- package/dist/vite-plugin/sveltekit-generator.js.map +1 -1
- package/dist/vite-plugin/web-collections.d.ts +6 -0
- package/dist/vite-plugin/web-collections.d.ts.map +1 -1
- package/dist/vite-plugin/web-collections.js +9 -4
- package/dist/vite-plugin/web-collections.js.map +1 -1
- package/package.json +4 -4
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sanitized, serializable snapshot of the booted `ObjectRegistry` (#1831).
|
|
3
|
+
*
|
|
4
|
+
* The registry holds live constructors, validators, tool payloads, and
|
|
5
|
+
* absolute source paths. None of that may cross a process or wire boundary:
|
|
6
|
+
* a development agent observing a booted runtime gets *this* DTO, built by
|
|
7
|
+
* projection, never the raw `RegisteredClass`. Every value here is plain JSON
|
|
8
|
+
* (strings, numbers, booleans, arrays, plain objects), which the test suite
|
|
9
|
+
* asserts structurally rather than by allow-list alone.
|
|
10
|
+
*
|
|
11
|
+
* The snapshot is a read of in-process state: it never touches a database,
|
|
12
|
+
* never boots project code, and never mutates the registry.
|
|
13
|
+
*/
|
|
14
|
+
/** Provenance label for facts read from the booted in-process registry. */
|
|
15
|
+
export declare const BOOTED_PROVENANCE = "booted (registry)";
|
|
16
|
+
export interface RegistrySnapshotField {
|
|
17
|
+
name: string;
|
|
18
|
+
type: string;
|
|
19
|
+
required: boolean;
|
|
20
|
+
/** Relationship target for foreignKey / crossPackageRef / *ToMany fields. */
|
|
21
|
+
related: string | null;
|
|
22
|
+
transient: boolean;
|
|
23
|
+
/** Inherited from a base class rather than declared on the object. */
|
|
24
|
+
inherited: boolean;
|
|
25
|
+
/** Declared on a tenant-scoped/sensitive field policy — the value is never here. */
|
|
26
|
+
sqlType: string | null;
|
|
27
|
+
}
|
|
28
|
+
export interface RegistrySnapshotMethodParameter {
|
|
29
|
+
name: string;
|
|
30
|
+
type: string;
|
|
31
|
+
optional: boolean;
|
|
32
|
+
}
|
|
33
|
+
export interface RegistrySnapshotMethod {
|
|
34
|
+
name: string;
|
|
35
|
+
async: boolean;
|
|
36
|
+
isStatic: boolean;
|
|
37
|
+
isPublic: boolean;
|
|
38
|
+
parameters: RegistrySnapshotMethodParameter[];
|
|
39
|
+
returnType: string;
|
|
40
|
+
inherited: boolean;
|
|
41
|
+
}
|
|
42
|
+
export interface RegistrySnapshotObject {
|
|
43
|
+
name: string;
|
|
44
|
+
qualifiedName: string | null;
|
|
45
|
+
packageName: string | null;
|
|
46
|
+
tableName: string | null;
|
|
47
|
+
collection: string | null;
|
|
48
|
+
extends: string | null;
|
|
49
|
+
extendsTypeArg: string | null;
|
|
50
|
+
inheritanceChain: string[];
|
|
51
|
+
visibility: string | null;
|
|
52
|
+
/** Source path relative to `projectRoot`; absolute paths never leave the process. */
|
|
53
|
+
sourceFile: string | null;
|
|
54
|
+
tenantScoped: {
|
|
55
|
+
mode: 'required' | 'optional';
|
|
56
|
+
field: string;
|
|
57
|
+
} | null;
|
|
58
|
+
fieldCount: number;
|
|
59
|
+
methodCount: number;
|
|
60
|
+
fields: RegistrySnapshotField[];
|
|
61
|
+
methods: RegistrySnapshotMethod[];
|
|
62
|
+
}
|
|
63
|
+
export interface RegistrySnapshotDiagnostic {
|
|
64
|
+
severity: 'warn' | 'error';
|
|
65
|
+
code: string;
|
|
66
|
+
message: string;
|
|
67
|
+
}
|
|
68
|
+
export interface RegistrySnapshotSummary {
|
|
69
|
+
objectCount: number;
|
|
70
|
+
packages: Array<{
|
|
71
|
+
name: string;
|
|
72
|
+
objectCount: number;
|
|
73
|
+
}>;
|
|
74
|
+
tables: string[];
|
|
75
|
+
diagnosticCount: number;
|
|
76
|
+
}
|
|
77
|
+
export interface RuntimeRegistrySnapshot {
|
|
78
|
+
provenance: typeof BOOTED_PROVENANCE;
|
|
79
|
+
generatedAt: string;
|
|
80
|
+
summary: RegistrySnapshotSummary;
|
|
81
|
+
objects: RegistrySnapshotObject[];
|
|
82
|
+
diagnostics: RegistrySnapshotDiagnostic[];
|
|
83
|
+
}
|
|
84
|
+
export interface RegistrySnapshotOptions {
|
|
85
|
+
/** Root used to relativize source paths. Defaults to `process.cwd()`. */
|
|
86
|
+
projectRoot?: string;
|
|
87
|
+
/** Restrict `objects` to these simple or qualified names (summary stays global). */
|
|
88
|
+
objects?: string[];
|
|
89
|
+
/** Include per-object field and method detail (default `true`). */
|
|
90
|
+
detail?: boolean;
|
|
91
|
+
/** Clock for `generatedAt` (defaults to `new Date()`). */
|
|
92
|
+
now?: Date;
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Project the booted registry into a sanitized, JSON-serializable snapshot.
|
|
96
|
+
*
|
|
97
|
+
* Pure read of in-process registry state: no database, no project code, no
|
|
98
|
+
* registry mutation. Objects are sorted by qualified name for stable output.
|
|
99
|
+
*/
|
|
100
|
+
export declare function snapshotRegistry(options?: RegistrySnapshotOptions): RuntimeRegistrySnapshot;
|
|
101
|
+
/**
|
|
102
|
+
* Reduce absolute paths embedded in a message to the same relative/basename
|
|
103
|
+
* form {@link relativeSourcePath} produces, so diagnostic text cannot leak
|
|
104
|
+
* filesystem layout the structured fields already hide.
|
|
105
|
+
*/
|
|
106
|
+
export declare function sanitizeMessagePaths(message: string, projectRoot: string): string;
|
|
107
|
+
/**
|
|
108
|
+
* Assert a value is plain JSON: no functions, classes, symbols, or cycles.
|
|
109
|
+
* Exposed so callers and tests can prove a snapshot is wire-safe.
|
|
110
|
+
*/
|
|
111
|
+
export declare function assertPlainJson(value: unknown, path?: string): void;
|
|
112
|
+
//# sourceMappingURL=registry-snapshot.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"registry-snapshot.d.ts","sourceRoot":"","sources":["../../src/system/registry-snapshot.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
import { ObjectRegistry } from "../registry.js";
|
|
2
|
+
import { basename, isAbsolute, relative, sep, win32 } from "node:path";
|
|
3
|
+
//#region src/system/registry-snapshot.ts
|
|
4
|
+
/**
|
|
5
|
+
* Sanitized, serializable snapshot of the booted `ObjectRegistry` (#1831).
|
|
6
|
+
*
|
|
7
|
+
* The registry holds live constructors, validators, tool payloads, and
|
|
8
|
+
* absolute source paths. None of that may cross a process or wire boundary:
|
|
9
|
+
* a development agent observing a booted runtime gets *this* DTO, built by
|
|
10
|
+
* projection, never the raw `RegisteredClass`. Every value here is plain JSON
|
|
11
|
+
* (strings, numbers, booleans, arrays, plain objects), which the test suite
|
|
12
|
+
* asserts structurally rather than by allow-list alone.
|
|
13
|
+
*
|
|
14
|
+
* The snapshot is a read of in-process state: it never touches a database,
|
|
15
|
+
* never boots project code, and never mutates the registry.
|
|
16
|
+
*/
|
|
17
|
+
/** Provenance label for facts read from the booted in-process registry. */
|
|
18
|
+
var BOOTED_PROVENANCE = "booted (registry)";
|
|
19
|
+
function toStringOrNull(value) {
|
|
20
|
+
return typeof value === "string" && value.length > 0 ? value : null;
|
|
21
|
+
}
|
|
22
|
+
function relativeSourcePath(filePath, projectRoot) {
|
|
23
|
+
if (typeof filePath !== "string" || filePath.length === 0) return null;
|
|
24
|
+
const rel = relative(projectRoot, filePath);
|
|
25
|
+
if (rel === ".." || rel.startsWith(`..${sep}`) || isAbsolute(rel)) return basename(filePath) || null;
|
|
26
|
+
return rel.split(sep).join("/");
|
|
27
|
+
}
|
|
28
|
+
function toSnapshotField(name, field, inherited) {
|
|
29
|
+
return {
|
|
30
|
+
name,
|
|
31
|
+
type: String(field.type ?? "unknown"),
|
|
32
|
+
required: field.required === true,
|
|
33
|
+
related: toStringOrNull(field.related),
|
|
34
|
+
transient: field.transient === true,
|
|
35
|
+
inherited,
|
|
36
|
+
sqlType: toStringOrNull(field.sqlType)
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
function toSnapshotMethod(method, inherited) {
|
|
40
|
+
return {
|
|
41
|
+
name: method.name,
|
|
42
|
+
async: method.async === true,
|
|
43
|
+
isStatic: method.isStatic === true,
|
|
44
|
+
isPublic: method.isPublic === true,
|
|
45
|
+
parameters: (method.parameters ?? []).map((parameter) => ({
|
|
46
|
+
name: String(parameter.name),
|
|
47
|
+
type: String(parameter.type ?? "unknown"),
|
|
48
|
+
optional: parameter.optional === true
|
|
49
|
+
})),
|
|
50
|
+
returnType: String(method.returnType ?? "unknown"),
|
|
51
|
+
inherited
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
function toSnapshotObject(registered, projectRoot, detail) {
|
|
55
|
+
const fields = [];
|
|
56
|
+
const methods = [];
|
|
57
|
+
if (detail) {
|
|
58
|
+
for (const [name, field] of registered.inheritedFields ?? []) fields.push(toSnapshotField(name, field, true));
|
|
59
|
+
for (const [name, field] of registered.fields) fields.push(toSnapshotField(name, field, false));
|
|
60
|
+
for (const [, method] of registered.inheritedMethods ?? []) methods.push(toSnapshotMethod(method, true));
|
|
61
|
+
for (const [, method] of registered.methods) methods.push(toSnapshotMethod(method, false));
|
|
62
|
+
fields.sort((a, b) => a.name.localeCompare(b.name));
|
|
63
|
+
methods.sort((a, b) => a.name.localeCompare(b.name));
|
|
64
|
+
}
|
|
65
|
+
return {
|
|
66
|
+
name: registered.name,
|
|
67
|
+
qualifiedName: toStringOrNull(registered.qualifiedName),
|
|
68
|
+
packageName: toStringOrNull(registered.packageName),
|
|
69
|
+
tableName: ObjectRegistry.getTableName(registered.name) ?? null,
|
|
70
|
+
collection: toStringOrNull(registered.collection),
|
|
71
|
+
extends: toStringOrNull(registered.extends),
|
|
72
|
+
extendsTypeArg: toStringOrNull(registered.extendsTypeArg),
|
|
73
|
+
inheritanceChain: [...registered.inheritanceChain ?? []],
|
|
74
|
+
visibility: toStringOrNull(registered.visibility),
|
|
75
|
+
sourceFile: relativeSourcePath(registered.sourceFilePath, projectRoot),
|
|
76
|
+
tenantScoped: registered.tenantScopedConfig ? {
|
|
77
|
+
mode: registered.tenantScopedConfig.mode,
|
|
78
|
+
field: registered.tenantScopedConfig.field
|
|
79
|
+
} : null,
|
|
80
|
+
fieldCount: registered.fields.size + (registered.inheritedFields?.size ?? 0),
|
|
81
|
+
methodCount: registered.methods.size + (registered.inheritedMethods?.size ?? 0),
|
|
82
|
+
fields,
|
|
83
|
+
methods
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
function matchesFilter(registered, filter) {
|
|
87
|
+
if (!filter) return true;
|
|
88
|
+
return filter.has(registered.name) || typeof registered.qualifiedName === "string" && filter.has(registered.qualifiedName);
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Project the booted registry into a sanitized, JSON-serializable snapshot.
|
|
92
|
+
*
|
|
93
|
+
* Pure read of in-process registry state: no database, no project code, no
|
|
94
|
+
* registry mutation. Objects are sorted by qualified name for stable output.
|
|
95
|
+
*/
|
|
96
|
+
function snapshotRegistry(options = {}) {
|
|
97
|
+
const projectRoot = options.projectRoot ?? process.cwd();
|
|
98
|
+
const detail = options.detail !== false;
|
|
99
|
+
const filter = options.objects && options.objects.length > 0 ? new Set(options.objects) : null;
|
|
100
|
+
const all = [...ObjectRegistry.getAllClasses().values()].sort((a, b) => String(a.qualifiedName ?? a.name).localeCompare(String(b.qualifiedName ?? b.name)));
|
|
101
|
+
const packageCounts = /* @__PURE__ */ new Map();
|
|
102
|
+
const tables = /* @__PURE__ */ new Set();
|
|
103
|
+
for (const registered of all) {
|
|
104
|
+
const pkg = toStringOrNull(registered.packageName) ?? "(unqualified)";
|
|
105
|
+
packageCounts.set(pkg, (packageCounts.get(pkg) ?? 0) + 1);
|
|
106
|
+
const table = ObjectRegistry.getTableName(registered.name);
|
|
107
|
+
if (table) tables.add(table);
|
|
108
|
+
}
|
|
109
|
+
const diagnostics = ObjectRegistry.getDiagnostics().map((diagnostic) => ({
|
|
110
|
+
severity: diagnostic.severity,
|
|
111
|
+
code: diagnostic.code,
|
|
112
|
+
message: sanitizeMessagePaths(diagnostic.message, projectRoot)
|
|
113
|
+
}));
|
|
114
|
+
return {
|
|
115
|
+
provenance: BOOTED_PROVENANCE,
|
|
116
|
+
generatedAt: (options.now ?? /* @__PURE__ */ new Date()).toISOString(),
|
|
117
|
+
summary: {
|
|
118
|
+
objectCount: all.length,
|
|
119
|
+
packages: [...packageCounts.entries()].map(([name, objectCount]) => ({
|
|
120
|
+
name,
|
|
121
|
+
objectCount
|
|
122
|
+
})).sort((a, b) => a.name.localeCompare(b.name)),
|
|
123
|
+
tables: [...tables].sort(),
|
|
124
|
+
diagnosticCount: diagnostics.length
|
|
125
|
+
},
|
|
126
|
+
objects: all.filter((registered) => matchesFilter(registered, filter)).map((registered) => toSnapshotObject(registered, projectRoot, detail)),
|
|
127
|
+
diagnostics
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
/** POSIX or Windows absolute path tokens inside free text. */
|
|
131
|
+
var ABSOLUTE_PATH_TOKEN = /(?:[A-Za-z]:)?(?:[\\/][^\s\\/'"`(),;]+){2,}/g;
|
|
132
|
+
/**
|
|
133
|
+
* Reduce absolute paths embedded in a message to the same relative/basename
|
|
134
|
+
* form {@link relativeSourcePath} produces, so diagnostic text cannot leak
|
|
135
|
+
* filesystem layout the structured fields already hide.
|
|
136
|
+
*/
|
|
137
|
+
function sanitizeMessagePaths(message, projectRoot) {
|
|
138
|
+
return message.replace(ABSOLUTE_PATH_TOKEN, (token) => {
|
|
139
|
+
if (isAbsolute(token)) return relativeSourcePath(token, projectRoot) ?? token;
|
|
140
|
+
if (win32.isAbsolute(token)) return win32.basename(token) || token;
|
|
141
|
+
return token;
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* Assert a value is plain JSON: no functions, classes, symbols, or cycles.
|
|
146
|
+
* Exposed so callers and tests can prove a snapshot is wire-safe.
|
|
147
|
+
*/
|
|
148
|
+
function assertPlainJson(value, path = "$") {
|
|
149
|
+
const seen = /* @__PURE__ */ new WeakSet();
|
|
150
|
+
const walk = (node, at) => {
|
|
151
|
+
if (node === null) return;
|
|
152
|
+
const kind = typeof node;
|
|
153
|
+
if (kind === "string" || kind === "number" || kind === "boolean") return;
|
|
154
|
+
if (kind !== "object") throw new Error(`non-JSON value (${kind}) at ${at}`);
|
|
155
|
+
const obj = node;
|
|
156
|
+
if (seen.has(obj)) throw new Error(`cycle at ${at}`);
|
|
157
|
+
seen.add(obj);
|
|
158
|
+
if (Array.isArray(obj)) {
|
|
159
|
+
for (const [index, item] of obj.entries()) walk(item, `${at}[${index}]`);
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
const proto = Object.getPrototypeOf(obj);
|
|
163
|
+
if (proto !== Object.prototype && proto !== null) throw new Error(`non-plain object at ${at}`);
|
|
164
|
+
for (const [key, item] of Object.entries(obj)) walk(item, `${at}.${key}`);
|
|
165
|
+
};
|
|
166
|
+
walk(value, path);
|
|
167
|
+
}
|
|
168
|
+
//#endregion
|
|
169
|
+
export { BOOTED_PROVENANCE, assertPlainJson, sanitizeMessagePaths, snapshotRegistry };
|
|
170
|
+
|
|
171
|
+
//# sourceMappingURL=registry-snapshot.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"registry-snapshot.js","names":[],"sources":["../../src/system/registry-snapshot.ts"],"sourcesContent":["/**\n * Sanitized, serializable snapshot of the booted `ObjectRegistry` (#1831).\n *\n * The registry holds live constructors, validators, tool payloads, and\n * absolute source paths. None of that may cross a process or wire boundary:\n * a development agent observing a booted runtime gets *this* DTO, built by\n * projection, never the raw `RegisteredClass`. Every value here is plain JSON\n * (strings, numbers, booleans, arrays, plain objects), which the test suite\n * asserts structurally rather than by allow-list alone.\n *\n * The snapshot is a read of in-process state: it never touches a database,\n * never boots project code, and never mutates the registry.\n */\n\nimport { basename, isAbsolute, relative, sep, win32 } from 'node:path';\nimport type { RegisteredClass, RegisteredField } from '../registry/types.js';\nimport { ObjectRegistry } from '../registry.js';\nimport type { MethodDefinition } from '../scanner/types.js';\n\n/** Provenance label for facts read from the booted in-process registry. */\nexport const BOOTED_PROVENANCE = 'booted (registry)';\n\nexport interface RegistrySnapshotField {\n name: string;\n type: string;\n required: boolean;\n /** Relationship target for foreignKey / crossPackageRef / *ToMany fields. */\n related: string | null;\n transient: boolean;\n /** Inherited from a base class rather than declared on the object. */\n inherited: boolean;\n /** Declared on a tenant-scoped/sensitive field policy — the value is never here. */\n sqlType: string | null;\n}\n\nexport interface RegistrySnapshotMethodParameter {\n name: string;\n type: string;\n optional: boolean;\n}\n\nexport interface RegistrySnapshotMethod {\n name: string;\n async: boolean;\n isStatic: boolean;\n isPublic: boolean;\n parameters: RegistrySnapshotMethodParameter[];\n returnType: string;\n inherited: boolean;\n}\n\nexport interface RegistrySnapshotObject {\n name: string;\n qualifiedName: string | null;\n packageName: string | null;\n tableName: string | null;\n collection: string | null;\n extends: string | null;\n extendsTypeArg: string | null;\n inheritanceChain: string[];\n visibility: string | null;\n /** Source path relative to `projectRoot`; absolute paths never leave the process. */\n sourceFile: string | null;\n tenantScoped: {\n mode: 'required' | 'optional';\n field: string;\n } | null;\n fieldCount: number;\n methodCount: number;\n fields: RegistrySnapshotField[];\n methods: RegistrySnapshotMethod[];\n}\n\nexport interface RegistrySnapshotDiagnostic {\n severity: 'warn' | 'error';\n code: string;\n message: string;\n}\n\nexport interface RegistrySnapshotSummary {\n objectCount: number;\n packages: Array<{ name: string; objectCount: number }>;\n tables: string[];\n diagnosticCount: number;\n}\n\nexport interface RuntimeRegistrySnapshot {\n provenance: typeof BOOTED_PROVENANCE;\n generatedAt: string;\n summary: RegistrySnapshotSummary;\n objects: RegistrySnapshotObject[];\n diagnostics: RegistrySnapshotDiagnostic[];\n}\n\nexport interface RegistrySnapshotOptions {\n /** Root used to relativize source paths. Defaults to `process.cwd()`. */\n projectRoot?: string;\n /** Restrict `objects` to these simple or qualified names (summary stays global). */\n objects?: string[];\n /** Include per-object field and method detail (default `true`). */\n detail?: boolean;\n /** Clock for `generatedAt` (defaults to `new Date()`). */\n now?: Date;\n}\n\nfunction toStringOrNull(value: unknown): string | null {\n return typeof value === 'string' && value.length > 0 ? value : null;\n}\n\nfunction relativeSourcePath(\n filePath: unknown,\n projectRoot: string,\n): string | null {\n if (typeof filePath !== 'string' || filePath.length === 0) return null;\n const rel = relative(projectRoot, filePath);\n // A path outside the project root (installed package, symlinked workspace,\n // or another drive on Windows — `relative` keeps those absolute) would\n // still leak the layout through `../..` walking; report only the basename.\n if (rel === '..' || rel.startsWith(`..${sep}`) || isAbsolute(rel)) {\n return basename(filePath) || null;\n }\n return rel.split(sep).join('/');\n}\n\nfunction toSnapshotField(\n name: string,\n field: RegisteredField,\n inherited: boolean,\n): RegistrySnapshotField {\n return {\n name,\n type: String(field.type ?? 'unknown'),\n required: field.required === true,\n related: toStringOrNull(field.related),\n transient: field.transient === true,\n inherited,\n sqlType: toStringOrNull(field.sqlType),\n };\n}\n\nfunction toSnapshotMethod(\n method: MethodDefinition,\n inherited: boolean,\n): RegistrySnapshotMethod {\n return {\n name: method.name,\n async: method.async === true,\n isStatic: method.isStatic === true,\n isPublic: method.isPublic === true,\n parameters: (method.parameters ?? []).map((parameter) => ({\n name: String(parameter.name),\n type: String(parameter.type ?? 'unknown'),\n optional: parameter.optional === true,\n })),\n returnType: String(method.returnType ?? 'unknown'),\n inherited,\n };\n}\n\nfunction toSnapshotObject(\n registered: RegisteredClass,\n projectRoot: string,\n detail: boolean,\n): RegistrySnapshotObject {\n const fields: RegistrySnapshotField[] = [];\n const methods: RegistrySnapshotMethod[] = [];\n if (detail) {\n for (const [name, field] of registered.inheritedFields ?? []) {\n fields.push(toSnapshotField(name, field, true));\n }\n for (const [name, field] of registered.fields) {\n fields.push(toSnapshotField(name, field, false));\n }\n for (const [, method] of registered.inheritedMethods ?? []) {\n methods.push(toSnapshotMethod(method, true));\n }\n for (const [, method] of registered.methods) {\n methods.push(toSnapshotMethod(method, false));\n }\n fields.sort((a, b) => a.name.localeCompare(b.name));\n methods.sort((a, b) => a.name.localeCompare(b.name));\n }\n return {\n name: registered.name,\n qualifiedName: toStringOrNull(registered.qualifiedName),\n packageName: toStringOrNull(registered.packageName),\n tableName: ObjectRegistry.getTableName(registered.name) ?? null,\n collection: toStringOrNull(registered.collection),\n extends: toStringOrNull(registered.extends),\n extendsTypeArg: toStringOrNull(registered.extendsTypeArg),\n inheritanceChain: [...(registered.inheritanceChain ?? [])],\n visibility: toStringOrNull(registered.visibility),\n sourceFile: relativeSourcePath(registered.sourceFilePath, projectRoot),\n tenantScoped: registered.tenantScopedConfig\n ? {\n mode: registered.tenantScopedConfig.mode,\n field: registered.tenantScopedConfig.field,\n }\n : null,\n fieldCount:\n registered.fields.size + (registered.inheritedFields?.size ?? 0),\n methodCount:\n registered.methods.size + (registered.inheritedMethods?.size ?? 0),\n fields,\n methods,\n };\n}\n\nfunction matchesFilter(\n registered: RegisteredClass,\n filter: Set<string> | null,\n): boolean {\n if (!filter) return true;\n return (\n filter.has(registered.name) ||\n (typeof registered.qualifiedName === 'string' &&\n filter.has(registered.qualifiedName))\n );\n}\n\n/**\n * Project the booted registry into a sanitized, JSON-serializable snapshot.\n *\n * Pure read of in-process registry state: no database, no project code, no\n * registry mutation. Objects are sorted by qualified name for stable output.\n */\nexport function snapshotRegistry(\n options: RegistrySnapshotOptions = {},\n): RuntimeRegistrySnapshot {\n const projectRoot = options.projectRoot ?? process.cwd();\n const detail = options.detail !== false;\n const filter =\n options.objects && options.objects.length > 0\n ? new Set(options.objects)\n : null;\n const all = [...ObjectRegistry.getAllClasses().values()].sort((a, b) =>\n String(a.qualifiedName ?? a.name).localeCompare(\n String(b.qualifiedName ?? b.name),\n ),\n );\n\n const packageCounts = new Map<string, number>();\n const tables = new Set<string>();\n for (const registered of all) {\n const pkg = toStringOrNull(registered.packageName) ?? '(unqualified)';\n packageCounts.set(pkg, (packageCounts.get(pkg) ?? 0) + 1);\n const table = ObjectRegistry.getTableName(registered.name);\n if (table) tables.add(table);\n }\n\n // `context` may carry absolute paths and is dropped. Messages such as\n // PACKAGE_MANIFEST_NOT_FOUND also interpolate paths, so every absolute\n // path token in the message text is reduced the same way source files are.\n const diagnostics: RegistrySnapshotDiagnostic[] =\n ObjectRegistry.getDiagnostics().map((diagnostic) => ({\n severity: diagnostic.severity,\n code: diagnostic.code,\n message: sanitizeMessagePaths(diagnostic.message, projectRoot),\n }));\n\n return {\n provenance: BOOTED_PROVENANCE,\n generatedAt: (options.now ?? new Date()).toISOString(),\n summary: {\n objectCount: all.length,\n packages: [...packageCounts.entries()]\n .map(([name, objectCount]) => ({ name, objectCount }))\n .sort((a, b) => a.name.localeCompare(b.name)),\n tables: [...tables].sort(),\n diagnosticCount: diagnostics.length,\n },\n objects: all\n .filter((registered) => matchesFilter(registered, filter))\n .map((registered) => toSnapshotObject(registered, projectRoot, detail)),\n diagnostics,\n };\n}\n\n/** POSIX or Windows absolute path tokens inside free text. */\nconst ABSOLUTE_PATH_TOKEN = /(?:[A-Za-z]:)?(?:[\\\\/][^\\s\\\\/'\"`(),;]+){2,}/g;\n\n/**\n * Reduce absolute paths embedded in a message to the same relative/basename\n * form {@link relativeSourcePath} produces, so diagnostic text cannot leak\n * filesystem layout the structured fields already hide.\n */\nexport function sanitizeMessagePaths(\n message: string,\n projectRoot: string,\n): string {\n return message.replace(ABSOLUTE_PATH_TOKEN, (token) => {\n if (isAbsolute(token)) {\n return relativeSourcePath(token, projectRoot) ?? token;\n }\n // A Windows path quoted on a POSIX host (or vice versa) is still a layout\n // leak; the platform-specific check catches it and keeps the basename.\n if (win32.isAbsolute(token)) {\n return win32.basename(token) || token;\n }\n return token;\n });\n}\n\n/**\n * Assert a value is plain JSON: no functions, classes, symbols, or cycles.\n * Exposed so callers and tests can prove a snapshot is wire-safe.\n */\nexport function assertPlainJson(value: unknown, path = '$'): void {\n const seen = new WeakSet<object>();\n const walk = (node: unknown, at: string): void => {\n if (node === null) return;\n const kind = typeof node;\n if (kind === 'string' || kind === 'number' || kind === 'boolean') return;\n if (kind !== 'object') {\n throw new Error(`non-JSON value (${kind}) at ${at}`);\n }\n const obj = node as object;\n if (seen.has(obj)) throw new Error(`cycle at ${at}`);\n seen.add(obj);\n if (Array.isArray(obj)) {\n for (const [index, item] of obj.entries()) {\n walk(item, `${at}[${index}]`);\n }\n return;\n }\n const proto = Object.getPrototypeOf(obj);\n if (proto !== Object.prototype && proto !== null) {\n throw new Error(`non-plain object at ${at}`);\n }\n for (const [key, item] of Object.entries(obj)) walk(item, `${at}.${key}`);\n };\n walk(value, path);\n}\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"database.d.ts","sourceRoot":"","sources":["../../src/testing/database.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAEH,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,oBAAoB,CAAC;
|
|
1
|
+
{"version":3,"file":"database.d.ts","sourceRoot":"","sources":["../../src/testing/database.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAEH,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,oBAAoB,CAAC;AA0J5D;;GAEG;AACH,MAAM,WAAW,mBAAmB;IAClC;;;;;;OAMG;IACH,IAAI,CAAC,EAAE,QAAQ,GAAG,MAAM,GAAG,QAAQ,GAAG,UAAU,CAAC;IAEjD;;;;OAIG;IACH,GAAG,CAAC,EAAE,MAAM,CAAC;IAEb;;;OAGG;IACH,EAAE,CAAC,EAAE,iBAAiB,CAAC;IAEvB;;;OAGG;IACH,OAAO,CAAC,EAAE,MAAM,EAAE,CAAC;IAEnB;;;OAGG;IACH,mBAAmB,CAAC,EAAE,OAAO,CAAC;IAE9B;;;;;OAKG;IACH,yBAAyB,CAAC,EAAE,OAAO,CAAC;CACrC;AAED,6EAA6E;AAC7E,wBAAgB,4BAA4B,CAC1C,IAAI,EAAE,mBAAmB,CAAC,MAAM,CAAC,EACjC,EAAE,EAAE,iBAAiB,EACrB,iBAAiB,UAAQ,GACxB,QAAQ,GAAG,MAAM,GAAG,QAAQ,GAAG,UAAU,CAwC3C;AA4DD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6CG;AACH,wBAAsB,eAAe,CACnC,OAAO,GAAE,mBAAwB,GAChC,OAAO,CAAC,iBAAiB,CAAC,CAgQ5B"}
|
package/dist/testing/database.js
CHANGED
|
@@ -3,9 +3,10 @@ import { detectEngine } from "../schema/ddl/index.js";
|
|
|
3
3
|
import { getSystemTableDDL } from "../system/schema.js";
|
|
4
4
|
import { ensurePostgresChangeFeedAppendFunction } from "../change-feed.js";
|
|
5
5
|
import { ensureLegacySystemTableCompatibility } from "../system/compatibility.js";
|
|
6
|
+
import { recordRegistryDiagnostic } from "../registry/diagnostics.js";
|
|
6
7
|
import { SchemaGenerator } from "../schema/generator.js";
|
|
7
|
-
import { isCollectionRegistration, resolveCollectionItemClassName, resolveRelatedRegistration } from "../registry/collection-resolution.js";
|
|
8
8
|
import { isFrameworkBaseClass } from "../registry/framework-base-classes.js";
|
|
9
|
+
import { isCollectionRegistration, resolveCollectionItemClassName, resolveRelatedRegistration } from "../registry/collection-resolution.js";
|
|
9
10
|
import { ObjectRegistry } from "../registry.js";
|
|
10
11
|
import { planForeignKeyCreation } from "../schema/foreign-key-planner.js";
|
|
11
12
|
import { getDatabase } from "@happyvertical/sql";
|
|
@@ -156,7 +157,14 @@ async function getTestDatabase(options = {}) {
|
|
|
156
157
|
if (includeSystemTables) await initializeSystemTables(db);
|
|
157
158
|
const isImplicitClassList = classes === void 0;
|
|
158
159
|
const classNames = resolveRequestedSchemaClassNames(classes ?? ObjectRegistry.getQualifiedClassNames());
|
|
159
|
-
if (classNames.length === 0)
|
|
160
|
+
if (classNames.length === 0) {
|
|
161
|
+
if (isImplicitClassList) {
|
|
162
|
+
const emptyRegistryMessage = "getTestDatabase() found zero registered classes in ObjectRegistry and created only system tables. If you expected model tables, the registry is likely empty in this process — for smrt-vitest consumers, confirm registration (smrtVitestPlugin() manifest loading, or your setupFiles) actually runs in the same process as your test code, or pass an explicit `classes` list to getTestDatabase() to opt out of this check.";
|
|
163
|
+
console.warn(`[getTestDatabase] ${emptyRegistryMessage}`);
|
|
164
|
+
recordRegistryDiagnostic("warn", "TEST_DATABASE_EMPTY_IMPLICIT_REGISTRY", emptyRegistryMessage);
|
|
165
|
+
}
|
|
166
|
+
return db;
|
|
167
|
+
}
|
|
160
168
|
const schemaGenerator = new SchemaGenerator();
|
|
161
169
|
const ddlEngine = resolveTestDatabaseDDLEngine(type, db, existingDb !== void 0);
|
|
162
170
|
const schemas = /* @__PURE__ */ new Map();
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"database.js","names":[],"sources":["../../src/testing/database.ts"],"sourcesContent":["/**\n * Test database utilities for SMRT framework\n *\n * Provides `getTestDatabase()` which creates an in-memory database with all\n * registered SMRT object schemas. Uses the same schema generation logic as\n * the migration system to ensure consistency between test and production.\n *\n * @example\n * ```typescript\n * import { getTestDatabase } from '@happyvertical/smrt-core/testing';\n *\n * beforeEach(async () => {\n * const db = await getTestDatabase();\n * collection = await MyCollection.create({ db });\n * });\n * ```\n */\n\nimport type { DatabaseInterface } from '@happyvertical/sql';\nimport { getDatabase } from '@happyvertical/sql';\nimport { ensurePostgresChangeFeedAppendFunction } from '../change-feed.js';\nimport {\n type CollectionRegistrationLookup,\n isCollectionRegistration,\n resolveCollectionItemClassName,\n resolveRelatedRegistration,\n} from '../registry/collection-resolution.js';\nimport { isFrameworkBaseClass } from '../registry/framework-base-classes.js';\nimport { ObjectRegistry } from '../registry.js';\nimport { detectEngine } from '../schema/ddl/index.js';\nimport {\n schemaForeignKeys,\n schemaForeignKeysForEngine,\n} from '../schema/foreign-key-ddl.js';\nimport { planForeignKeyCreation } from '../schema/foreign-key-planner.js';\nimport { SchemaGenerator } from '../schema/generator.js';\nimport type { SchemaDefinition } from '../schema/types.js';\nimport { ensureLegacySystemTableCompatibility } from '../system/compatibility.js';\nimport { getSystemTableDDL } from '../system/schema.js';\n\ntype TestDatabaseConnectionOptions = Parameters<typeof getDatabase>[0] & {\n __smrtSkipVitestSchemaPreparation?: boolean;\n};\n\nfunction resolveSTIBaseRegistration(className: string, stiBaseName: string) {\n const registered = ObjectRegistry.getClass(className);\n\n if (stiBaseName.includes(':')) {\n return ObjectRegistry.getClass(stiBaseName);\n }\n\n if (registered?.packageName) {\n const samePackageBase = ObjectRegistry.getClassInPackage(\n registered.packageName,\n stiBaseName,\n );\n if (samePackageBase) {\n return samePackageBase;\n }\n }\n\n return ObjectRegistry.getClass(stiBaseName);\n}\n\nfunction resolveSTIBaseLookupName(\n className: string,\n stiBaseName: string,\n): string {\n const stiBase = resolveSTIBaseRegistration(className, stiBaseName);\n return stiBase?.qualifiedName || stiBase?.name || stiBaseName;\n}\n\nfunction isSTIChild(className: string): boolean {\n const stiBaseName = ObjectRegistry.getSTIBase(className);\n if (!stiBaseName) {\n return false;\n }\n\n const registered = ObjectRegistry.getClass(className);\n const stiBase = resolveSTIBaseRegistration(className, stiBaseName);\n\n if (registered && stiBase) {\n return registered !== stiBase;\n }\n\n return stiBaseName !== className;\n}\n\ntype RegisteredSchemaClass = NonNullable<\n ReturnType<typeof ObjectRegistry.getClass>\n>;\n\nconst collectionRegistrationLookup: CollectionRegistrationLookup = {\n findClass: (className) => ObjectRegistry.getClass(className),\n findClassInPackage: (packageName, className) =>\n ObjectRegistry.getClassInPackage(packageName, className),\n getInheritanceChain: (className) =>\n ObjectRegistry.getInheritanceChain(className),\n};\n\nfunction resolveCollectionSchemaClassName(\n className: string,\n registered: RegisteredSchemaClass,\n): string {\n const itemClassName = resolveCollectionItemClassName(\n className,\n registered,\n collectionRegistrationLookup,\n );\n if (itemClassName) {\n const itemRegistration = resolveRelatedRegistration(\n itemClassName,\n registered,\n collectionRegistrationLookup,\n );\n const itemLookupName =\n itemRegistration?.qualifiedName ||\n itemRegistration?.name ||\n itemClassName;\n const stiBase = ObjectRegistry.getSTIBase(itemLookupName);\n return stiBase\n ? resolveSTIBaseLookupName(itemLookupName, stiBase)\n : itemLookupName;\n }\n\n const tableName =\n registered.schema?.tableName ||\n registered.config.tableName ||\n ObjectRegistry.getTableName(className);\n if (!tableName) {\n return className;\n }\n\n const collectionPackage = registered.packageName;\n\n for (const candidate of ObjectRegistry.getAllClasses().values()) {\n if (\n candidate === registered ||\n isCollectionRegistration(\n candidate.qualifiedName || candidate.name,\n candidate,\n collectionRegistrationLookup,\n )\n ) {\n continue;\n }\n\n const candidateTableName =\n candidate.schema?.tableName || candidate.config.tableName;\n if (candidateTableName !== tableName) {\n continue;\n }\n\n if (\n collectionPackage &&\n candidate.packageName &&\n candidate.packageName !== collectionPackage\n ) {\n continue;\n }\n\n const candidateLookupName = candidate.qualifiedName || candidate.name;\n const stiBase = ObjectRegistry.getSTIBase(candidateLookupName);\n return stiBase\n ? resolveSTIBaseLookupName(candidateLookupName, stiBase)\n : candidateLookupName;\n }\n\n return className;\n}\n\n/**\n * Options for creating a test database\n */\nexport interface TestDatabaseOptions {\n /**\n * Database type (default: 'sqlite')\n * - 'sqlite': SQLite database\n * - 'json': JSON adapter (stores data as JSON files with DuckDB for querying)\n * - 'duckdb': Native DuckDB database\n * - 'postgres': PostgreSQL database (normally supplied through `db`)\n */\n type?: 'sqlite' | 'json' | 'duckdb' | 'postgres';\n\n /**\n * Database URL (default: ':memory:')\n * Use ':memory:' for in-memory databases (fastest for tests)\n * Or provide a file path for persistent test databases\n */\n url?: string;\n\n /**\n * Pre-existing database to initialize schemas in.\n * If provided, `type` and `url` are ignored.\n */\n db?: DatabaseInterface;\n\n /**\n * Specific classes to setup schemas for.\n * If not provided, sets up schemas for all registered classes.\n */\n classes?: string[];\n\n /**\n * Whether to include system tables (default: true)\n * System tables include _smrt_contexts, _smrt_migrations, etc.\n */\n includeSystemTables?: boolean;\n\n /**\n * Explicitly omit physical FK constraints for adapter/query tests whose\n * subject is unrelated to referential enforcement. Defaults to `false`.\n * This is required (and deliberately noisy at the call site) when a DuckDB\n * fixture uses generated `ON UPDATE CASCADE`, which DuckDB cannot enforce.\n */\n omitForeignKeyConstraints?: boolean;\n}\n\n/** Resolve the DDL dialect used when preparing an existing test database. */\nexport function resolveTestDatabaseDDLEngine(\n type: TestDatabaseOptions['type'],\n db: DatabaseInterface,\n inferFromDatabase = false,\n): 'sqlite' | 'json' | 'duckdb' | 'postgres' {\n // When this helper created the adapter, the requested type is authoritative.\n // Native DuckDB and JSON-on-DuckDB both expose exportTable(), so capability\n // inference must not override an explicit native DuckDB request.\n if (!inferFromDatabase) {\n return type ?? 'sqlite';\n }\n\n const configuredDb = db as DatabaseInterface & {\n config?: { type?: string; url?: string };\n type?: string;\n exportTable?: unknown;\n inferSchemaFromJSON?: unknown;\n getTableLoadErrors?: unknown;\n };\n\n if (\n typeof configuredDb.inferSchemaFromJSON === 'function' ||\n typeof configuredDb.getTableLoadErrors === 'function'\n ) {\n return 'json';\n }\n\n if (typeof configuredDb.exportTable === 'function') {\n // The native DuckDB adapter exposes schema evolution capabilities that the\n // JSON adapter does not. Keep exportTable-only test doubles compatible with\n // the historical JSON structural marker.\n if (\n typeof configuredDb.getTableSchema === 'function' &&\n typeof configuredDb.alterTable?.addColumn === 'function'\n ) {\n return 'duckdb';\n }\n return 'json';\n }\n\n return detectEngine(\n db.url || configuredDb.config?.url || '',\n configuredDb.type || configuredDb.config?.type,\n );\n}\n\nfunction omitTestForeignKeyConstraints(\n schema: SchemaDefinition,\n): SchemaDefinition {\n const foreignKeyTargets = new Set(\n schemaForeignKeys(schema).map((foreignKey) => foreignKey.referencesTable),\n );\n return {\n ...schema,\n columns: Object.fromEntries(\n Object.entries(schema.columns).map(([name, column]) => [\n name,\n column.foreignKey ? { ...column, foreignKey: undefined } : column,\n ]),\n ),\n foreignKeys: [],\n dependencies: schema.dependencies.filter(\n (dependency) => !foreignKeyTargets.has(dependency),\n ),\n };\n}\n\nfunction resolveRequestedSchemaClassName(className: string): string {\n const registered = ObjectRegistry.getClass(className);\n if (!registered) {\n return className;\n }\n\n if (\n !isCollectionRegistration(\n className,\n registered,\n collectionRegistrationLookup,\n )\n ) {\n const stiBase = ObjectRegistry.getSTIBase(className);\n return stiBase ? resolveSTIBaseLookupName(className, stiBase) : className;\n }\n\n return resolveCollectionSchemaClassName(className, registered);\n}\n\nfunction resolveRequestedSchemaClassNames(classNames: string[]): string[] {\n const resolved: string[] = [];\n const seen = new Set<string>();\n\n for (const className of classNames) {\n const schemaClassName = resolveRequestedSchemaClassName(className);\n if (seen.has(schemaClassName)) {\n continue;\n }\n\n seen.add(schemaClassName);\n resolved.push(schemaClassName);\n }\n\n return resolved;\n}\n\n/**\n * Creates an in-memory test database with schemas pre-created\n *\n * This utility is designed for testing. It creates an in-memory database\n * and initializes all registered SMRT object schemas using the **same\n * schema generation logic** as the production migration system.\n *\n * **Key features:**\n * - Uses `SchemaGenerator.generateSQL()` - the single source of truth for DDL\n * - Handles STI (Single Table Inheritance) correctly\n * - Creates system tables for framework functionality\n * - Safe for parallel test execution: each call creates an isolated\n * in-memory instance with its own embedded write-queue identity, so\n * unrelated `:memory:` databases never serialize writes against each\n * other (#2707). A `cache=shared` URL is the deliberate exception: it\n * asks SQLite/DuckDB to genuinely share the underlying database, so it\n * keeps sharing one write-queue identity too.\n *\n * @param options - Configuration options\n * @returns Promise resolving to configured DatabaseInterface\n *\n * @example\n * ```typescript\n * // Basic usage - all registered schemas\n * const db = await getTestDatabase();\n *\n * // Specific classes only\n * const db = await getTestDatabase({ classes: ['Council', 'Meeting'] });\n *\n * // JSON adapter instead of SQLite\n * const db = await getTestDatabase({ type: 'json' });\n *\n * // Native DuckDB adapter\n * const db = await getTestDatabase({ type: 'duckdb', url: ':memory:' });\n *\n * // File-based database for persistent tests\n * const db = await getTestDatabase({ type: 'sqlite', url: '/tmp/test.db' });\n *\n * // Initialize schemas in an existing database\n * const myDb = await getDatabase({ type: 'sqlite', url: ':memory:' });\n * await getTestDatabase({ db: myDb });\n *\n * // Skip system tables (rare use case)\n * const db = await getTestDatabase({ includeSystemTables: false });\n * ```\n */\nexport async function getTestDatabase(\n options: TestDatabaseOptions = {},\n): Promise<DatabaseInterface> {\n const {\n type,\n url = ':memory:',\n db: existingDb,\n classes,\n includeSystemTables = true,\n omitForeignKeyConstraints = false,\n } = options;\n\n // Use existing database or create new one\n const db =\n existingDb ??\n (await getDatabase({\n type: type ?? 'sqlite',\n url,\n __smrtSkipVitestSchemaPreparation: true,\n } as TestDatabaseConnectionOptions));\n\n // Initialize system tables (same as production)\n if (includeSystemTables) {\n await initializeSystemTables(db);\n }\n\n // Get class names to setup. The framework-base filter below only applies\n // when the caller left `classes` implicit: an explicit list naming a\n // framework base class must still be honored (#2645).\n const isImplicitClassList = classes === undefined;\n const classNames = resolveRequestedSchemaClassNames(\n classes ?? ObjectRegistry.getQualifiedClassNames(),\n );\n\n // Skip if no classes registered\n if (classNames.length === 0) {\n return db;\n }\n\n // Use the same schema generation as production\n const schemaGenerator = new SchemaGenerator();\n const ddlEngine = resolveTestDatabaseDDLEngine(\n type,\n db,\n existingDb !== undefined,\n );\n\n // Collect every table before executing DDL so dependency ordering and cycle\n // handling are identical to production migration/schema paths (#2413).\n const schemas = new Map<\n string,\n { schema: SchemaDefinition; className: string }\n >();\n const authoritativeSchemas = ObjectRegistry.getAllSchemasAsDefinitions();\n\n for (const className of classNames) {\n // R11: the registration carries idType (native uuid vs text); read it\n // below when building runtimeSchemaConfig.\n const registered = ObjectRegistry.getClass(className);\n // Skip STI children - their schema is part of the base class table.\n // main (#1324): isSTIChild() compares RegisteredClass *identity* rather\n // than raw strings, so it stays correct under R5-canon (getSTIBase returns\n // qualified names) while also handling collection/override registrations.\n if (isSTIChild(className)) {\n continue;\n }\n\n // Framework abstract base classes (SmrtObject, SmrtClass, ...) are\n // scaffolding, not resources, and produce phantom smrt_objects/\n // smrt_classes/smrt_collections/smrt_hierarchicals/\n // smrt_polymorphic_associations tables when the default registry\n // snapshot happens to include them (#2645). Mirrors the production\n // `buildMergedTableSchemas()` filter in `registry/schema-builder.ts`, but\n // only for the implicit class list — an explicit `classes: [...]` caller\n // that names a framework base class is still honored.\n if (\n isImplicitClassList &&\n isFrameworkBaseClass(registered?.name, registered?.packageName)\n ) {\n continue;\n }\n\n const tableName = ObjectRegistry.getTableName(className);\n if (!tableName || schemas.has(tableName)) {\n continue;\n }\n\n const fields = await ObjectRegistry.getAllFields(className);\n const strategy = ObjectRegistry.getTableStrategy(className);\n // Mirrors `schema/utils.ts`: this bag is rebuilt by hand, so every `@smrt()`\n // option the generator reads has to be listed or tests silently diverge\n // from the production schema. `indexes` (#2357) was missing from both.\n const runtimeSchemaConfig = {\n conflictColumns: ObjectRegistry.getConflictColumns(className),\n idType: registered?.config.idType,\n indexes: registered?.config.indexes,\n registry: ObjectRegistry,\n };\n\n // Generate schema through the registry paths. Production (`manifest.json`\n // and therefore `smrt db:migrate`) uses the manifest paths\n // (`generateSTISchemaFromManifest` / `generateCTISchemaFromManifest`);\n // the two families are held to the same column and index sets by\n // `src/schema/schema-path-parity.test.ts` (#2359) — do not assume they\n // agree by construction.\n const schema =\n strategy === 'sti'\n ? await schemaGenerator.generateSTISchemaFromRegistry(\n className,\n tableName,\n fields,\n runtimeSchemaConfig,\n )\n : schemaGenerator.generateSchemaFromRegistry(\n className,\n tableName,\n fields,\n runtimeSchemaConfig,\n );\n\n // Manifest registrations can carry explicit FK actions that differ from\n // decorator defaults. The deterministic merged table schema preserves\n // those authoritative actions while still backfilling runtime-only fields.\n const authoritativeSchema = authoritativeSchemas[tableName];\n const effectiveColumns = authoritativeSchema\n ? Object.fromEntries(\n Object.entries(schema.columns).map(([name, column]) => {\n const manifestColumn = authoritativeSchema.columns[name];\n return [\n name,\n manifestColumn\n ? { ...column, foreignKey: manifestColumn.foreignKey }\n : column,\n ];\n }),\n )\n : schema.columns;\n const effectiveForeignKeys = schemaForeignKeys({\n columns: effectiveColumns,\n foreignKeys: [],\n });\n const effectiveSchema: SchemaDefinition = {\n ...schema,\n columns: effectiveColumns,\n foreignKeys: effectiveForeignKeys,\n dependencies: effectiveForeignKeys\n .map((foreignKey) => foreignKey.referencesTable)\n .filter((dependency) => dependency !== tableName),\n };\n\n schemas.set(tableName, {\n schema: omitForeignKeyConstraints\n ? omitTestForeignKeyConstraints(effectiveSchema)\n : effectiveSchema,\n className,\n });\n }\n\n // Explicitly requested test classes still need their registered parents.\n // Add the authoritative dependency closure so a child-only request cannot\n // emit a physical reference to a table this helper never creates.\n const addDependencies = (schema: SchemaDefinition): void => {\n for (const foreignKey of schemaForeignKeysForEngine(schema, ddlEngine)) {\n if (schemas.has(foreignKey.referencesTable)) continue;\n const dependency = authoritativeSchemas[foreignKey.referencesTable];\n if (!dependency) continue;\n const effectiveDependency = omitForeignKeyConstraints\n ? omitTestForeignKeyConstraints(dependency)\n : dependency;\n schemas.set(dependency.tableName, {\n schema: effectiveDependency,\n className: dependency.tableName,\n });\n addDependencies(effectiveDependency);\n }\n };\n for (const { schema } of [...schemas.values()]) addDependencies(schema);\n\n const tablePlan = planForeignKeyCreation(\n [...schemas.values()].map(({ schema }) => schema),\n ddlEngine,\n );\n const ddlStrategy = (await import('../schema/ddl/index.js')).getDDLStrategy(\n ddlEngine,\n );\n\n for (const schema of tablePlan.schemas) {\n const className = schemas.get(schema.tableName)?.className ?? 'unknown';\n const ddl = schemaGenerator.generateSQL(schema, ddlEngine);\n\n try {\n await db.query(ddl);\n\n // Create indexes (use DDL strategy so jsonPath / where / etc. render)\n const indexStatements = ddlStrategy.generateIndexes(schema);\n for (const indexSQL of indexStatements) {\n try {\n await db.query(indexSQL);\n } catch (indexError) {\n // Log but don't fail on index creation errors\n // Some indexes may fail if columns don't exist (STI meta fields)\n console.warn(\n `[getTestDatabase] Warning: Failed to create index: ${indexError instanceof Error ? indexError.message : String(indexError)} (SQL: ${indexSQL})`,\n );\n }\n }\n } catch (error) {\n // Provide helpful error message for table creation failures\n throw new Error(\n `[getTestDatabase] Failed to create table '${schema.tableName}' for class '${className}': ${error instanceof Error ? error.message : String(error)}`,\n { cause: error },\n );\n }\n }\n\n for (const statement of tablePlan.deferredStatements) {\n try {\n await db.query(statement);\n } catch (error) {\n throw new Error(\n `[getTestDatabase] Failed to add deferred foreign-key constraint: ${error instanceof Error ? error.message : String(error)} (SQL: ${statement})`,\n { cause: error },\n );\n }\n }\n\n return db;\n}\n\n/**\n * Initialize SMRT system tables in a database\n *\n * System tables use _smrt_ prefix and store framework metadata.\n * All statements use `IF NOT EXISTS` for idempotency.\n *\n * @param db - Database interface to initialize\n */\nasync function initializeSystemTables(db: DatabaseInterface): Promise<void> {\n await ensureLegacySystemTableCompatibility(db);\n\n const configuredDb = db as DatabaseInterface & {\n config?: { type?: string; url?: string };\n type?: string;\n };\n const engine = detectEngine(\n db.url || configuredDb.config?.url || '',\n configuredDb.type || configuredDb.config?.type,\n );\n\n // Split multi-statement SQL into individual statements\n const allStatements: string[] = [];\n for (const multiStatementSQL of getSystemTableDDL(engine)) {\n const statements = multiStatementSQL\n .split(';')\n .map((s) => s.trim())\n .filter((s) => s.length > 0);\n allStatements.push(...statements);\n }\n\n // Use db.query() — system tables use CREATE TABLE/INDEX IF NOT EXISTS\n // which databases handle natively without per-column existence checks.\n for (const statement of allStatements) {\n await db.query(statement);\n }\n\n // PostgreSQL keeps the best-effort append boundary in a function that must\n // be executed whole rather than included in semicolon-split portable DDL.\n await ensurePostgresChangeFeedAppendFunction(db);\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"database.js","names":[],"sources":["../../src/testing/database.ts"],"sourcesContent":["/**\n * Test database utilities for SMRT framework\n *\n * Provides `getTestDatabase()` which creates an in-memory database with all\n * registered SMRT object schemas. Uses the same schema generation logic as\n * the migration system to ensure consistency between test and production.\n *\n * @example\n * ```typescript\n * import { getTestDatabase } from '@happyvertical/smrt-core/testing';\n *\n * beforeEach(async () => {\n * const db = await getTestDatabase();\n * collection = await MyCollection.create({ db });\n * });\n * ```\n */\n\nimport type { DatabaseInterface } from '@happyvertical/sql';\nimport { getDatabase } from '@happyvertical/sql';\nimport { ensurePostgresChangeFeedAppendFunction } from '../change-feed.js';\nimport {\n type CollectionRegistrationLookup,\n isCollectionRegistration,\n resolveCollectionItemClassName,\n resolveRelatedRegistration,\n} from '../registry/collection-resolution.js';\nimport { recordRegistryDiagnostic } from '../registry/diagnostics.js';\nimport { isFrameworkBaseClass } from '../registry/framework-base-classes.js';\nimport { ObjectRegistry } from '../registry.js';\nimport { detectEngine } from '../schema/ddl/index.js';\nimport {\n schemaForeignKeys,\n schemaForeignKeysForEngine,\n} from '../schema/foreign-key-ddl.js';\nimport { planForeignKeyCreation } from '../schema/foreign-key-planner.js';\nimport { SchemaGenerator } from '../schema/generator.js';\nimport type { SchemaDefinition } from '../schema/types.js';\nimport { ensureLegacySystemTableCompatibility } from '../system/compatibility.js';\nimport { getSystemTableDDL } from '../system/schema.js';\n\ntype TestDatabaseConnectionOptions = Parameters<typeof getDatabase>[0] & {\n __smrtSkipVitestSchemaPreparation?: boolean;\n};\n\nfunction resolveSTIBaseRegistration(className: string, stiBaseName: string) {\n const registered = ObjectRegistry.getClass(className);\n\n if (stiBaseName.includes(':')) {\n return ObjectRegistry.getClass(stiBaseName);\n }\n\n if (registered?.packageName) {\n const samePackageBase = ObjectRegistry.getClassInPackage(\n registered.packageName,\n stiBaseName,\n );\n if (samePackageBase) {\n return samePackageBase;\n }\n }\n\n return ObjectRegistry.getClass(stiBaseName);\n}\n\nfunction resolveSTIBaseLookupName(\n className: string,\n stiBaseName: string,\n): string {\n const stiBase = resolveSTIBaseRegistration(className, stiBaseName);\n return stiBase?.qualifiedName || stiBase?.name || stiBaseName;\n}\n\nfunction isSTIChild(className: string): boolean {\n const stiBaseName = ObjectRegistry.getSTIBase(className);\n if (!stiBaseName) {\n return false;\n }\n\n const registered = ObjectRegistry.getClass(className);\n const stiBase = resolveSTIBaseRegistration(className, stiBaseName);\n\n if (registered && stiBase) {\n return registered !== stiBase;\n }\n\n return stiBaseName !== className;\n}\n\ntype RegisteredSchemaClass = NonNullable<\n ReturnType<typeof ObjectRegistry.getClass>\n>;\n\nconst collectionRegistrationLookup: CollectionRegistrationLookup = {\n findClass: (className) => ObjectRegistry.getClass(className),\n findClassInPackage: (packageName, className) =>\n ObjectRegistry.getClassInPackage(packageName, className),\n getInheritanceChain: (className) =>\n ObjectRegistry.getInheritanceChain(className),\n};\n\nfunction resolveCollectionSchemaClassName(\n className: string,\n registered: RegisteredSchemaClass,\n): string {\n const itemClassName = resolveCollectionItemClassName(\n className,\n registered,\n collectionRegistrationLookup,\n );\n if (itemClassName) {\n const itemRegistration = resolveRelatedRegistration(\n itemClassName,\n registered,\n collectionRegistrationLookup,\n );\n const itemLookupName =\n itemRegistration?.qualifiedName ||\n itemRegistration?.name ||\n itemClassName;\n const stiBase = ObjectRegistry.getSTIBase(itemLookupName);\n return stiBase\n ? resolveSTIBaseLookupName(itemLookupName, stiBase)\n : itemLookupName;\n }\n\n const tableName =\n registered.schema?.tableName ||\n registered.config.tableName ||\n ObjectRegistry.getTableName(className);\n if (!tableName) {\n return className;\n }\n\n const collectionPackage = registered.packageName;\n\n for (const candidate of ObjectRegistry.getAllClasses().values()) {\n if (\n candidate === registered ||\n isCollectionRegistration(\n candidate.qualifiedName || candidate.name,\n candidate,\n collectionRegistrationLookup,\n )\n ) {\n continue;\n }\n\n const candidateTableName =\n candidate.schema?.tableName || candidate.config.tableName;\n if (candidateTableName !== tableName) {\n continue;\n }\n\n if (\n collectionPackage &&\n candidate.packageName &&\n candidate.packageName !== collectionPackage\n ) {\n continue;\n }\n\n const candidateLookupName = candidate.qualifiedName || candidate.name;\n const stiBase = ObjectRegistry.getSTIBase(candidateLookupName);\n return stiBase\n ? resolveSTIBaseLookupName(candidateLookupName, stiBase)\n : candidateLookupName;\n }\n\n return className;\n}\n\n/**\n * Options for creating a test database\n */\nexport interface TestDatabaseOptions {\n /**\n * Database type (default: 'sqlite')\n * - 'sqlite': SQLite database\n * - 'json': JSON adapter (stores data as JSON files with DuckDB for querying)\n * - 'duckdb': Native DuckDB database\n * - 'postgres': PostgreSQL database (normally supplied through `db`)\n */\n type?: 'sqlite' | 'json' | 'duckdb' | 'postgres';\n\n /**\n * Database URL (default: ':memory:')\n * Use ':memory:' for in-memory databases (fastest for tests)\n * Or provide a file path for persistent test databases\n */\n url?: string;\n\n /**\n * Pre-existing database to initialize schemas in.\n * If provided, `type` and `url` are ignored.\n */\n db?: DatabaseInterface;\n\n /**\n * Specific classes to setup schemas for.\n * If not provided, sets up schemas for all registered classes.\n */\n classes?: string[];\n\n /**\n * Whether to include system tables (default: true)\n * System tables include _smrt_contexts, _smrt_migrations, etc.\n */\n includeSystemTables?: boolean;\n\n /**\n * Explicitly omit physical FK constraints for adapter/query tests whose\n * subject is unrelated to referential enforcement. Defaults to `false`.\n * This is required (and deliberately noisy at the call site) when a DuckDB\n * fixture uses generated `ON UPDATE CASCADE`, which DuckDB cannot enforce.\n */\n omitForeignKeyConstraints?: boolean;\n}\n\n/** Resolve the DDL dialect used when preparing an existing test database. */\nexport function resolveTestDatabaseDDLEngine(\n type: TestDatabaseOptions['type'],\n db: DatabaseInterface,\n inferFromDatabase = false,\n): 'sqlite' | 'json' | 'duckdb' | 'postgres' {\n // When this helper created the adapter, the requested type is authoritative.\n // Native DuckDB and JSON-on-DuckDB both expose exportTable(), so capability\n // inference must not override an explicit native DuckDB request.\n if (!inferFromDatabase) {\n return type ?? 'sqlite';\n }\n\n const configuredDb = db as DatabaseInterface & {\n config?: { type?: string; url?: string };\n type?: string;\n exportTable?: unknown;\n inferSchemaFromJSON?: unknown;\n getTableLoadErrors?: unknown;\n };\n\n if (\n typeof configuredDb.inferSchemaFromJSON === 'function' ||\n typeof configuredDb.getTableLoadErrors === 'function'\n ) {\n return 'json';\n }\n\n if (typeof configuredDb.exportTable === 'function') {\n // The native DuckDB adapter exposes schema evolution capabilities that the\n // JSON adapter does not. Keep exportTable-only test doubles compatible with\n // the historical JSON structural marker.\n if (\n typeof configuredDb.getTableSchema === 'function' &&\n typeof configuredDb.alterTable?.addColumn === 'function'\n ) {\n return 'duckdb';\n }\n return 'json';\n }\n\n return detectEngine(\n db.url || configuredDb.config?.url || '',\n configuredDb.type || configuredDb.config?.type,\n );\n}\n\nfunction omitTestForeignKeyConstraints(\n schema: SchemaDefinition,\n): SchemaDefinition {\n const foreignKeyTargets = new Set(\n schemaForeignKeys(schema).map((foreignKey) => foreignKey.referencesTable),\n );\n return {\n ...schema,\n columns: Object.fromEntries(\n Object.entries(schema.columns).map(([name, column]) => [\n name,\n column.foreignKey ? { ...column, foreignKey: undefined } : column,\n ]),\n ),\n foreignKeys: [],\n dependencies: schema.dependencies.filter(\n (dependency) => !foreignKeyTargets.has(dependency),\n ),\n };\n}\n\nfunction resolveRequestedSchemaClassName(className: string): string {\n const registered = ObjectRegistry.getClass(className);\n if (!registered) {\n return className;\n }\n\n if (\n !isCollectionRegistration(\n className,\n registered,\n collectionRegistrationLookup,\n )\n ) {\n const stiBase = ObjectRegistry.getSTIBase(className);\n return stiBase ? resolveSTIBaseLookupName(className, stiBase) : className;\n }\n\n return resolveCollectionSchemaClassName(className, registered);\n}\n\nfunction resolveRequestedSchemaClassNames(classNames: string[]): string[] {\n const resolved: string[] = [];\n const seen = new Set<string>();\n\n for (const className of classNames) {\n const schemaClassName = resolveRequestedSchemaClassName(className);\n if (seen.has(schemaClassName)) {\n continue;\n }\n\n seen.add(schemaClassName);\n resolved.push(schemaClassName);\n }\n\n return resolved;\n}\n\n/**\n * Creates an in-memory test database with schemas pre-created\n *\n * This utility is designed for testing. It creates an in-memory database\n * and initializes all registered SMRT object schemas using the **same\n * schema generation logic** as the production migration system.\n *\n * **Key features:**\n * - Uses `SchemaGenerator.generateSQL()` - the single source of truth for DDL\n * - Handles STI (Single Table Inheritance) correctly\n * - Creates system tables for framework functionality\n * - Safe for parallel test execution: each call creates an isolated\n * in-memory instance with its own embedded write-queue identity, so\n * unrelated `:memory:` databases never serialize writes against each\n * other (#2707). A `cache=shared` URL is the deliberate exception: it\n * asks SQLite/DuckDB to genuinely share the underlying database, so it\n * keeps sharing one write-queue identity too.\n *\n * @param options - Configuration options\n * @returns Promise resolving to configured DatabaseInterface\n *\n * @example\n * ```typescript\n * // Basic usage - all registered schemas\n * const db = await getTestDatabase();\n *\n * // Specific classes only\n * const db = await getTestDatabase({ classes: ['Council', 'Meeting'] });\n *\n * // JSON adapter instead of SQLite\n * const db = await getTestDatabase({ type: 'json' });\n *\n * // Native DuckDB adapter\n * const db = await getTestDatabase({ type: 'duckdb', url: ':memory:' });\n *\n * // File-based database for persistent tests\n * const db = await getTestDatabase({ type: 'sqlite', url: '/tmp/test.db' });\n *\n * // Initialize schemas in an existing database\n * const myDb = await getDatabase({ type: 'sqlite', url: ':memory:' });\n * await getTestDatabase({ db: myDb });\n *\n * // Skip system tables (rare use case)\n * const db = await getTestDatabase({ includeSystemTables: false });\n * ```\n */\nexport async function getTestDatabase(\n options: TestDatabaseOptions = {},\n): Promise<DatabaseInterface> {\n const {\n type,\n url = ':memory:',\n db: existingDb,\n classes,\n includeSystemTables = true,\n omitForeignKeyConstraints = false,\n } = options;\n\n // Use existing database or create new one\n const db =\n existingDb ??\n (await getDatabase({\n type: type ?? 'sqlite',\n url,\n __smrtSkipVitestSchemaPreparation: true,\n } as TestDatabaseConnectionOptions));\n\n // Initialize system tables (same as production)\n if (includeSystemTables) {\n await initializeSystemTables(db);\n }\n\n // Get class names to setup. The framework-base filter below only applies\n // when the caller left `classes` implicit: an explicit list naming a\n // framework base class must still be honored (#2645).\n const isImplicitClassList = classes === undefined;\n const classNames = resolveRequestedSchemaClassNames(\n classes ?? ObjectRegistry.getQualifiedClassNames(),\n );\n\n // Skip if no classes registered. When the caller left `classes` implicit,\n // an empty registry usually means registration never reached this\n // process/module instance of ObjectRegistry (e.g. a Vitest plugin hook\n // registering manifests in a different worker/process than the test code\n // — #2750) rather than a deliberate \"system tables only\" setup. Warn (never\n // throw by default) so that failure surfaces here instead of as a much\n // later, harder-to-diagnose \"no such table\" error. An explicit empty\n // `classes: []` array is a deliberate choice and stays silent.\n if (classNames.length === 0) {\n if (isImplicitClassList) {\n const emptyRegistryMessage =\n 'getTestDatabase() found zero registered classes in ObjectRegistry and ' +\n 'created only system tables. If you expected model tables, the ' +\n 'registry is likely empty in this process — for smrt-vitest ' +\n 'consumers, confirm registration (smrtVitestPlugin() manifest ' +\n 'loading, or your setupFiles) actually runs in the same process ' +\n 'as your test code, or pass an explicit `classes` list to ' +\n 'getTestDatabase() to opt out of this check.';\n // recordRegistryDiagnostic('warn', ...) only buffers -- nothing reads\n // that buffer automatically (only strict-mode 'error' severity throws;\n // printing the buffer requires an explicit flushRegistryDiagnostics()\n // call this package never makes). Pair it with an immediate\n // console.warn so the failure is actually visible at the point of the\n // problem, which is the entire point of this diagnostic (#2750) --\n // the buffered record stays available for programmatic inspection via\n // getRegistryDiagnostics().\n console.warn(`[getTestDatabase] ${emptyRegistryMessage}`);\n recordRegistryDiagnostic(\n 'warn',\n 'TEST_DATABASE_EMPTY_IMPLICIT_REGISTRY',\n emptyRegistryMessage,\n );\n }\n return db;\n }\n\n // Use the same schema generation as production\n const schemaGenerator = new SchemaGenerator();\n const ddlEngine = resolveTestDatabaseDDLEngine(\n type,\n db,\n existingDb !== undefined,\n );\n\n // Collect every table before executing DDL so dependency ordering and cycle\n // handling are identical to production migration/schema paths (#2413).\n const schemas = new Map<\n string,\n { schema: SchemaDefinition; className: string }\n >();\n const authoritativeSchemas = ObjectRegistry.getAllSchemasAsDefinitions();\n\n for (const className of classNames) {\n // R11: the registration carries idType (native uuid vs text); read it\n // below when building runtimeSchemaConfig.\n const registered = ObjectRegistry.getClass(className);\n // Skip STI children - their schema is part of the base class table.\n // main (#1324): isSTIChild() compares RegisteredClass *identity* rather\n // than raw strings, so it stays correct under R5-canon (getSTIBase returns\n // qualified names) while also handling collection/override registrations.\n if (isSTIChild(className)) {\n continue;\n }\n\n // Framework abstract base classes (SmrtObject, SmrtClass, ...) are\n // scaffolding, not resources, and produce phantom smrt_objects/\n // smrt_classes/smrt_collections/smrt_hierarchicals/\n // smrt_polymorphic_associations tables when the default registry\n // snapshot happens to include them (#2645). Mirrors the production\n // `buildMergedTableSchemas()` filter in `registry/schema-builder.ts`, but\n // only for the implicit class list — an explicit `classes: [...]` caller\n // that names a framework base class is still honored.\n if (\n isImplicitClassList &&\n isFrameworkBaseClass(registered?.name, registered?.packageName)\n ) {\n continue;\n }\n\n const tableName = ObjectRegistry.getTableName(className);\n if (!tableName || schemas.has(tableName)) {\n continue;\n }\n\n const fields = await ObjectRegistry.getAllFields(className);\n const strategy = ObjectRegistry.getTableStrategy(className);\n // Mirrors `schema/utils.ts`: this bag is rebuilt by hand, so every `@smrt()`\n // option the generator reads has to be listed or tests silently diverge\n // from the production schema. `indexes` (#2357) was missing from both.\n const runtimeSchemaConfig = {\n conflictColumns: ObjectRegistry.getConflictColumns(className),\n idType: registered?.config.idType,\n indexes: registered?.config.indexes,\n registry: ObjectRegistry,\n };\n\n // Generate schema through the registry paths. Production (`manifest.json`\n // and therefore `smrt db:migrate`) uses the manifest paths\n // (`generateSTISchemaFromManifest` / `generateCTISchemaFromManifest`);\n // the two families are held to the same column and index sets by\n // `src/schema/schema-path-parity.test.ts` (#2359) — do not assume they\n // agree by construction.\n const schema =\n strategy === 'sti'\n ? await schemaGenerator.generateSTISchemaFromRegistry(\n className,\n tableName,\n fields,\n runtimeSchemaConfig,\n )\n : schemaGenerator.generateSchemaFromRegistry(\n className,\n tableName,\n fields,\n runtimeSchemaConfig,\n );\n\n // Manifest registrations can carry explicit FK actions that differ from\n // decorator defaults. The deterministic merged table schema preserves\n // those authoritative actions while still backfilling runtime-only fields.\n const authoritativeSchema = authoritativeSchemas[tableName];\n const effectiveColumns = authoritativeSchema\n ? Object.fromEntries(\n Object.entries(schema.columns).map(([name, column]) => {\n const manifestColumn = authoritativeSchema.columns[name];\n return [\n name,\n manifestColumn\n ? { ...column, foreignKey: manifestColumn.foreignKey }\n : column,\n ];\n }),\n )\n : schema.columns;\n const effectiveForeignKeys = schemaForeignKeys({\n columns: effectiveColumns,\n foreignKeys: [],\n });\n const effectiveSchema: SchemaDefinition = {\n ...schema,\n columns: effectiveColumns,\n foreignKeys: effectiveForeignKeys,\n dependencies: effectiveForeignKeys\n .map((foreignKey) => foreignKey.referencesTable)\n .filter((dependency) => dependency !== tableName),\n };\n\n schemas.set(tableName, {\n schema: omitForeignKeyConstraints\n ? omitTestForeignKeyConstraints(effectiveSchema)\n : effectiveSchema,\n className,\n });\n }\n\n // Explicitly requested test classes still need their registered parents.\n // Add the authoritative dependency closure so a child-only request cannot\n // emit a physical reference to a table this helper never creates.\n const addDependencies = (schema: SchemaDefinition): void => {\n for (const foreignKey of schemaForeignKeysForEngine(schema, ddlEngine)) {\n if (schemas.has(foreignKey.referencesTable)) continue;\n const dependency = authoritativeSchemas[foreignKey.referencesTable];\n if (!dependency) continue;\n const effectiveDependency = omitForeignKeyConstraints\n ? omitTestForeignKeyConstraints(dependency)\n : dependency;\n schemas.set(dependency.tableName, {\n schema: effectiveDependency,\n className: dependency.tableName,\n });\n addDependencies(effectiveDependency);\n }\n };\n for (const { schema } of [...schemas.values()]) addDependencies(schema);\n\n const tablePlan = planForeignKeyCreation(\n [...schemas.values()].map(({ schema }) => schema),\n ddlEngine,\n );\n const ddlStrategy = (await import('../schema/ddl/index.js')).getDDLStrategy(\n ddlEngine,\n );\n\n for (const schema of tablePlan.schemas) {\n const className = schemas.get(schema.tableName)?.className ?? 'unknown';\n const ddl = schemaGenerator.generateSQL(schema, ddlEngine);\n\n try {\n await db.query(ddl);\n\n // Create indexes (use DDL strategy so jsonPath / where / etc. render)\n const indexStatements = ddlStrategy.generateIndexes(schema);\n for (const indexSQL of indexStatements) {\n try {\n await db.query(indexSQL);\n } catch (indexError) {\n // Log but don't fail on index creation errors\n // Some indexes may fail if columns don't exist (STI meta fields)\n console.warn(\n `[getTestDatabase] Warning: Failed to create index: ${indexError instanceof Error ? indexError.message : String(indexError)} (SQL: ${indexSQL})`,\n );\n }\n }\n } catch (error) {\n // Provide helpful error message for table creation failures\n throw new Error(\n `[getTestDatabase] Failed to create table '${schema.tableName}' for class '${className}': ${error instanceof Error ? error.message : String(error)}`,\n { cause: error },\n );\n }\n }\n\n for (const statement of tablePlan.deferredStatements) {\n try {\n await db.query(statement);\n } catch (error) {\n throw new Error(\n `[getTestDatabase] Failed to add deferred foreign-key constraint: ${error instanceof Error ? error.message : String(error)} (SQL: ${statement})`,\n { cause: error },\n );\n }\n }\n\n return db;\n}\n\n/**\n * Initialize SMRT system tables in a database\n *\n * System tables use _smrt_ prefix and store framework metadata.\n * All statements use `IF NOT EXISTS` for idempotency.\n *\n * @param db - Database interface to initialize\n */\nasync function initializeSystemTables(db: DatabaseInterface): Promise<void> {\n await ensureLegacySystemTableCompatibility(db);\n\n const configuredDb = db as DatabaseInterface & {\n config?: { type?: string; url?: string };\n type?: string;\n };\n const engine = detectEngine(\n db.url || configuredDb.config?.url || '',\n configuredDb.type || configuredDb.config?.type,\n );\n\n // Split multi-statement SQL into individual statements\n const allStatements: string[] = [];\n for (const multiStatementSQL of getSystemTableDDL(engine)) {\n const statements = multiStatementSQL\n .split(';')\n .map((s) => s.trim())\n .filter((s) => s.length > 0);\n allStatements.push(...statements);\n }\n\n // Use db.query() — system tables use CREATE TABLE/INDEX IF NOT EXISTS\n // which databases handle natively without per-column existence checks.\n for (const statement of allStatements) {\n await db.query(statement);\n }\n\n // PostgreSQL keeps the best-effort append boundary in a function that must\n // be executed whole rather than included in semicolon-split portable DDL.\n await ensurePostgresChangeFeedAppendFunction(db);\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { SvelteKitOptions } from './sveltekit-generator.js';
|
|
2
|
+
/** Whether `@happyvertical/smrt-dev-mcp/dev-plane` resolves from the consumer. */
|
|
3
|
+
export declare function consumerHasSmrtDevMcp(projectRoot: string): boolean;
|
|
4
|
+
export declare function generateDevPlaneRoute(projectRoot: string, options: SvelteKitOptions): boolean;
|
|
5
|
+
//# sourceMappingURL=dev-plane-route.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"dev-plane-route.d.ts","sourceRoot":"","sources":["../../src/vite-plugin/dev-plane-route.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAOH,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,0BAA0B,CAAC;AAEjE,kFAAkF;AAClF,wBAAgB,qBAAqB,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAUlE;AAED,wBAAgB,qBAAqB,CACnC,WAAW,EAAE,MAAM,EACnB,OAAO,EAAE,gBAAgB,GACxB,OAAO,CA8BT"}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { AUTO_GENERATED_ROUTE_HEADER } from "./route-header.js";
|
|
2
|
+
import { existsSync, mkdirSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { createRequire } from "node:module";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { pathToFileURL } from "node:url";
|
|
6
|
+
//#region src/vite-plugin/dev-plane-route.ts
|
|
7
|
+
/**
|
|
8
|
+
* Generated `_dev/[...tool]/+server.ts` route: the in-app runtime dev-plane
|
|
9
|
+
* (#2782). Opt-in via `sveltekit: { devPlaneRoute: { enabled: true } }`.
|
|
10
|
+
*
|
|
11
|
+
* The route is dev-only twice over: it 404s unless SvelteKit's `dev` flag is
|
|
12
|
+
* set, and it refuses to start without `SMRT_DEV_MCP_TOKEN`. Everything else
|
|
13
|
+
* — loopback checks, bearer auth, the positive read-only catalog, the MCP
|
|
14
|
+
* endpoint — lives in `@happyvertical/smrt-dev-mcp/dev-plane`, which the
|
|
15
|
+
* generated file imports at consumer runtime. Like `_resources`, the route is
|
|
16
|
+
* only emitted when that subpath resolves from the consumer.
|
|
17
|
+
*/
|
|
18
|
+
/** Whether `@happyvertical/smrt-dev-mcp/dev-plane` resolves from the consumer. */
|
|
19
|
+
function consumerHasSmrtDevMcp(projectRoot) {
|
|
20
|
+
try {
|
|
21
|
+
createRequire(pathToFileURL(join(projectRoot, "package.json")).href).resolve("@happyvertical/smrt-dev-mcp/dev-plane");
|
|
22
|
+
return true;
|
|
23
|
+
} catch {
|
|
24
|
+
return false;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
function generateDevPlaneRoute(projectRoot, options) {
|
|
28
|
+
if (options.devPlaneRoute?.enabled !== true) return false;
|
|
29
|
+
if (!consumerHasSmrtDevMcp(projectRoot)) {
|
|
30
|
+
console.log("[smrt] Skipping _dev route - @happyvertical/smrt-dev-mcp/dev-plane is not resolvable; install @happyvertical/smrt-dev-mcp as a devDependency");
|
|
31
|
+
return false;
|
|
32
|
+
}
|
|
33
|
+
const configPath = options.configPath || "src/lib/server";
|
|
34
|
+
const configFileName = (options.configFileName || "smrt.ts").replace(/\.ts$/, "");
|
|
35
|
+
const routeDir = join(projectRoot, options.routesDir, "_dev", "[...tool]");
|
|
36
|
+
if (!existsSync(routeDir)) mkdirSync(routeDir, { recursive: true });
|
|
37
|
+
const filePath = join(routeDir, "+server.ts");
|
|
38
|
+
writeFileSync(filePath, generateDevPlaneRouteTemplate(`$lib/${configPath.replace(/^src\/lib\//, "")}/${configFileName}`), "utf-8");
|
|
39
|
+
console.log(`[smrt] Generated: ${filePath}`);
|
|
40
|
+
return true;
|
|
41
|
+
}
|
|
42
|
+
function generateDevPlaneRouteTemplate(configImport) {
|
|
43
|
+
return `${AUTO_GENERATED_ROUTE_HEADER}
|
|
44
|
+
// DO NOT EDIT - changes will be overwritten
|
|
45
|
+
//
|
|
46
|
+
// In-app runtime dev-plane (#2782). Dev mode only. Serves the read-only
|
|
47
|
+
// runtime tools over JSON (GET|POST /_dev/<tool>, GET /_dev for the catalog)
|
|
48
|
+
// and MCP (POST /_dev/mcp) against this app's live registry and database.
|
|
49
|
+
// Requires SMRT_DEV_MCP_TOKEN; every request must present it as a bearer
|
|
50
|
+
// token from loopback. Never mounted in production builds.
|
|
51
|
+
import { dev } from '$app/environment';
|
|
52
|
+
import { error } from '@sveltejs/kit';
|
|
53
|
+
import type { RequestHandler } from './$types';
|
|
54
|
+
import { createDevPlane, type DevPlane } from '@happyvertical/smrt-dev-mcp/dev-plane';
|
|
55
|
+
import { getSmrtConfig } from '${configImport}';
|
|
56
|
+
|
|
57
|
+
let plane: DevPlane | null = null;
|
|
58
|
+
|
|
59
|
+
function getPlane(): DevPlane | null {
|
|
60
|
+
if (!dev) return null;
|
|
61
|
+
const token = process.env.SMRT_DEV_MCP_TOKEN?.trim();
|
|
62
|
+
if (!token) return null;
|
|
63
|
+
if (!plane) {
|
|
64
|
+
// The default config's db is what the app itself opens; passing its
|
|
65
|
+
// url/type makes the SDK connection cache hand the plane the same handle.
|
|
66
|
+
const db = getSmrtConfig('__smrt_dev_plane__').db as
|
|
67
|
+
| { url?: string; type?: string }
|
|
68
|
+
| undefined;
|
|
69
|
+
plane = createDevPlane({ token, projectRoot: process.cwd(), db });
|
|
70
|
+
}
|
|
71
|
+
return plane;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const handle: RequestHandler = async ({ request, url }) => {
|
|
75
|
+
if (!dev) throw error(404, 'Not found');
|
|
76
|
+
const active = getPlane();
|
|
77
|
+
if (!active) {
|
|
78
|
+
throw error(503, 'SMRT_DEV_MCP_TOKEN is not set; the dev-plane is disabled');
|
|
79
|
+
}
|
|
80
|
+
const base = url.pathname.slice(0, url.pathname.indexOf('/_dev') + '/_dev'.length);
|
|
81
|
+
return active.handleRequest(request, base);
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
export const GET = handle;
|
|
85
|
+
export const POST = handle;
|
|
86
|
+
`;
|
|
87
|
+
}
|
|
88
|
+
//#endregion
|
|
89
|
+
export { consumerHasSmrtDevMcp, generateDevPlaneRoute };
|
|
90
|
+
|
|
91
|
+
//# sourceMappingURL=dev-plane-route.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"dev-plane-route.js","names":[],"sources":["../../src/vite-plugin/dev-plane-route.ts"],"sourcesContent":["/**\n * Generated `_dev/[...tool]/+server.ts` route: the in-app runtime dev-plane\n * (#2782). Opt-in via `sveltekit: { devPlaneRoute: { enabled: true } }`.\n *\n * The route is dev-only twice over: it 404s unless SvelteKit's `dev` flag is\n * set, and it refuses to start without `SMRT_DEV_MCP_TOKEN`. Everything else\n * — loopback checks, bearer auth, the positive read-only catalog, the MCP\n * endpoint — lives in `@happyvertical/smrt-dev-mcp/dev-plane`, which the\n * generated file imports at consumer runtime. Like `_resources`, the route is\n * only emitted when that subpath resolves from the consumer.\n */\n\nimport { existsSync, mkdirSync, writeFileSync } from 'node:fs';\nimport { createRequire } from 'node:module';\nimport { join } from 'node:path';\nimport { pathToFileURL } from 'node:url';\nimport { AUTO_GENERATED_ROUTE_HEADER } from './route-header.js';\nimport type { SvelteKitOptions } from './sveltekit-generator.js';\n\n/** Whether `@happyvertical/smrt-dev-mcp/dev-plane` resolves from the consumer. */\nexport function consumerHasSmrtDevMcp(projectRoot: string): boolean {\n try {\n const consumerRequire = createRequire(\n pathToFileURL(join(projectRoot, 'package.json')).href,\n );\n consumerRequire.resolve('@happyvertical/smrt-dev-mcp/dev-plane');\n return true;\n } catch {\n return false;\n }\n}\n\nexport function generateDevPlaneRoute(\n projectRoot: string,\n options: SvelteKitOptions,\n): boolean {\n if (options.devPlaneRoute?.enabled !== true) {\n return false;\n }\n if (!consumerHasSmrtDevMcp(projectRoot)) {\n console.log(\n '[smrt] Skipping _dev route - @happyvertical/smrt-dev-mcp/dev-plane is ' +\n 'not resolvable; install @happyvertical/smrt-dev-mcp as a devDependency',\n );\n return false;\n }\n const configPath = options.configPath || 'src/lib/server';\n const configFileName = (options.configFileName || 'smrt.ts').replace(\n /\\.ts$/,\n '',\n );\n const routeDir = join(projectRoot, options.routesDir, '_dev', '[...tool]');\n if (!existsSync(routeDir)) {\n mkdirSync(routeDir, { recursive: true });\n }\n const filePath = join(routeDir, '+server.ts');\n writeFileSync(\n filePath,\n generateDevPlaneRouteTemplate(\n `$lib/${configPath.replace(/^src\\/lib\\//, '')}/${configFileName}`,\n ),\n 'utf-8',\n );\n console.log(`[smrt] Generated: ${filePath}`);\n return true;\n}\n\nfunction generateDevPlaneRouteTemplate(configImport: string): string {\n return `${AUTO_GENERATED_ROUTE_HEADER}\n// DO NOT EDIT - changes will be overwritten\n//\n// In-app runtime dev-plane (#2782). Dev mode only. Serves the read-only\n// runtime tools over JSON (GET|POST /_dev/<tool>, GET /_dev for the catalog)\n// and MCP (POST /_dev/mcp) against this app's live registry and database.\n// Requires SMRT_DEV_MCP_TOKEN; every request must present it as a bearer\n// token from loopback. Never mounted in production builds.\nimport { dev } from '$app/environment';\nimport { error } from '@sveltejs/kit';\nimport type { RequestHandler } from './$types';\nimport { createDevPlane, type DevPlane } from '@happyvertical/smrt-dev-mcp/dev-plane';\nimport { getSmrtConfig } from '${configImport}';\n\nlet plane: DevPlane | null = null;\n\nfunction getPlane(): DevPlane | null {\n if (!dev) return null;\n const token = process.env.SMRT_DEV_MCP_TOKEN?.trim();\n if (!token) return null;\n if (!plane) {\n // The default config's db is what the app itself opens; passing its\n // url/type makes the SDK connection cache hand the plane the same handle.\n const db = getSmrtConfig('__smrt_dev_plane__').db as\n | { url?: string; type?: string }\n | undefined;\n plane = createDevPlane({ token, projectRoot: process.cwd(), db });\n }\n return plane;\n}\n\nconst handle: RequestHandler = async ({ request, url }) => {\n if (!dev) throw error(404, 'Not found');\n const active = getPlane();\n if (!active) {\n throw error(503, 'SMRT_DEV_MCP_TOKEN is not set; the dev-plane is disabled');\n }\n const base = url.pathname.slice(0, url.pathname.indexOf('/_dev') + '/_dev'.length);\n return active.handleRequest(request, base);\n};\n\nexport const GET = handle;\nexport const POST = handle;\n`;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;AAoBA,SAAgB,sBAAsB,aAA8B;CAClE,IAAI;EAIF,cAFE,cAAc,KAAK,aAAa,cAAc,CAAC,CAAC,CAAC,IAEnD,CAAA,CAAgB,QAAQ,uCAAuC;EAC/D,OAAO;CACT,QAAQ;EACN,OAAO;CACT;AACF;AAEA,SAAgB,sBACd,aACA,SACS;CACT,IAAI,QAAQ,eAAe,YAAY,MACrC,OAAO;CAET,IAAI,CAAC,sBAAsB,WAAW,GAAG;EACvC,QAAQ,IACN,8IAEF;EACA,OAAO;CACT;CACA,MAAM,aAAa,QAAQ,cAAc;CACzC,MAAM,kBAAkB,QAAQ,kBAAkB,UAAA,CAAW,QAC3D,SACA,EACF;CACA,MAAM,WAAW,KAAK,aAAa,QAAQ,WAAW,QAAQ,WAAW;CACzE,IAAI,CAAC,WAAW,QAAQ,GACtB,UAAU,UAAU,EAAE,WAAW,KAAK,CAAC;CAEzC,MAAM,WAAW,KAAK,UAAU,YAAY;CAC5C,cACE,UACA,8BACE,QAAQ,WAAW,QAAQ,eAAe,EAAE,EAAE,GAAG,gBACnD,GACA,OACF;CACA,QAAQ,IAAI,qBAAqB,UAAU;CAC3C,OAAO;AACT;AAEA,SAAS,8BAA8B,cAA8B;CACnE,OAAO,GAAG,4BAA4B;;;;;;;;;;;;iCAYP,aAAa;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAgC9C"}
|