@metaobjectsdev/cli 0.5.0 → 0.6.0-rc.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +20 -1
- package/dist/src/commands/init.d.ts +1 -0
- package/dist/src/commands/init.d.ts.map +1 -1
- package/dist/src/commands/init.js +34 -5
- package/dist/src/commands/init.js.map +1 -1
- package/dist/src/commands/migrate.d.ts +4 -1
- package/dist/src/commands/migrate.d.ts.map +1 -1
- package/dist/src/commands/migrate.js +233 -5
- package/dist/src/commands/migrate.js.map +1 -1
- package/dist/src/commands/prompt-snapshot.d.ts +2 -0
- package/dist/src/commands/prompt-snapshot.d.ts.map +1 -0
- package/dist/src/commands/prompt-snapshot.js +125 -0
- package/dist/src/commands/prompt-snapshot.js.map +1 -0
- package/dist/src/commands/verify.d.ts +2 -0
- package/dist/src/commands/verify.d.ts.map +1 -0
- package/dist/src/commands/verify.js +93 -0
- package/dist/src/commands/verify.js.map +1 -0
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +22 -1
- package/dist/src/index.js.map +1 -1
- package/dist/src/lib/args.d.ts +18 -1
- package/dist/src/lib/args.d.ts.map +1 -1
- package/dist/src/lib/args.js +39 -1
- package/dist/src/lib/args.js.map +1 -1
- package/dist/src/lib/config.d.ts +11 -1
- package/dist/src/lib/config.d.ts.map +1 -1
- package/dist/src/lib/config.js +10 -1
- package/dist/src/lib/config.js.map +1 -1
- package/dist/src/lib/file-provider.d.ts +7 -0
- package/dist/src/lib/file-provider.d.ts.map +1 -0
- package/dist/src/lib/file-provider.js +33 -0
- package/dist/src/lib/file-provider.js.map +1 -0
- package/dist/src/lib/kysely.d.ts +1 -1
- package/dist/src/lib/kysely.d.ts.map +1 -1
- package/dist/src/lib/kysely.js +3 -0
- package/dist/src/lib/kysely.js.map +1 -1
- package/dist/src/lib/load-metaobjects-config.d.ts.map +1 -1
- package/dist/src/lib/load-metaobjects-config.js +32 -8
- package/dist/src/lib/load-metaobjects-config.js.map +1 -1
- package/dist/src/lib/output.d.ts +3 -2
- package/dist/src/lib/output.d.ts.map +1 -1
- package/dist/src/lib/output.js.map +1 -1
- package/dist/src/lib/payload-field-tree.d.ts +9 -0
- package/dist/src/lib/payload-field-tree.d.ts.map +1 -0
- package/dist/src/lib/payload-field-tree.js +36 -0
- package/dist/src/lib/payload-field-tree.js.map +1 -0
- package/dist/src/lib/projection-migrations.d.ts +2 -1
- package/dist/src/lib/projection-migrations.d.ts.map +1 -1
- package/dist/src/lib/projection-migrations.js +4 -2
- package/dist/src/lib/projection-migrations.js.map +1 -1
- package/dist/src/lib/snapshot.d.ts +11 -0
- package/dist/src/lib/snapshot.d.ts.map +1 -0
- package/dist/src/lib/snapshot.js +36 -0
- package/dist/src/lib/snapshot.js.map +1 -0
- package/dist/src/lib/wrangler.d.ts +18 -0
- package/dist/src/lib/wrangler.d.ts.map +1 -0
- package/dist/src/lib/wrangler.js +30 -0
- package/dist/src/lib/wrangler.js.map +1 -0
- package/package.json +8 -7
- package/src/commands/init.ts +35 -5
- package/src/commands/migrate.ts +287 -5
- package/src/commands/prompt-snapshot.ts +142 -0
- package/src/commands/verify.ts +111 -0
- package/src/index.ts +22 -1
- package/src/lib/args.ts +67 -1
- package/src/lib/config.ts +23 -3
- package/src/lib/file-provider.ts +33 -0
- package/src/lib/kysely.ts +7 -1
- package/src/lib/load-metaobjects-config.ts +32 -8
- package/src/lib/output.ts +4 -2
- package/src/lib/payload-field-tree.ts +47 -0
- package/src/lib/projection-migrations.ts +6 -3
- package/src/lib/snapshot.ts +50 -0
- package/src/lib/wrangler.ts +45 -0
package/src/lib/args.ts
CHANGED
|
@@ -9,6 +9,7 @@ export interface InitFlags {
|
|
|
9
9
|
quiet: boolean;
|
|
10
10
|
printOnly: boolean;
|
|
11
11
|
refreshDocs: boolean;
|
|
12
|
+
d1: boolean;
|
|
12
13
|
}
|
|
13
14
|
|
|
14
15
|
export function parseInitArgs(argv: string[]): InitFlags {
|
|
@@ -19,6 +20,7 @@ export function parseInitArgs(argv: string[]): InitFlags {
|
|
|
19
20
|
quiet: { type: "boolean", default: false },
|
|
20
21
|
"print-only": { type: "boolean", default: false },
|
|
21
22
|
"refresh-docs": { type: "boolean", default: false },
|
|
23
|
+
d1: { type: "boolean", default: false },
|
|
22
24
|
},
|
|
23
25
|
strict: true,
|
|
24
26
|
allowPositionals: false,
|
|
@@ -28,6 +30,7 @@ export function parseInitArgs(argv: string[]): InitFlags {
|
|
|
28
30
|
quiet: !!values.quiet,
|
|
29
31
|
printOnly: !!values["print-only"],
|
|
30
32
|
refreshDocs: !!values["refresh-docs"],
|
|
33
|
+
d1: !!values.d1,
|
|
31
34
|
};
|
|
32
35
|
}
|
|
33
36
|
|
|
@@ -77,11 +80,61 @@ export function parseExportArgs(argv: string[]): ExportFlags {
|
|
|
77
80
|
};
|
|
78
81
|
}
|
|
79
82
|
|
|
83
|
+
// ---------------------------------------------------------------------------
|
|
84
|
+
// verify flags
|
|
85
|
+
// ---------------------------------------------------------------------------
|
|
86
|
+
|
|
87
|
+
export interface VerifyFlags {
|
|
88
|
+
/** Directory (relative to cwd) holding provider-resolved template text. */
|
|
89
|
+
prompts: string | undefined;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export function parseVerifyArgs(argv: string[]): VerifyFlags {
|
|
93
|
+
const { values } = parseArgs({
|
|
94
|
+
args: argv,
|
|
95
|
+
options: {
|
|
96
|
+
prompts: { type: "string" },
|
|
97
|
+
},
|
|
98
|
+
strict: true,
|
|
99
|
+
allowPositionals: false,
|
|
100
|
+
});
|
|
101
|
+
return {
|
|
102
|
+
prompts: values.prompts,
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// ---------------------------------------------------------------------------
|
|
107
|
+
// prompt-snapshot flags
|
|
108
|
+
// ---------------------------------------------------------------------------
|
|
109
|
+
|
|
110
|
+
export interface PromptSnapshotFlags {
|
|
111
|
+
/** Compare against committed snapshots and fail on drift; never write. */
|
|
112
|
+
check: boolean;
|
|
113
|
+
/** Directory (relative to cwd) holding provider-resolved template text. */
|
|
114
|
+
prompts: string | undefined;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export function parsePromptSnapshotArgs(argv: string[]): PromptSnapshotFlags {
|
|
118
|
+
const { values } = parseArgs({
|
|
119
|
+
args: argv,
|
|
120
|
+
options: {
|
|
121
|
+
check: { type: "boolean", default: false },
|
|
122
|
+
prompts: { type: "string" },
|
|
123
|
+
},
|
|
124
|
+
strict: true,
|
|
125
|
+
allowPositionals: false,
|
|
126
|
+
});
|
|
127
|
+
return {
|
|
128
|
+
check: !!values.check,
|
|
129
|
+
prompts: values.prompts,
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
|
|
80
133
|
// ---------------------------------------------------------------------------
|
|
81
134
|
// migrate flags
|
|
82
135
|
// ---------------------------------------------------------------------------
|
|
83
136
|
|
|
84
|
-
const DIALECTS = ["sqlite", "postgres"] as const;
|
|
137
|
+
const DIALECTS = ["sqlite", "postgres", "d1"] as const;
|
|
85
138
|
type Dialect = (typeof DIALECTS)[number];
|
|
86
139
|
|
|
87
140
|
const ALLOW_TOKENS = [
|
|
@@ -105,6 +158,11 @@ export interface MigrateFlags {
|
|
|
105
158
|
allow: AllowToken[];
|
|
106
159
|
onAmbiguous: OnAmbiguous | undefined;
|
|
107
160
|
dryRun: boolean;
|
|
161
|
+
// D1-specific:
|
|
162
|
+
d1Binding: string | undefined;
|
|
163
|
+
remote: boolean;
|
|
164
|
+
apply: boolean;
|
|
165
|
+
yes: boolean;
|
|
108
166
|
}
|
|
109
167
|
|
|
110
168
|
export function parseMigrateArgs(argv: string[]): MigrateFlags {
|
|
@@ -118,6 +176,10 @@ export function parseMigrateArgs(argv: string[]): MigrateFlags {
|
|
|
118
176
|
"allow": { type: "string" },
|
|
119
177
|
"on-ambiguous": { type: "string" },
|
|
120
178
|
"dry-run": { type: "boolean", default: false },
|
|
179
|
+
"d1": { type: "string" },
|
|
180
|
+
"remote": { type: "boolean", default: false },
|
|
181
|
+
"apply": { type: "boolean", default: false },
|
|
182
|
+
"yes": { type: "boolean", default: false },
|
|
121
183
|
},
|
|
122
184
|
strict: true,
|
|
123
185
|
allowPositionals: false,
|
|
@@ -153,5 +215,9 @@ export function parseMigrateArgs(argv: string[]): MigrateFlags {
|
|
|
153
215
|
allow: allowTokens as AllowToken[],
|
|
154
216
|
onAmbiguous: onAmb as OnAmbiguous | undefined,
|
|
155
217
|
dryRun: !!values["dry-run"],
|
|
218
|
+
d1Binding: values.d1 as string | undefined,
|
|
219
|
+
remote: !!values.remote,
|
|
220
|
+
apply: !!values.apply,
|
|
221
|
+
yes: !!values.yes,
|
|
156
222
|
};
|
|
157
223
|
}
|
package/src/lib/config.ts
CHANGED
|
@@ -2,15 +2,18 @@ import { readFile } from "node:fs/promises";
|
|
|
2
2
|
import { join } from "node:path";
|
|
3
3
|
import { ConfigSchema, type Config, DEFAULT_METAOBJECTS_DIR } from "@metaobjectsdev/sdk";
|
|
4
4
|
import type { GenFlags, MigrateFlags } from "./args.js";
|
|
5
|
+
import type { Dialect } from "./kysely.js";
|
|
5
6
|
|
|
6
7
|
// ---------------------------------------------------------------------------
|
|
7
8
|
// Built-in defaults
|
|
8
9
|
// ---------------------------------------------------------------------------
|
|
9
10
|
|
|
11
|
+
export const MIGRATE_DEFAULT_OUT_DIR = "./.metaobjects/migrations";
|
|
12
|
+
|
|
10
13
|
const MIGRATE_DEFAULTS = {
|
|
11
|
-
outDir:
|
|
14
|
+
outDir: MIGRATE_DEFAULT_OUT_DIR,
|
|
12
15
|
databaseUrl: undefined as string | undefined,
|
|
13
|
-
dialect: undefined as
|
|
16
|
+
dialect: undefined as Dialect | undefined,
|
|
14
17
|
onAmbiguous: "abort" as const,
|
|
15
18
|
allow: [] as string[],
|
|
16
19
|
};
|
|
@@ -24,14 +27,23 @@ export interface ResolvedGenConfig {
|
|
|
24
27
|
entities: string[];
|
|
25
28
|
}
|
|
26
29
|
|
|
30
|
+
export interface ResolvedD1Config {
|
|
31
|
+
binding: string | undefined;
|
|
32
|
+
remote: boolean;
|
|
33
|
+
autoApply: boolean;
|
|
34
|
+
wranglerConfigPath: string | undefined;
|
|
35
|
+
}
|
|
36
|
+
|
|
27
37
|
export interface ResolvedMigrateConfig {
|
|
28
38
|
outDir: string;
|
|
29
39
|
databaseUrl: string | undefined;
|
|
30
|
-
dialect:
|
|
40
|
+
dialect: Dialect | undefined;
|
|
31
41
|
onAmbiguous: "abort" | "rename" | "drop-add";
|
|
32
42
|
allow: string[];
|
|
33
43
|
slug: string | undefined;
|
|
34
44
|
dryRun: boolean;
|
|
45
|
+
yes: boolean;
|
|
46
|
+
d1: ResolvedD1Config;
|
|
35
47
|
}
|
|
36
48
|
|
|
37
49
|
// ---------------------------------------------------------------------------
|
|
@@ -61,6 +73,7 @@ export async function resolveMigrateConfig(
|
|
|
61
73
|
): Promise<ResolvedMigrateConfig> {
|
|
62
74
|
const config = await tryLoadConfig(metaRoot);
|
|
63
75
|
const cfgBlock = config?.migrate ?? {};
|
|
76
|
+
const d1Block = cfgBlock.d1 ?? {};
|
|
64
77
|
|
|
65
78
|
const envUrl = process.env.DATABASE_URL;
|
|
66
79
|
|
|
@@ -74,5 +87,12 @@ export async function resolveMigrateConfig(
|
|
|
74
87
|
: (cfgBlock.allow ?? MIGRATE_DEFAULTS.allow),
|
|
75
88
|
slug: flags.slug,
|
|
76
89
|
dryRun: flags.dryRun,
|
|
90
|
+
yes: flags.yes,
|
|
91
|
+
d1: {
|
|
92
|
+
binding: flags.d1Binding ?? d1Block.binding,
|
|
93
|
+
remote: flags.remote || (d1Block.remote ?? false),
|
|
94
|
+
autoApply: flags.apply || (d1Block.autoApply ?? false),
|
|
95
|
+
wranglerConfigPath: d1Block.wranglerConfigPath,
|
|
96
|
+
},
|
|
77
97
|
};
|
|
78
98
|
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
// The filesystem template provider for `meta verify` (FR-004 Plan #3, T6).
|
|
2
|
+
//
|
|
3
|
+
// Maps a 2-layer logical reference (`group/source`) onto a file under a base
|
|
4
|
+
// directory, trying a small set of conventional extensions. This is the CLI's
|
|
5
|
+
// concrete provider; the render engine itself stays provider-agnostic (it only
|
|
6
|
+
// knows the `Provider` interface), so production hosts can swap in an RDB/NoSQL
|
|
7
|
+
// provider without touching the engine.
|
|
8
|
+
|
|
9
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
10
|
+
import { join } from "node:path";
|
|
11
|
+
import type { Provider } from "@metaobjectsdev/render";
|
|
12
|
+
|
|
13
|
+
const EXTENSIONS = [".mustache", ".txt", ""] as const;
|
|
14
|
+
|
|
15
|
+
export class FileProvider implements Provider {
|
|
16
|
+
constructor(private readonly baseDir: string) {}
|
|
17
|
+
|
|
18
|
+
resolve(ref: string): string | undefined {
|
|
19
|
+
// `group/source` → <baseDir>/group/source<ext>; node:path.join normalizes
|
|
20
|
+
// the embedded "/" separators for the host OS.
|
|
21
|
+
for (const ext of EXTENSIONS) {
|
|
22
|
+
const path = join(this.baseDir, ref) + ext;
|
|
23
|
+
if (existsSync(path)) {
|
|
24
|
+
try {
|
|
25
|
+
return readFileSync(path, "utf8");
|
|
26
|
+
} catch {
|
|
27
|
+
// unreadable — fall through and try the next candidate
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
return undefined;
|
|
32
|
+
}
|
|
33
|
+
}
|
package/src/lib/kysely.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Kysely } from "kysely";
|
|
2
2
|
|
|
3
|
-
export type Dialect = "sqlite" | "postgres";
|
|
3
|
+
export type Dialect = "sqlite" | "postgres" | "d1";
|
|
4
4
|
|
|
5
5
|
export interface KyselyHandle {
|
|
6
6
|
db: Kysely<Record<string, unknown>>;
|
|
@@ -56,6 +56,12 @@ export async function buildKyselyFromUrl(
|
|
|
56
56
|
const dialect = dialectOverride ?? inferDialect(url);
|
|
57
57
|
const displayUrl = redactUrl(url);
|
|
58
58
|
|
|
59
|
+
if (dialect === "d1") {
|
|
60
|
+
throw new Error(
|
|
61
|
+
`dialect 'd1' does not use a URL connection; use meta migrate --d1 <binding>`,
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
|
|
59
65
|
if (dialect === "sqlite") {
|
|
60
66
|
type LibsqlDialectCtor = new (opts: { url: string }) => ConstructorParameters<typeof Kysely<Record<string, unknown>>>[0]["dialect"];
|
|
61
67
|
let LibsqlDialect: LibsqlDialectCtor;
|
|
@@ -19,13 +19,11 @@ const _thisFile = fileURLToPath(import.meta.url);
|
|
|
19
19
|
const _isCompiled = _thisFile.includes("/dist/");
|
|
20
20
|
const _cliDir = resolve(_thisFile, _isCompiled ? "../../../.." : "../../..");
|
|
21
21
|
const _require = createRequire(import.meta.url);
|
|
22
|
-
//
|
|
23
|
-
//
|
|
24
|
-
//
|
|
25
|
-
//
|
|
26
|
-
// unrebuilt dist/.
|
|
22
|
+
// Fallback layout for each codegen specifier (relative to _cliDir), used only
|
|
23
|
+
// when standard module resolution can't locate it. Compiled output lives in
|
|
24
|
+
// dist/; un-compiled runs (bun test, `meta` from the workspace) use src/ so the
|
|
25
|
+
// CLI never depends on a stale, unrebuilt dist/.
|
|
27
26
|
//
|
|
28
|
-
// The three workspace deps resolve through the CLI's own node_modules.
|
|
29
27
|
// @metaobjectsdev/cli is this package itself, so it resolves directly from
|
|
30
28
|
// _cliDir rather than through node_modules (which would be a non-existent
|
|
31
29
|
// self-referential symlink).
|
|
@@ -38,6 +36,10 @@ const CLI_PKG_PATHS: Record<string, { dist: string; src: string }> = {
|
|
|
38
36
|
dist: "node_modules/@metaobjectsdev/codegen-ts/dist/generators/index.js",
|
|
39
37
|
src: "node_modules/@metaobjectsdev/codegen-ts/src/generators/index.ts",
|
|
40
38
|
},
|
|
39
|
+
"@metaobjectsdev/codegen-ts-react": {
|
|
40
|
+
dist: "node_modules/@metaobjectsdev/codegen-ts-react/dist/index.js",
|
|
41
|
+
src: "node_modules/@metaobjectsdev/codegen-ts-react/src/index.ts",
|
|
42
|
+
},
|
|
41
43
|
"@metaobjectsdev/codegen-ts-tanstack": {
|
|
42
44
|
dist: "node_modules/@metaobjectsdev/codegen-ts-tanstack/dist/index.js",
|
|
43
45
|
src: "node_modules/@metaobjectsdev/codegen-ts-tanstack/src/index.ts",
|
|
@@ -48,12 +50,33 @@ const CLI_PKG_PATHS: Record<string, { dist: string; src: string }> = {
|
|
|
48
50
|
},
|
|
49
51
|
};
|
|
50
52
|
|
|
53
|
+
// Resolve a codegen specifier to an absolute path for jiti's alias map, so a
|
|
54
|
+
// user's metaobjects.config.ts can import @metaobjectsdev/codegen-ts* without
|
|
55
|
+
// declaring it directly — the CLI's own copy is used.
|
|
56
|
+
//
|
|
57
|
+
// Standard module resolution is tried first: it follows whatever node_modules
|
|
58
|
+
// layout exists — npm (flat), pnpm (deps as siblings in the virtual store,
|
|
59
|
+
// NOT nested under the CLI dir), or bun — and honors the package's export
|
|
60
|
+
// conditions. The CLI_PKG_PATHS fallback only kicks in when a specifier isn't
|
|
61
|
+
// require-resolvable from the CLI module.
|
|
51
62
|
function resolveCliPkg(specifier: string): string {
|
|
52
63
|
const paths = CLI_PKG_PATHS[specifier];
|
|
53
|
-
|
|
64
|
+
// The cli self-reference always points at this package's own entry, never a
|
|
65
|
+
// (possibly absent) self-referential node_modules symlink.
|
|
66
|
+
if (specifier === "@metaobjectsdev/cli" && paths !== undefined) {
|
|
54
67
|
return resolve(_cliDir, _isCompiled ? paths.dist : paths.src);
|
|
55
68
|
}
|
|
56
|
-
|
|
69
|
+
try {
|
|
70
|
+
return _require.resolve(specifier);
|
|
71
|
+
} catch {
|
|
72
|
+
if (paths !== undefined) {
|
|
73
|
+
const candidate = resolve(_cliDir, _isCompiled ? paths.dist : paths.src);
|
|
74
|
+
if (existsSync(candidate)) return candidate;
|
|
75
|
+
}
|
|
76
|
+
throw new Error(
|
|
77
|
+
`metaobjects: could not resolve ${specifier} from the CLI — try reinstalling @metaobjectsdev/cli.`,
|
|
78
|
+
);
|
|
79
|
+
}
|
|
57
80
|
}
|
|
58
81
|
|
|
59
82
|
export async function loadMetaobjectsConfig(projectRoot: string): Promise<MetaobjectsGenConfig> {
|
|
@@ -72,6 +95,7 @@ export async function loadMetaobjectsConfig(projectRoot: string): Promise<Metaob
|
|
|
72
95
|
alias: {
|
|
73
96
|
"@metaobjectsdev/codegen-ts": resolveCliPkg("@metaobjectsdev/codegen-ts"),
|
|
74
97
|
"@metaobjectsdev/codegen-ts/generators": resolveCliPkg("@metaobjectsdev/codegen-ts/generators"),
|
|
98
|
+
"@metaobjectsdev/codegen-ts-react": resolveCliPkg("@metaobjectsdev/codegen-ts-react"),
|
|
75
99
|
"@metaobjectsdev/codegen-ts-tanstack": resolveCliPkg("@metaobjectsdev/codegen-ts-tanstack"),
|
|
76
100
|
"@metaobjectsdev/cli": resolveCliPkg("@metaobjectsdev/cli"),
|
|
77
101
|
},
|
package/src/lib/output.ts
CHANGED
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
// TTY-gated glyphs: unicode (✓ ↺ ✗ = ⚠) when stdout is a TTY, plain words
|
|
4
4
|
// (NEW MERGED CONFLICT UNCHANGED REFUSED) otherwise. Per SP5 §5.1.
|
|
5
5
|
|
|
6
|
+
import type { Dialect } from "./kysely.js";
|
|
7
|
+
|
|
6
8
|
export interface FormatOptions {
|
|
7
9
|
isTTY: boolean;
|
|
8
10
|
}
|
|
@@ -22,7 +24,7 @@ export interface GenFileEntry {
|
|
|
22
24
|
export interface GenResultShape {
|
|
23
25
|
files: GenFileEntry[];
|
|
24
26
|
outDir: string;
|
|
25
|
-
dialect:
|
|
27
|
+
dialect: Dialect;
|
|
26
28
|
dryRun: boolean;
|
|
27
29
|
warnings: string[];
|
|
28
30
|
}
|
|
@@ -103,7 +105,7 @@ export interface AmbiguousEntry {
|
|
|
103
105
|
}
|
|
104
106
|
|
|
105
107
|
export interface MigrateResultShape {
|
|
106
|
-
dialect:
|
|
108
|
+
dialect: Dialect;
|
|
107
109
|
displayUrl: string;
|
|
108
110
|
changeCounts: Record<string, number>;
|
|
109
111
|
blocked: BlockedEntry[];
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
// Derive a plain payload field tree from a loaded `object.value` view-object,
|
|
2
|
+
// for `meta verify` (FR-004 Plan #3, T6). This is the metadata-side bridge to
|
|
3
|
+
// the zero-core-dependency render engine: render's `verify` takes a PLAIN
|
|
4
|
+
// PayloadField[] (no metadata import), and this function produces it by walking
|
|
5
|
+
// the view-object exactly as payload-codegen.ts does — scalars become leaves,
|
|
6
|
+
// `field.object` with an `@objectRef` becomes a nested tree.
|
|
7
|
+
|
|
8
|
+
import {
|
|
9
|
+
type MetaData,
|
|
10
|
+
TYPE_OBJECT,
|
|
11
|
+
TYPE_FIELD,
|
|
12
|
+
FIELD_SUBTYPE_OBJECT,
|
|
13
|
+
FIELD_ATTR_OBJECT_REF,
|
|
14
|
+
} from "@metaobjectsdev/metadata";
|
|
15
|
+
import type { PayloadField } from "@metaobjectsdev/render";
|
|
16
|
+
|
|
17
|
+
function findObject(root: MetaData, name: string): MetaData | undefined {
|
|
18
|
+
return root.ownChildren().find((c) => c.type === TYPE_OBJECT && c.name === name);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Walk an `object.value` view-object into a render `PayloadField[]`. Object-ref
|
|
23
|
+
* fields recurse into their referenced view-object; a `seen` set guards against
|
|
24
|
+
* a (pathological) reference cycle.
|
|
25
|
+
*/
|
|
26
|
+
export function derivePayloadFieldTree(
|
|
27
|
+
root: MetaData,
|
|
28
|
+
voName: string,
|
|
29
|
+
seen: ReadonlySet<string> = new Set(),
|
|
30
|
+
): PayloadField[] {
|
|
31
|
+
if (seen.has(voName)) return [];
|
|
32
|
+
const vo = findObject(root, voName);
|
|
33
|
+
if (!vo) return [];
|
|
34
|
+
const nextSeen = new Set(seen).add(voName);
|
|
35
|
+
const fields: PayloadField[] = [];
|
|
36
|
+
for (const f of vo.children().filter((c) => c.type === TYPE_FIELD)) {
|
|
37
|
+
if (f.subType === FIELD_SUBTYPE_OBJECT) {
|
|
38
|
+
const ref = f.ownAttr(FIELD_ATTR_OBJECT_REF);
|
|
39
|
+
if (typeof ref === "string") {
|
|
40
|
+
fields.push({ name: f.name, fields: derivePayloadFieldTree(root, ref, nextSeen) });
|
|
41
|
+
continue;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
fields.push({ name: f.name });
|
|
45
|
+
}
|
|
46
|
+
return fields;
|
|
47
|
+
}
|
|
@@ -13,13 +13,14 @@ import {
|
|
|
13
13
|
type ViewMigrationInput,
|
|
14
14
|
type ViewMigrationsResult,
|
|
15
15
|
} from "@metaobjectsdev/migrate-ts";
|
|
16
|
+
import type { Dialect } from "./kysely.js";
|
|
16
17
|
|
|
17
18
|
/** view-name → set of source-table names the view's SELECT depends on. */
|
|
18
19
|
export type ProjectionViewDependencies = ReadonlyMap<string, ReadonlySet<string>>;
|
|
19
20
|
|
|
20
21
|
export interface ProjectionMigrationsOpts {
|
|
21
22
|
readonly metadata: MetaData;
|
|
22
|
-
readonly dialect:
|
|
23
|
+
readonly dialect: Dialect;
|
|
23
24
|
readonly allowBreaking?: boolean;
|
|
24
25
|
/** Column naming strategy forwarded to extractViewSpec. Defaults to "snake_case". */
|
|
25
26
|
readonly columnNamingStrategy?: "snake_case" | "literal" | "kebab-case";
|
|
@@ -53,6 +54,8 @@ export function computeProjectionMigrations(
|
|
|
53
54
|
if (!(opts.metadata instanceof MetaRoot)) {
|
|
54
55
|
throw new Error("computeProjectionMigrations: opts.metadata must be a loaded MetaRoot.");
|
|
55
56
|
}
|
|
57
|
+
// D1 is SQLite at the SQL level; normalize before passing to downstream emitters.
|
|
58
|
+
const dialect: "postgres" | "sqlite" = opts.dialect === "d1" ? "sqlite" : opts.dialect;
|
|
56
59
|
const root = opts.metadata;
|
|
57
60
|
const columnNamingStrategy = opts.columnNamingStrategy ?? "snake_case";
|
|
58
61
|
|
|
@@ -84,7 +87,7 @@ export function computeProjectionMigrations(
|
|
|
84
87
|
}
|
|
85
88
|
|
|
86
89
|
const createSql = emitViewDdl(spec, {
|
|
87
|
-
dialect
|
|
90
|
+
dialect,
|
|
88
91
|
baseTableName,
|
|
89
92
|
joinTables,
|
|
90
93
|
});
|
|
@@ -111,7 +114,7 @@ export function computeProjectionMigrations(
|
|
|
111
114
|
}
|
|
112
115
|
|
|
113
116
|
return computeViewMigrations({
|
|
114
|
-
dialect
|
|
117
|
+
dialect,
|
|
115
118
|
allowBreaking: opts.allowBreaking ?? false,
|
|
116
119
|
views,
|
|
117
120
|
});
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
// Path + diff helpers for `meta prompt-snapshot`. Pure (no I/O, no metadata) so
|
|
2
|
+
// they unit-test trivially; the command does the filesystem work.
|
|
3
|
+
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
|
|
6
|
+
export interface SnapshotPaths {
|
|
7
|
+
/** The per-template snapshot directory: <cwd>/.metaobjects/snapshots/<name>. */
|
|
8
|
+
dir: string;
|
|
9
|
+
/** The committed fixture payload (author-owned input). */
|
|
10
|
+
payloadPath: string;
|
|
11
|
+
/** The golden rendered output (tool-managed, byte-exact). */
|
|
12
|
+
snapPath: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function snapshotPaths(cwd: string, templateName: string): SnapshotPaths {
|
|
16
|
+
const dir = join(cwd, ".metaobjects", "snapshots", templateName);
|
|
17
|
+
return {
|
|
18
|
+
dir,
|
|
19
|
+
payloadPath: join(dir, "payload.json"),
|
|
20
|
+
snapPath: join(dir, "output.snap"),
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// A compact line diff: trim the common leading/trailing lines, then show the
|
|
25
|
+
// differing middle as `- <expected>` followed by `+ <actual>`. Enough to make
|
|
26
|
+
// drift reviewable in CI output without pulling in a diff dependency.
|
|
27
|
+
export function unifiedDiff(expected: string, actual: string): string {
|
|
28
|
+
const e = expected.split("\n");
|
|
29
|
+
const a = actual.split("\n");
|
|
30
|
+
|
|
31
|
+
let pre = 0;
|
|
32
|
+
while (pre < e.length && pre < a.length && e[pre] === a[pre]) pre++;
|
|
33
|
+
|
|
34
|
+
let suf = 0;
|
|
35
|
+
while (
|
|
36
|
+
suf < e.length - pre &&
|
|
37
|
+
suf < a.length - pre &&
|
|
38
|
+
e[e.length - 1 - suf] === a[a.length - 1 - suf]
|
|
39
|
+
) {
|
|
40
|
+
suf++;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const eMid = e.slice(pre, e.length - suf);
|
|
44
|
+
const aMid = a.slice(pre, a.length - suf);
|
|
45
|
+
|
|
46
|
+
const out: string[] = [`@@ line ${pre + 1} @@`];
|
|
47
|
+
for (const line of eMid) out.push(`- ${line}`);
|
|
48
|
+
for (const line of aMid) out.push(`+ ${line}`);
|
|
49
|
+
return out.join("\n");
|
|
50
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { execFile as execFileCb } from "node:child_process";
|
|
2
|
+
import { promisify } from "node:util";
|
|
3
|
+
|
|
4
|
+
const execFile = promisify(execFileCb);
|
|
5
|
+
|
|
6
|
+
export interface WranglerExecuteOptions {
|
|
7
|
+
binding: string;
|
|
8
|
+
remote: boolean;
|
|
9
|
+
command: string;
|
|
10
|
+
configPath: string | undefined;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function buildWranglerExecuteArgs(opts: WranglerExecuteOptions): string[] {
|
|
14
|
+
const args: string[] = [
|
|
15
|
+
"d1", "execute", opts.binding,
|
|
16
|
+
opts.remote ? "--remote" : "--local",
|
|
17
|
+
"--json",
|
|
18
|
+
"--command", opts.command,
|
|
19
|
+
];
|
|
20
|
+
if (opts.configPath !== undefined) {
|
|
21
|
+
args.push("--config", opts.configPath);
|
|
22
|
+
}
|
|
23
|
+
return args;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Run wrangler with the given args; return stdout. Stderr is included in the
|
|
28
|
+
* error message when wrangler exits non-zero. `cwd` is the directory wrangler
|
|
29
|
+
* runs in (defaults to process.cwd() — caller should pass the project root).
|
|
30
|
+
*/
|
|
31
|
+
export type WranglerRunner = (args: string[], cwd: string) => Promise<{ stdout: string; stderr: string }>;
|
|
32
|
+
|
|
33
|
+
export const defaultWranglerRunner: WranglerRunner = async (args, cwd) => {
|
|
34
|
+
try {
|
|
35
|
+
const { stdout, stderr } = await execFile("wrangler", args, { cwd, maxBuffer: 16 * 1024 * 1024 });
|
|
36
|
+
return { stdout, stderr };
|
|
37
|
+
} catch (err) {
|
|
38
|
+
const e = err as NodeJS.ErrnoException & { stderr?: string; stdout?: string };
|
|
39
|
+
if (e.code === "ENOENT") {
|
|
40
|
+
throw new Error(`wrangler not found on PATH; install it: 'npm i -D wrangler'`);
|
|
41
|
+
}
|
|
42
|
+
const stderr = e.stderr ?? "";
|
|
43
|
+
throw new Error(`wrangler ${args.join(" ")} failed: ${stderr || e.message}`);
|
|
44
|
+
}
|
|
45
|
+
};
|