@deterministic-code/datasources-cli 0.1.14 → 0.1.16
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/commands.js +9 -4
- package/dist/env.js +7 -0
- package/dist/generate/DeterministicProvider.js +2 -9
- package/dist/generate/applyGenerate.js +3 -1
- package/dist/generate/applyMigrations.js +10 -5
- package/dist/generate/destEnv.js +12 -0
- package/dist/generate/migrateUp.js +6 -33
- package/dist/generate/sqlDialects.js +29 -0
- package/dist/wireRegister.js +12 -3
- package/dist/wireRustProviders.js +31 -3
- package/node_modules/@deterministic-code/datasources-code-generators/package.json +7 -7
- package/node_modules/@deterministic-code/deterministic-specifications-typescript/README.md +61 -1
- package/node_modules/@deterministic-code/deterministic-specifications-typescript/typescript/parser/specification-parser.ts +1 -0
- package/node_modules/@deterministic-code/deterministic-specifications-typescript/typescript/parser/version-snapshot.ts +154 -0
- package/node_modules/@deterministic-code/deterministic-specifications-typescript/typescript/parser/yaml-node.ts +2 -2
- package/package.json +8 -8
package/dist/commands.js
CHANGED
|
@@ -1,14 +1,19 @@
|
|
|
1
1
|
import { catalog, host } from "./catalog.js";
|
|
2
2
|
import { csharpTarget } from "./detect.js";
|
|
3
|
+
import { sqlMigrateProviders } from "./migrateInit.js";
|
|
4
|
+
const npmDialects = (names) => {
|
|
5
|
+
const dialects = sqlMigrateProviders(names);
|
|
6
|
+
return dialects.length === 0 ? [] : npmInstall(dialects);
|
|
7
|
+
};
|
|
3
8
|
const addCommands = (ecosystem, names, entries) => ({
|
|
4
9
|
ts: () => npmInstall(names),
|
|
5
|
-
rust: () => cargoAdd(names),
|
|
6
|
-
csharp: () => dotnetAdd(names, csharpTarget(entries))
|
|
10
|
+
rust: () => [...cargoAdd(names), ...npmDialects(names)],
|
|
11
|
+
csharp: () => [...dotnetAdd(names, csharpTarget(entries)), ...npmDialects(names)]
|
|
7
12
|
})[ecosystem]();
|
|
8
13
|
const removeCommands = (ecosystem, names, _remaining, entries) => ({
|
|
9
14
|
ts: () => npmUninstall(names),
|
|
10
|
-
rust: () => cargoRemove(names),
|
|
11
|
-
csharp: () => dotnetRemove(names, csharpTarget(entries))
|
|
15
|
+
rust: () => [...cargoRemove(names), ...npmUninstall(sqlMigrateProviders(names))],
|
|
16
|
+
csharp: () => [...dotnetRemove(names, csharpTarget(entries)), ...npmUninstall(sqlMigrateProviders(names))]
|
|
12
17
|
})[ecosystem]();
|
|
13
18
|
const npmInstall = (names) => [
|
|
14
19
|
{
|
package/dist/env.js
CHANGED
|
@@ -29,6 +29,12 @@ const withSchema = (url, schema) => {
|
|
|
29
29
|
};
|
|
30
30
|
const urlOf = (env) => env.DATASOURCE_URL || "";
|
|
31
31
|
const installedOf = (env) => env.INSTALLED_PROVIDERS || scheme(urlOf(env)) || "";
|
|
32
|
+
const overlayEnv = (file, env) => ({
|
|
33
|
+
...file,
|
|
34
|
+
...env.INSTALLED_PROVIDERS ? { INSTALLED_PROVIDERS: env.INSTALLED_PROVIDERS } : {},
|
|
35
|
+
...env.PROVIDER ? { PROVIDER: env.PROVIDER } : {},
|
|
36
|
+
...env.DATASOURCE_URL ? { DATASOURCE_URL: env.DATASOURCE_URL } : {}
|
|
37
|
+
});
|
|
32
38
|
const parseEnv = (text) => Object.fromEntries(
|
|
33
39
|
text.split(/\r?\n/).filter((line) => line.trim() && !line.trimStart().startsWith("#") && line.includes("=")).map((line) => {
|
|
34
40
|
const i = line.indexOf("=");
|
|
@@ -83,6 +89,7 @@ export {
|
|
|
83
89
|
answersFromEnv,
|
|
84
90
|
ensureProviders,
|
|
85
91
|
knownEnvKeys,
|
|
92
|
+
overlayEnv,
|
|
86
93
|
parseEnv,
|
|
87
94
|
schemaOf,
|
|
88
95
|
scheme,
|
|
@@ -13,6 +13,7 @@ import { rustRepositories } from "./RustRepositories.js";
|
|
|
13
13
|
import { typescriptRepositories } from "./TypeScriptRepositories.js";
|
|
14
14
|
import { planRepositories } from "./RepositoryPlan.js";
|
|
15
15
|
import { datasourceEnginesOf, engineDatasourceOf, overlayDatasource } from "./datasourceYaml.js";
|
|
16
|
+
import { sqlDialectsFromEnv } from "./sqlDialects.js";
|
|
16
17
|
const place = (dir, entries) => entries.map((entry) => ({ ...entry, filename: `${dir}/${entry.filename}` }));
|
|
17
18
|
const remap = (entries, settings, dialect) => {
|
|
18
19
|
const casing = createCasingStrategy(dialect, settings);
|
|
@@ -58,14 +59,6 @@ const withProviderWrites = async (request, models) => {
|
|
|
58
59
|
)
|
|
59
60
|
}));
|
|
60
61
|
};
|
|
61
|
-
const dialectsOf = (settings) => {
|
|
62
|
-
const out = [];
|
|
63
|
-
for (const name of (settings["backend.datasources"] ?? "").split(",")) {
|
|
64
|
-
const key = name.trim();
|
|
65
|
-
if (key !== "" && !out.includes(key)) out.push(key);
|
|
66
|
-
}
|
|
67
|
-
return out;
|
|
68
|
-
};
|
|
69
62
|
const deterministic = {
|
|
70
63
|
name: "deterministic",
|
|
71
64
|
generateTypes: async (request) => {
|
|
@@ -96,7 +89,7 @@ const deterministic = {
|
|
|
96
89
|
request.dialect
|
|
97
90
|
);
|
|
98
91
|
},
|
|
99
|
-
generateDatasource: async (request) => place("sql", await generateSql(context(request),
|
|
92
|
+
generateDatasource: async (request) => place("sql", await generateSql(context(request), sqlDialectsFromEnv(request.env ?? {})))
|
|
100
93
|
};
|
|
101
94
|
export {
|
|
102
95
|
deterministic
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { isAbsolute, join } from "node:path";
|
|
2
2
|
import { memoryReader } from "@deterministic-code/generators-common/deterministic-reader";
|
|
3
|
+
import { destEnv } from "./destEnv.js";
|
|
3
4
|
import { loadSpec } from "./loadSpec.js";
|
|
4
5
|
import { providerByName } from "./providers.js";
|
|
5
6
|
import { withStoredProcedures } from "./storedProcedures.js";
|
|
@@ -19,7 +20,8 @@ const applyGenerate = async (command, flags, deps) => {
|
|
|
19
20
|
const request = {
|
|
20
21
|
dialect: command.dialect,
|
|
21
22
|
reader: memoryReader(loaded.files),
|
|
22
|
-
settings
|
|
23
|
+
settings,
|
|
24
|
+
env: await destEnv(deps.fs, flags.cwd)
|
|
23
25
|
};
|
|
24
26
|
const entries = await {
|
|
25
27
|
all: async () => [
|
|
@@ -1,19 +1,24 @@
|
|
|
1
1
|
import { isAbsolute, join } from "node:path";
|
|
2
|
-
import {
|
|
2
|
+
import { generateSql } from "@deterministic-code/datasources-code-generators";
|
|
3
3
|
import { memoryReader } from "@deterministic-code/generators-common/deterministic-reader";
|
|
4
|
+
import { destEnv } from "./destEnv.js";
|
|
4
5
|
import { loadSpec } from "./loadSpec.js";
|
|
5
6
|
import { providerByName } from "./providers.js";
|
|
6
7
|
import { overlaySqlFlags } from "./sqlFlags.js";
|
|
8
|
+
import { sqlDialectsFromEnv } from "./sqlDialects.js";
|
|
7
9
|
import { withStoredProcedures } from "./storedProcedures.js";
|
|
8
10
|
import { writeEntries } from "./writeEntries.js";
|
|
9
11
|
const at = (root, path) => isAbsolute(path) ? path : join(root, path);
|
|
10
12
|
const applyMigrations = async (command, flags, deps) => {
|
|
11
13
|
providerByName(command.provider);
|
|
12
14
|
const loaded = await loadSpec(deps.fs, at(flags.cwd, command.input));
|
|
13
|
-
const entries = await generateSql(
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
15
|
+
const entries = await generateSql(
|
|
16
|
+
{
|
|
17
|
+
reader: memoryReader(loaded.files),
|
|
18
|
+
settings: overlaySqlFlags(withStoredProcedures(loaded.settings, command.storedProcedures), command)
|
|
19
|
+
},
|
|
20
|
+
sqlDialectsFromEnv(await destEnv(deps.fs, flags.cwd))
|
|
21
|
+
);
|
|
17
22
|
const root = at(flags.cwd, command.migrationsRoot);
|
|
18
23
|
if (flags.dryRun) {
|
|
19
24
|
for (const entry of entries) deps.log(join(root, entry.filename));
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { join } from "node:path";
|
|
2
|
+
import { overlayEnv, parseEnv } from "../env.js";
|
|
3
|
+
const destEnv = async (fs, cwd) => {
|
|
4
|
+
try {
|
|
5
|
+
return overlayEnv(parseEnv(await fs.readFile(join(cwd, ".env"))), process.env);
|
|
6
|
+
} catch {
|
|
7
|
+
return overlayEnv({}, process.env);
|
|
8
|
+
}
|
|
9
|
+
};
|
|
10
|
+
export {
|
|
11
|
+
destEnv
|
|
12
|
+
};
|
|
@@ -1,30 +1,8 @@
|
|
|
1
1
|
import { isAbsolute, join } from "node:path";
|
|
2
|
-
import { parseEnv, scheme } from "../env.js";
|
|
3
|
-
import { migrateProviders } from "../migrateInit.js";
|
|
2
|
+
import { overlayEnv, parseEnv, scheme } from "../env.js";
|
|
4
3
|
import { loadSpec } from "./loadSpec.js";
|
|
4
|
+
import { canonical, isSql, sqlDialectsFromEnv } from "./sqlDialects.js";
|
|
5
5
|
const at = (root, path) => isAbsolute(path) ? path : join(root, path);
|
|
6
|
-
const aliases = {
|
|
7
|
-
sqlite3: "sqlite",
|
|
8
|
-
postgresql: "postgres",
|
|
9
|
-
pg: "postgres",
|
|
10
|
-
mariadb: "mysql",
|
|
11
|
-
mssql: "sqlserver",
|
|
12
|
-
mssqlserver: "sqlserver",
|
|
13
|
-
ora: "oracle"
|
|
14
|
-
};
|
|
15
|
-
const canonical = (name) => {
|
|
16
|
-
const key = name.trim().toLowerCase();
|
|
17
|
-
return aliases[key] ?? key;
|
|
18
|
-
};
|
|
19
|
-
const isSql = (name) => migrateProviders.includes(name);
|
|
20
|
-
const sqlDialects = (settings) => {
|
|
21
|
-
const out = [];
|
|
22
|
-
for (const name of settings["backend.datasources"].split(",")) {
|
|
23
|
-
const key = canonical(name);
|
|
24
|
-
if (key !== "" && isSql(key) && !out.includes(key)) out.push(key);
|
|
25
|
-
}
|
|
26
|
-
return out;
|
|
27
|
-
};
|
|
28
6
|
const readEnv = async (fs, cwd) => {
|
|
29
7
|
try {
|
|
30
8
|
return parseEnv(await fs.readFile(join(cwd, ".env")));
|
|
@@ -32,16 +10,11 @@ const readEnv = async (fs, cwd) => {
|
|
|
32
10
|
return {};
|
|
33
11
|
}
|
|
34
12
|
};
|
|
35
|
-
const merged = (file, env) => ({
|
|
36
|
-
...file,
|
|
37
|
-
...env.PROVIDER ? { PROVIDER: env.PROVIDER } : {},
|
|
38
|
-
...env.DATASOURCE_URL ? { DATASOURCE_URL: env.DATASOURCE_URL } : {}
|
|
39
|
-
});
|
|
40
13
|
const inMemory = (url) => url === ":memory:" || url.includes(":memory:");
|
|
41
14
|
const applyMigrateUp = async (command, flags, deps, env = process.env) => {
|
|
42
|
-
|
|
43
|
-
const
|
|
44
|
-
const
|
|
15
|
+
await loadSpec(deps.fs, at(flags.cwd, command.input));
|
|
16
|
+
const values = overlayEnv(await readEnv(deps.fs, flags.cwd), env);
|
|
17
|
+
const dialects = sqlDialectsFromEnv(values);
|
|
45
18
|
const connection = values.DATASOURCE_URL ?? "";
|
|
46
19
|
const provider = canonical(values.PROVIDER || scheme(connection) || "");
|
|
47
20
|
if (!provider || !connection) {
|
|
@@ -58,7 +31,7 @@ const applyMigrateUp = async (command, flags, deps, env = process.env) => {
|
|
|
58
31
|
}
|
|
59
32
|
if (!dialects.includes(provider))
|
|
60
33
|
throw new Error(
|
|
61
|
-
`migrate-up: provider ${provider} is not in
|
|
34
|
+
`migrate-up: provider ${provider} is not in INSTALLED_PROVIDERS (${dialects.join(", ") || "none"})`
|
|
62
35
|
);
|
|
63
36
|
const npx = (verb, extra = []) => [
|
|
64
37
|
verb,
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { scheme, splitProviders } from "../env.js";
|
|
2
|
+
import { migrateProviders } from "../migrateInit.js";
|
|
3
|
+
const aliases = {
|
|
4
|
+
sqlite3: "sqlite",
|
|
5
|
+
postgresql: "postgres",
|
|
6
|
+
pg: "postgres",
|
|
7
|
+
mariadb: "mysql",
|
|
8
|
+
mssql: "sqlserver",
|
|
9
|
+
mssqlserver: "sqlserver",
|
|
10
|
+
ora: "oracle"
|
|
11
|
+
};
|
|
12
|
+
const canonical = (name) => {
|
|
13
|
+
const key = name.trim().toLowerCase();
|
|
14
|
+
return aliases[key] ?? key;
|
|
15
|
+
};
|
|
16
|
+
const isSql = (name) => migrateProviders.includes(name);
|
|
17
|
+
const sqlDialectsFromEnv = (env) => {
|
|
18
|
+
const out = [];
|
|
19
|
+
for (const name of splitProviders(env.INSTALLED_PROVIDERS || env.PROVIDER || scheme(env.DATASOURCE_URL || "") || "")) {
|
|
20
|
+
const key = canonical(name);
|
|
21
|
+
if (key !== "" && isSql(key) && !out.includes(key)) out.push(key);
|
|
22
|
+
}
|
|
23
|
+
return out;
|
|
24
|
+
};
|
|
25
|
+
export {
|
|
26
|
+
canonical,
|
|
27
|
+
isSql,
|
|
28
|
+
sqlDialectsFromEnv
|
|
29
|
+
};
|
package/dist/wireRegister.js
CHANGED
|
@@ -22,12 +22,21 @@ pub mod repositories;
|
|
|
22
22
|
pub use repositories::${rustIdent(casing.convertFileName("repository_registry"))}::Repositories;
|
|
23
23
|
`;
|
|
24
24
|
};
|
|
25
|
-
const
|
|
26
|
-
|
|
25
|
+
const rustCargoLine = (cargo, name, line) => new RegExp(`^\\s*${name}\\s*=`, "m").test(cargo) ? void 0 : line;
|
|
26
|
+
const cargoWithCtor = (cargo) => {
|
|
27
|
+
const add = [
|
|
28
|
+
rustCargoLine(cargo, "ctor", 'ctor = "0.4"'),
|
|
29
|
+
rustCargoLine(cargo, "tokio", 'tokio = { version = "1", features = ["macros", "rt-multi-thread"] }')
|
|
30
|
+
].filter((line) => line !== void 0);
|
|
31
|
+
if (add.length === 0) return void 0;
|
|
32
|
+
const block = add.join("\n");
|
|
33
|
+
return cargo.includes("[dependencies]") ? cargo.replace("[dependencies]", `[dependencies]
|
|
34
|
+
${block}`) : `${cargo.trimEnd()}
|
|
27
35
|
|
|
28
36
|
[dependencies]
|
|
29
|
-
|
|
37
|
+
${block}
|
|
30
38
|
`;
|
|
39
|
+
};
|
|
31
40
|
const attached = (dialect, entry, entryPath, hook) => dialect === "typescript" ? entry.includes("datasources-register") ? void 0 : `import "${specFrom(entryPath, hook)}";
|
|
32
41
|
${entry}` : entry.includes("datasources_register") ? void 0 : `mod datasources_register;
|
|
33
42
|
use datasources_register::Repositories;
|
|
@@ -1,5 +1,18 @@
|
|
|
1
1
|
import { dirname, join } from "node:path";
|
|
2
2
|
import { catalog } from "./catalog.js";
|
|
3
|
+
const rustDriver = {
|
|
4
|
+
json: "JsonDriver",
|
|
5
|
+
memory: "MemoryDriver",
|
|
6
|
+
sqlite: "SqliteDriver",
|
|
7
|
+
xml: "XmlDriver",
|
|
8
|
+
yaml: "YamlDriver",
|
|
9
|
+
mysql: "MysqlDriver",
|
|
10
|
+
sqlserver: "SqlServerDriver",
|
|
11
|
+
postgres: "PostgresDriver",
|
|
12
|
+
oracle: "OracleDriver",
|
|
13
|
+
mongodb: "MongoDbDriver",
|
|
14
|
+
dynamodb: "DynamoDbDriver"
|
|
15
|
+
};
|
|
3
16
|
const optional = async (fs, path) => {
|
|
4
17
|
try {
|
|
5
18
|
return await fs.readFile(path);
|
|
@@ -15,10 +28,25 @@ const firstFile = async (fs, cwd, names) => {
|
|
|
15
28
|
}
|
|
16
29
|
return void 0;
|
|
17
30
|
};
|
|
18
|
-
const rustBindContents = (names) =>
|
|
19
|
-
|
|
20
|
-
const
|
|
31
|
+
const rustBindContents = (names) => `pub fn register() {
|
|
32
|
+
${names.map((name) => {
|
|
33
|
+
const crate = catalog[name].cargo.replaceAll("-", "_");
|
|
34
|
+
return ` datasources::register("${name}", std::sync::Arc::new(${crate}::${rustDriver[name]}));`;
|
|
35
|
+
}).join("\n")}
|
|
36
|
+
}
|
|
37
|
+
`;
|
|
38
|
+
const withMod = (entry) => entry.includes("datasources_providers") ? entry : `mod datasources_providers;
|
|
21
39
|
${entry}`;
|
|
40
|
+
const withRegisterCall = (entry) => {
|
|
41
|
+
if (entry.includes("datasources_providers::register()")) return entry;
|
|
42
|
+
const main = /((?:async\s+)?fn\s+main\s*\([^)]*\)(?:\s*->\s*[^{]+)?\s*)\{(\s*)/;
|
|
43
|
+
return entry.replace(main, (_, sig, ws) => `${sig}{
|
|
44
|
+
datasources_providers::register();${ws === "" ? "\n" : ws}`);
|
|
45
|
+
};
|
|
46
|
+
const attachedProviders = (entry) => {
|
|
47
|
+
const next = withRegisterCall(withMod(entry));
|
|
48
|
+
return next === entry ? void 0 : next;
|
|
49
|
+
};
|
|
22
50
|
const wireRustProviders = async (cwd, names, dryRun, deps) => {
|
|
23
51
|
const entry = await firstFile(deps.fs, cwd, ["src/lib.rs", "src/main.rs", "lib.rs", "main.rs"]);
|
|
24
52
|
const bind = join(entry ? dirname(entry.path) : cwd, "datasources_providers.rs");
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@deterministic-code/datasources-code-generators",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.16",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"exports": {
|
|
6
6
|
".": "./dist/generateSql.js",
|
|
@@ -14,12 +14,12 @@
|
|
|
14
14
|
"./sql/common/infer-reference-types": "./dist/sql/common/infer-reference-types.js"
|
|
15
15
|
},
|
|
16
16
|
"dependencies": {
|
|
17
|
-
"@deterministic-code/datasources": "^0.1.
|
|
18
|
-
"@deterministic-code/datasources-mysql": "^0.1.
|
|
19
|
-
"@deterministic-code/datasources-oracle": "^0.1.
|
|
20
|
-
"@deterministic-code/datasources-postgres": "^0.1.
|
|
21
|
-
"@deterministic-code/datasources-sqlite": "^0.1.
|
|
22
|
-
"@deterministic-code/datasources-sqlserver": "^0.1.
|
|
17
|
+
"@deterministic-code/datasources": "^0.1.14",
|
|
18
|
+
"@deterministic-code/datasources-mysql": "^0.1.14",
|
|
19
|
+
"@deterministic-code/datasources-oracle": "^0.1.14",
|
|
20
|
+
"@deterministic-code/datasources-postgres": "^0.1.14",
|
|
21
|
+
"@deterministic-code/datasources-sqlite": "^0.1.14",
|
|
22
|
+
"@deterministic-code/datasources-sqlserver": "^0.1.14",
|
|
23
23
|
"@deterministic-code/deterministic-generators-common": "0.1.0",
|
|
24
24
|
"@deterministic-code/generators-common": "0.0.1",
|
|
25
25
|
"pluralize": "^8.0.0",
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
Parser and validators for the [deterministic-specifications](https://github.com/deterministic-code/deterministic-specifications) contract. Each language is its own library. Specs and samples stay in the specifications repo; these libraries only read them.
|
|
4
4
|
|
|
5
|
-
- [`typescript/`](typescript) — npm package `@deterministic-code/deterministic-generators-common` (`DeterministicParser` on the `./parser` export, validators on the package root)
|
|
5
|
+
- [`typescript/`](typescript) — npm package `@deterministic-code/deterministic-generators-common` (`DeterministicParser` and `VersionSnapshot` on the `./parser` export, validators on the package root)
|
|
6
6
|
- [`rust/`](rust) — crate `deterministic_generators_common` (path/git dependency)
|
|
7
7
|
- [`csharp/`](csharp) — `Deterministic.Generators.Common` (`net10.0` class library)
|
|
8
8
|
|
|
@@ -15,8 +15,68 @@ deterministic_generators_common = { git = "https://github.com/deterministic-code
|
|
|
15
15
|
<ProjectReference Include="path/to/deterministic-generators-common/csharp/Deterministic.Generators.Common/Deterministic.Generators.Common.csproj" />
|
|
16
16
|
```
|
|
17
17
|
|
|
18
|
+
```xml
|
|
19
|
+
<PackageReference Include="Deterministic.Generators.Common" Version="0.1.1" />
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
TypeScript packs pin `#main`:
|
|
23
|
+
|
|
24
|
+
```json
|
|
25
|
+
"@deterministic-code/deterministic-generators-common": "github:deterministic-code/deterministic-generators-common#main"
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
C# first-party consumers use the `ProjectReference`. Out-of-repo C# can take the NuGet package.
|
|
29
|
+
|
|
30
|
+
```text
|
|
31
|
+
npm run publish:nuget # pack and smoke-test
|
|
32
|
+
npm run publish:nuget -- --publish # push to nuget.org (NUGET_API_KEY)
|
|
33
|
+
```
|
|
34
|
+
|
|
18
35
|
```text
|
|
19
36
|
npm test # TypeScript parser + validator (100% coverage)
|
|
20
37
|
sh rust/coverage.sh # cargo llvm-cov, 100% lines + functions
|
|
21
38
|
dotnet test csharp/Deterministic.Generators.Common.slnx --settings csharp/coverlet.runsettings
|
|
22
39
|
```
|
|
40
|
+
|
|
41
|
+
## VersionSnapshot
|
|
42
|
+
|
|
43
|
+
`VersionSnapshot` reconstructs specification YAML by folding every `deterministic/version-*/diff.yaml` in name order. Documents are the library YAML AST (`YamlValue` / `YamlMap` / `YamlSeq` / `YamlScalar`), not compiled `IDeterministic` and not `DeterministicParser` output.
|
|
44
|
+
|
|
45
|
+
| API | Role |
|
|
46
|
+
| --- | --- |
|
|
47
|
+
| `Combine` / `combine` | Fold already-parsed diff trees (mappings), in order |
|
|
48
|
+
| `Read` / `read` | Scan a `deterministic/` directory and fold matching version dirs |
|
|
49
|
+
| `Documents` / `documents` | Reconstructed file path → tree |
|
|
50
|
+
| `Yaml` / `yaml(path)` | Serialize one reconstructed file with the library serializer |
|
|
51
|
+
|
|
52
|
+
Apply helpers are private instance methods. Version directories must match `^version-\d{8}T\d{6}Z-\d{3}$`. Missing or empty `diff.yaml` is skipped. A non-mapping diff throws `expected a mapping`. TypeScript `read` is async; C# and Rust `Read` are sync.
|
|
53
|
+
|
|
54
|
+
```csharp
|
|
55
|
+
var snapshot = VersionSnapshot.Read(deterministicRoot);
|
|
56
|
+
var types = snapshot.Yaml("types.yaml");
|
|
57
|
+
var datasource = snapshot.Yaml("datasource.yaml");
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
```typescript
|
|
61
|
+
import { VersionSnapshot } from "@deterministic-code/deterministic-generators-common/parser";
|
|
62
|
+
|
|
63
|
+
const snapshot = await VersionSnapshot.read(deterministicRoot);
|
|
64
|
+
const types = snapshot.yaml("types.yaml");
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
```rust
|
|
68
|
+
let snapshot = VersionSnapshot::read(deterministic_root)?;
|
|
69
|
+
let types = snapshot.yaml("types.yaml")?;
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
`Combine` takes an ordered list of parsed mappings, not YAML strings. To compare a fold to a live file, wrap the live tree as `{ added: true, content }` and compare `Yaml` strings from the same serializer — do not string-compare authored YAML (comments and flow sequences differ).
|
|
73
|
+
|
|
74
|
+
Per file key in a diff:
|
|
75
|
+
|
|
76
|
+
- `{ added: true, content }` inserts `content` (`true` or the strings `"true"` / `"True"`).
|
|
77
|
+
- `{ removed: true }` deletes the file.
|
|
78
|
+
- Object merge: nested apply, `removed:` string list deletes keys, new keys are appended.
|
|
79
|
+
- Keyed collections (sequence of single-key maps): `{ added, changed, removed }`. Added entries are prepended.
|
|
80
|
+
- Otherwise the next value replaces.
|
|
81
|
+
|
|
82
|
+
`Read` does not grow `IDeterministicReader`.
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import { readdir, readFile } from "node:fs/promises";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { parse, stringify } from "yaml";
|
|
4
|
+
import type { YamlMap, YamlValue } from "./yaml-node.ts";
|
|
5
|
+
|
|
6
|
+
const versionDir = /^version-\d{8}T\d{6}Z-\d{3}$/;
|
|
7
|
+
const own = (obj: object, key: string) => Object.prototype.hasOwnProperty.call(obj, key);
|
|
8
|
+
|
|
9
|
+
export class VersionSnapshot {
|
|
10
|
+
readonly documents: Readonly<Record<string, YamlValue | undefined>>;
|
|
11
|
+
|
|
12
|
+
private constructor(diffsInOrder: Array<YamlValue | null | undefined>) {
|
|
13
|
+
const docs: Record<string, YamlValue | undefined> = {};
|
|
14
|
+
for (const diff of diffsInOrder) {
|
|
15
|
+
if (diff == null) continue;
|
|
16
|
+
if (!this.isMap(diff)) throw new Error("expected a mapping");
|
|
17
|
+
for (const [path, envelope] of Object.entries(diff)) {
|
|
18
|
+
const next = this.applyFile(docs[path], envelope);
|
|
19
|
+
if (this.isNull(next)) delete docs[path];
|
|
20
|
+
else docs[path] = next;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
this.documents = docs;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
static combine(diffsInOrder: Array<YamlValue | null | undefined>): VersionSnapshot {
|
|
27
|
+
return new VersionSnapshot(diffsInOrder);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
static async read(deterministicRoot: string): Promise<VersionSnapshot> {
|
|
31
|
+
const names = (await readdir(deterministicRoot, { withFileTypes: true }))
|
|
32
|
+
.filter((entry) => entry.isDirectory() && versionDir.test(entry.name))
|
|
33
|
+
.map((entry) => entry.name)
|
|
34
|
+
.sort();
|
|
35
|
+
const snapshot = new VersionSnapshot([]);
|
|
36
|
+
return VersionSnapshot.combine(await Promise.all(names.map((name) => snapshot.diffOf(deterministicRoot, name))));
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
yaml(path: string): string {
|
|
40
|
+
if (!own(this.documents, path)) throw new Error(`missing ${path}`);
|
|
41
|
+
return stringify(this.documents[path]);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
private async diffOf(root: string, name: string): Promise<YamlValue | undefined> {
|
|
45
|
+
let text: string;
|
|
46
|
+
try {
|
|
47
|
+
text = await readFile(join(root, name, "diff.yaml"), "utf8");
|
|
48
|
+
} catch {
|
|
49
|
+
return undefined;
|
|
50
|
+
}
|
|
51
|
+
if (text === "") return undefined;
|
|
52
|
+
let rootValue: YamlValue;
|
|
53
|
+
try {
|
|
54
|
+
rootValue = parse(text) as YamlValue;
|
|
55
|
+
} catch (error) {
|
|
56
|
+
throw new Error(`${name}/diff.yaml: ${String(error)}`);
|
|
57
|
+
}
|
|
58
|
+
if (!this.isMap(rootValue)) throw new Error(`${name}/diff.yaml: expected a mapping`);
|
|
59
|
+
return rootValue;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
private applyFile(before: YamlValue | undefined, envelope: YamlValue | undefined): YamlValue | undefined {
|
|
63
|
+
if (this.isNull(envelope)) return before;
|
|
64
|
+
if (this.isMap(envelope) && this.isTrue(envelope, "removed") && Object.keys(envelope).length === 1)
|
|
65
|
+
return undefined;
|
|
66
|
+
if (this.isMap(envelope) && this.isTrue(envelope, "added")) return envelope.content;
|
|
67
|
+
return this.apply(before, envelope);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
private apply(before: YamlValue | undefined, next: YamlValue | undefined): YamlValue | undefined {
|
|
71
|
+
if (this.isNull(next)) return before;
|
|
72
|
+
if (this.isKeyed(before) && this.isMap(next) && this.isSplit(next)) return this.applyKeyed(before, next);
|
|
73
|
+
if (this.isMap(before) && this.isMap(next)) return this.applyObject(before, next);
|
|
74
|
+
return next;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
private applyKeyed(beforeVal: YamlMap[], split: YamlMap): YamlValue {
|
|
78
|
+
const removed = new Set(this.strings(split, "removed"));
|
|
79
|
+
const changed = new Map(
|
|
80
|
+
this.seq(split.changed).flatMap((entry) => {
|
|
81
|
+
const pair = this.firstPair(entry);
|
|
82
|
+
return pair ? [[pair.key, pair.value] as const] : [];
|
|
83
|
+
}),
|
|
84
|
+
);
|
|
85
|
+
return [
|
|
86
|
+
...this.seq(split.added),
|
|
87
|
+
...beforeVal.flatMap((entry) => {
|
|
88
|
+
const pair = this.firstPair(entry);
|
|
89
|
+
if (!pair || removed.has(pair.key)) return [];
|
|
90
|
+
return [
|
|
91
|
+
changed.has(pair.key)
|
|
92
|
+
? { [pair.key]: this.apply(pair.value, changed.get(pair.key)) as YamlValue }
|
|
93
|
+
: entry,
|
|
94
|
+
];
|
|
95
|
+
}),
|
|
96
|
+
];
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
private applyObject(beforeVal: YamlMap, next: YamlMap): YamlMap {
|
|
100
|
+
const removed = new Set(this.strings(next, "removed"));
|
|
101
|
+
const output: Record<string, YamlValue> = {};
|
|
102
|
+
for (const [key, value] of Object.entries(beforeVal)) {
|
|
103
|
+
if (removed.has(key)) continue;
|
|
104
|
+
output[key] = (own(next, key) ? this.apply(value, next[key]) : value) as YamlValue;
|
|
105
|
+
}
|
|
106
|
+
for (const [key, value] of Object.entries(next)) {
|
|
107
|
+
if (key === "removed" && Array.isArray(value)) continue;
|
|
108
|
+
if (!own(beforeVal, key)) output[key] = value;
|
|
109
|
+
}
|
|
110
|
+
return output;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
private isSplit(map: YamlMap) {
|
|
114
|
+
return Object.keys(map).every((key) => key === "added" || key === "changed" || key === "removed");
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
private isKeyed(value: YamlValue | undefined): value is YamlMap[] {
|
|
118
|
+
return (
|
|
119
|
+
Array.isArray(value) &&
|
|
120
|
+
value.length > 0 &&
|
|
121
|
+
value.every((item) => this.isMap(item) && Object.keys(item).length === 1)
|
|
122
|
+
);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
private seq(value: YamlValue | undefined): YamlValue[] {
|
|
126
|
+
return Array.isArray(value) ? value : [];
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
private strings(map: YamlMap, key: string) {
|
|
130
|
+
return this.seq(map[key]).map((item) => (this.isScalar(item) ? String(item ?? "") : ""));
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
private firstPair(entry: YamlValue | undefined) {
|
|
134
|
+
if (!this.isMap(entry)) return undefined;
|
|
135
|
+
const key = Object.keys(entry)[0];
|
|
136
|
+
return key === undefined ? undefined : { key, value: entry[key] };
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
private isMap(value: YamlValue | undefined | null): value is YamlMap {
|
|
140
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
private isTrue(map: YamlMap, key: string) {
|
|
144
|
+
return map[key] === true || map[key] === "true" || map[key] === "True";
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
private isNull(value: YamlValue | undefined | null) {
|
|
148
|
+
return value == null;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
private isScalar(value: YamlValue | undefined) {
|
|
152
|
+
return value == null || typeof value === "string" || typeof value === "number" || typeof value === "boolean";
|
|
153
|
+
}
|
|
154
|
+
}
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { parse } from "yaml";
|
|
2
2
|
|
|
3
3
|
type YamlLiteral = string | number | boolean | null;
|
|
4
|
-
type YamlMap = { readonly [key: string]: YamlValue };
|
|
5
|
-
type YamlValue = YamlLiteral | YamlValue[] | YamlMap;
|
|
4
|
+
export type YamlMap = { readonly [key: string]: YamlValue };
|
|
5
|
+
export type YamlValue = YamlLiteral | YamlValue[] | YamlMap;
|
|
6
6
|
type FieldDefault = YamlLiteral | YamlValue[] | YamlMap;
|
|
7
7
|
type Named = { name: string; node: YamlNode };
|
|
8
8
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@deterministic-code/datasources-cli",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.16",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"bin": {
|
|
6
6
|
"datasources": "./bin/datasources.mjs"
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
"access": "public"
|
|
14
14
|
},
|
|
15
15
|
"dependencies": {
|
|
16
|
-
"@deterministic-code/datasources-code-generators": "0.1.
|
|
16
|
+
"@deterministic-code/datasources-code-generators": "0.1.16",
|
|
17
17
|
"@deterministic-code/generators-common": "0.0.1",
|
|
18
18
|
"@deterministic-code/deterministic-generators-common": "0.1.0",
|
|
19
19
|
"@deterministic-code/typescript-generators": "0.0.1",
|
|
@@ -21,12 +21,12 @@
|
|
|
21
21
|
"@deterministic-code/rust-generators": "0.0.1",
|
|
22
22
|
"@deterministic-code/generators-openapi": "0.0.1",
|
|
23
23
|
"@deterministic-code/patch-merger": "0.1.0",
|
|
24
|
-
"@deterministic-code/datasources": "^0.1.
|
|
25
|
-
"@deterministic-code/datasources-sqlite": "^0.1.
|
|
26
|
-
"@deterministic-code/datasources-mysql": "^0.1.
|
|
27
|
-
"@deterministic-code/datasources-postgres": "^0.1.
|
|
28
|
-
"@deterministic-code/datasources-sqlserver": "^0.1.
|
|
29
|
-
"@deterministic-code/datasources-oracle": "^0.1.
|
|
24
|
+
"@deterministic-code/datasources": "^0.1.14",
|
|
25
|
+
"@deterministic-code/datasources-sqlite": "^0.1.14",
|
|
26
|
+
"@deterministic-code/datasources-mysql": "^0.1.14",
|
|
27
|
+
"@deterministic-code/datasources-postgres": "^0.1.14",
|
|
28
|
+
"@deterministic-code/datasources-sqlserver": "^0.1.14",
|
|
29
|
+
"@deterministic-code/datasources-oracle": "^0.1.14",
|
|
30
30
|
"@inquirer/prompts": "^7.8.0",
|
|
31
31
|
"yaml": "^2.6.0",
|
|
32
32
|
"mustache": "^4.2.0",
|