@deterministic-code/datasources-cli 0.1.13 → 0.1.15

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.
Files changed (62) hide show
  1. package/dist/commands.js +9 -4
  2. package/dist/detect.js +21 -7
  3. package/dist/env.js +7 -0
  4. package/dist/generate/CSharpRepositories.js +18 -14
  5. package/dist/generate/DeterministicProvider.js +25 -16
  6. package/dist/generate/RepositoryModel.js +10 -4
  7. package/dist/generate/RepositoryPlan.js +12 -5
  8. package/dist/generate/RustRepositories.js +42 -31
  9. package/dist/generate/TypeScriptRepositories.js +22 -18
  10. package/dist/generate/applyGenerate.js +11 -2
  11. package/dist/generate/applyMigrations.js +14 -17
  12. package/dist/generate/casing.js +28 -0
  13. package/dist/generate/destEnv.js +12 -0
  14. package/dist/generate/migrateUp.js +8 -41
  15. package/dist/generate/sqlDialects.js +29 -0
  16. package/dist/generate/sqlFlags.js +27 -0
  17. package/dist/migrateInit.js +4 -3
  18. package/dist/parse.js +102 -25
  19. package/dist/run.js +42 -67
  20. package/dist/setup.js +7 -9
  21. package/dist/version/applyVersion.js +11 -14
  22. package/dist/version/delta.js +0 -50
  23. package/dist/version/render.js +1 -11
  24. package/dist/wireRegister.js +29 -11
  25. package/dist/wireRustProviders.js +42 -0
  26. package/node_modules/@deterministic-code/csharp-generators/dist/import-generator.js +16 -12
  27. package/node_modules/@deterministic-code/csharp-generators/src/import-generator.test.ts +22 -11
  28. package/node_modules/@deterministic-code/csharp-generators/src/import-generator.ts +21 -13
  29. package/node_modules/@deterministic-code/datasources-code-generators/dist/nestedSelectTypes.js +2 -6
  30. package/node_modules/@deterministic-code/datasources-code-generators/dist/sql/common/infer-reference-types.d.ts +2 -0
  31. package/node_modules/@deterministic-code/datasources-code-generators/dist/sql/common/infer-reference-types.js +37 -0
  32. package/node_modules/@deterministic-code/datasources-code-generators/dist/sql/common/reference-target.d.ts +2 -0
  33. package/node_modules/@deterministic-code/datasources-code-generators/dist/sql/common/reference-target.js +6 -0
  34. package/node_modules/@deterministic-code/datasources-code-generators/dist/sql/common/sql-schema.d.ts +1 -1
  35. package/node_modules/@deterministic-code/datasources-code-generators/dist/sql/common/sql-schema.js +3 -6
  36. package/node_modules/@deterministic-code/datasources-code-generators/dist/sql/common/version-migrations.js +25 -14
  37. package/node_modules/@deterministic-code/datasources-code-generators/package.json +10 -8
  38. package/node_modules/@deterministic-code/deterministic-generators-common/README.md +61 -1
  39. package/node_modules/@deterministic-code/deterministic-generators-common/typescript/parser/dist/specification-parser.js +128 -1
  40. package/node_modules/@deterministic-code/deterministic-generators-common/typescript/parser/specification-parser.ts +1 -0
  41. package/node_modules/@deterministic-code/deterministic-generators-common/typescript/parser/version-snapshot.ts +154 -0
  42. package/node_modules/@deterministic-code/deterministic-generators-common/typescript/parser/yaml-node.ts +2 -2
  43. package/node_modules/@deterministic-code/deterministic-specifications-typescript/README.md +61 -1
  44. package/node_modules/@deterministic-code/deterministic-specifications-typescript/typescript/parser/specification-parser.ts +1 -0
  45. package/node_modules/@deterministic-code/deterministic-specifications-typescript/typescript/parser/version-snapshot.ts +154 -0
  46. package/node_modules/@deterministic-code/deterministic-specifications-typescript/typescript/parser/yaml-node.ts +2 -2
  47. package/node_modules/@deterministic-code/rust-generators/dist/import-generator.js +25 -15
  48. package/node_modules/@deterministic-code/rust-generators/src/import-generator.test.ts +11 -0
  49. package/node_modules/@deterministic-code/rust-generators/src/import-generator.ts +36 -18
  50. package/node_modules/@deterministic-code/typescript-generators/dist/generate-frontend-types.js +6 -1
  51. package/node_modules/@deterministic-code/typescript-generators/dist/generate-frontend-validators.js +6 -1
  52. package/node_modules/@deterministic-code/typescript-generators/dist/generate-routes.js +1 -1
  53. package/node_modules/@deterministic-code/typescript-generators/dist/generate-services.js +1 -1
  54. package/node_modules/@deterministic-code/typescript-generators/dist/import-generator.js +29 -21
  55. package/node_modules/@deterministic-code/typescript-generators/src/emit.ts +2 -5
  56. package/node_modules/@deterministic-code/typescript-generators/src/generate-frontend-types.ts +7 -1
  57. package/node_modules/@deterministic-code/typescript-generators/src/generate-frontend-validators.ts +7 -1
  58. package/node_modules/@deterministic-code/typescript-generators/src/generate-routes.ts +1 -1
  59. package/node_modules/@deterministic-code/typescript-generators/src/generate-services.ts +1 -1
  60. package/node_modules/@deterministic-code/typescript-generators/src/import-generator.test.ts +16 -0
  61. package/node_modules/@deterministic-code/typescript-generators/src/import-generator.ts +37 -22
  62. package/package.json +8 -8
package/dist/setup.js CHANGED
@@ -15,16 +15,15 @@ types:
15
15
  size: 128
16
16
  `;
17
17
  const answersForSetup = (env, requested = []) => requested.length ? answersFromEnv({ ...env, INSTALLED_PROVIDERS: requested.join(",") }) : answersFromEnv(env);
18
- const promptSetup = async (prompt, env, found, explicit, askScaffold = false, requested = []) => {
18
+ const promptSetup = async (prompt, env, found, explicit = [], askScaffold = false, requested = []) => {
19
19
  const defaults = answersForSetup(env, requested);
20
- const ecosystem = explicit ?? (found.length === 1 ? found[0] : found.length === 0 ? void 0 : ecosystemOr(await prompt.select({
21
- message: "Project type",
22
- choices: found.map((value) => ({ name: value, value })),
23
- default: found[0]
24
- })));
20
+ const picked = explicit.length ? explicit : found.length === 1 ? found : found.length === 0 ? [] : (await prompt.checkbox({
21
+ message: "Languages",
22
+ choices: found.map((value) => ({ name: value, value, checked: true }))
23
+ })).filter(isEcosystem);
25
24
  const fromUrl = scheme(defaults.url);
26
25
  const selected = await prompt.checkbox({
27
- message: "Providers",
26
+ message: "Datasources",
28
27
  choices: providers.map((value) => ({
29
28
  name: value,
30
29
  value,
@@ -37,9 +36,8 @@ const promptSetup = async (prompt, env, found, explicit, askScaffold = false, re
37
36
  });
38
37
  const schema = await prompt.input({ message: "schema", default: defaults.schema });
39
38
  const scaffold = askScaffold ? await prompt.confirm({ message: "Create deterministic directory", default: true }) : false;
40
- return { ecosystem, providers: selected, url, schema, scaffold };
39
+ return { languages: picked.length ? picked : void 0, providers: selected, url, schema, scaffold };
41
40
  };
42
- const ecosystemOr = (value) => isEcosystem(value) ? value : void 0;
43
41
  const envFromAnswers = (answers) => {
44
42
  const provider = scheme(answers.url) ?? answers.providers[0] ?? "sqlite";
45
43
  return {
@@ -1,17 +1,21 @@
1
1
  import { isAbsolute, join } from "node:path";
2
- import { applyDocument, asDocument, parseYaml, render } from "./render.js";
2
+ import { VersionSnapshot } from "@deterministic-code/deterministic-generators-common/parser";
3
+ import { parseYaml, render } from "./render.js";
3
4
  const versionDir = /^version-\d{8}T\d{6}Z-(\d{3})$/;
4
5
  const at = (root, path) => isAbsolute(path) ? path : join(root, path);
5
6
  const applyVersion = async (command, flags, deps, now = () => /* @__PURE__ */ new Date()) => {
6
7
  const root = at(flags.cwd, command.input);
7
8
  const names = await deps.fs.readdir(root);
8
9
  const versions = names.filter((name) => versionDir.test(name)).sort();
9
- let before = {};
10
- for (const dir of versions) {
11
- const text = await optional(deps.fs, join(root, dir, "diff.yaml"));
12
- if (text === void 0 || text === "") continue;
13
- before = applyDocument(before, asDocument(parseYaml(text), `${dir}/diff.yaml`));
14
- }
10
+ const diffs = (await Promise.all(
11
+ versions.map(async (dir) => {
12
+ const listed = await deps.fs.readdir(join(root, dir));
13
+ if (!listed.includes("diff.yaml")) return void 0;
14
+ const text = await deps.fs.readFile(join(root, dir, "diff.yaml"));
15
+ return text === "" ? void 0 : parseYaml(text);
16
+ })
17
+ )).filter((diff) => diff !== void 0);
18
+ const before = VersionSnapshot.combine(diffs).documents;
15
19
  const current = names.filter((name) => name.endsWith(".yaml")).sort();
16
20
  const paths = [.../* @__PURE__ */ new Set([...Object.keys(before), ...current])];
17
21
  const yaml = render(
@@ -39,13 +43,6 @@ const nextSeq = (versions) => {
39
43
  };
40
44
  const pad = (n) => String(n).padStart(3, "0");
41
45
  const stamp = (date) => date.toISOString().replace(/[-:]/g, "").replace(/\.\d{3}/, "");
42
- const optional = async (fs, path) => {
43
- try {
44
- return await fs.readFile(path);
45
- } catch {
46
- return void 0;
47
- }
48
- };
49
46
  export {
50
47
  applyVersion
51
48
  };
@@ -4,20 +4,6 @@ const delta = (before, after) => {
4
4
  const value = build(before, after);
5
5
  return value === unchanged ? void 0 : value;
6
6
  };
7
- const apply = (before, next) => {
8
- if (next == null) return before;
9
- if (isKeyedCollection(before) && isKeyedSplit(next)) return applyKeyed(before, next);
10
- if (isObject(before) && isObject(next)) return applyObject(before, next);
11
- return next;
12
- };
13
- const applyFile = (before, envelope) => {
14
- if (envelope == null) return before;
15
- if (isObject(envelope)) {
16
- if (envelope.removed === true && Object.keys(envelope).length === 1) return void 0;
17
- if (envelope.added === true) return envelope.content;
18
- }
19
- return apply(before ?? null, envelope);
20
- };
21
7
  const build = (before, after) => {
22
8
  if (isDeepStrictEqual(before, after)) return unchanged;
23
9
  if (isKeyedCollection(before) && isKeyedCollection(after)) return keyedSplit(before, after);
@@ -60,40 +46,6 @@ const objectDelta = (beforeObj, afterObj) => {
60
46
  if (removed.length) result.removed = removed;
61
47
  return Object.keys(result).length === 0 ? unchanged : result;
62
48
  };
63
- const applyKeyed = (beforeVal, split) => {
64
- const removed = new Set(split.removed ?? []);
65
- const changed = /* @__PURE__ */ new Map();
66
- for (const entry of split.changed ?? []) {
67
- const key = Object.keys(entry)[0];
68
- changed.set(key, entry[key]);
69
- }
70
- return [
71
- ...beforeVal.flatMap((entry) => {
72
- const key = Object.keys(entry)[0];
73
- if (removed.has(key)) return [];
74
- return changed.has(key) ? [{ [key]: apply(entry[key], changed.get(key)) }] : [entry];
75
- }),
76
- ...split.added ?? []
77
- ];
78
- };
79
- const applyObject = (beforeVal, next) => {
80
- const removed = new Set(Array.isArray(next.removed) ? next.removed : []);
81
- const out = {};
82
- for (const key of Object.keys(beforeVal)) {
83
- if (removed.has(key)) continue;
84
- out[key] = key in next ? apply(beforeVal[key], next[key]) : beforeVal[key];
85
- }
86
- for (const key of Object.keys(next)) {
87
- if (key === "removed" && Array.isArray(next.removed)) continue;
88
- if (!(key in beforeVal)) out[key] = next[key];
89
- }
90
- return out;
91
- };
92
- const isKeyedSplit = (value) => {
93
- if (!isObject(value)) return false;
94
- const allowed = /* @__PURE__ */ new Set(["added", "changed", "removed"]);
95
- return Object.keys(value).every((key) => allowed.has(key));
96
- };
97
49
  const isObject = (value) => value !== null && value !== void 0 && typeof value === "object" && !Array.isArray(value) && Object.getPrototypeOf(value) === Object.prototype;
98
50
  const isKeyedCollection = (value) => Array.isArray(value) && value.length > 0 && value.every(
99
51
  (item) => item !== null && typeof item === "object" && !Array.isArray(item) && Object.keys(item).length === 1
@@ -107,8 +59,6 @@ const keyedMap = (arr) => {
107
59
  return map;
108
60
  };
109
61
  export {
110
- apply,
111
- applyFile,
112
62
  delta,
113
63
  isObject
114
64
  };
@@ -1,5 +1,5 @@
1
1
  import { parse, stringify } from "yaml";
2
- import { applyFile, delta, isObject } from "./delta.js";
2
+ import { delta, isObject } from "./delta.js";
3
3
  const render = (files) => {
4
4
  const out = {};
5
5
  for (const { path, before, after } of files) {
@@ -18,20 +18,10 @@ const render = (files) => {
18
18
  return Object.keys(out).length === 0 ? "" : stringify(out);
19
19
  };
20
20
  const parseYaml = (text) => parse(text);
21
- const applyDocument = (before, doc) => {
22
- const out = { ...before };
23
- for (const [path, envelope] of Object.entries(doc)) {
24
- const next = applyFile(out[path], envelope);
25
- if (next === void 0) delete out[path];
26
- else out[path] = next;
27
- }
28
- return out;
29
- };
30
21
  const asDocument = (value, source) => isObject(value) ? value : (() => {
31
22
  throw new Error(`${source}: expected a mapping`);
32
23
  })();
33
24
  export {
34
- applyDocument,
35
25
  asDocument,
36
26
  parseYaml,
37
27
  render
@@ -1,16 +1,27 @@
1
1
  import { dirname, isAbsolute, join, relative, sep } from "node:path";
2
+ import { createCasingStrategy } from "@deterministic-code/generators-common/casing-strategy";
2
3
  import { csharpTarget } from "./detect.js";
4
+ import { repositoriesPrefix } from "./generate/casing.js";
5
+ import { rustIdent } from "./generate/RepositoryModel.js";
3
6
  const posix = (path) => path.split(sep).join("/");
4
7
  const specFrom = (from, to) => {
5
8
  const rel = posix(relative(dirname(from), to));
6
9
  return rel.startsWith(".") ? rel : `./${rel}`;
7
10
  };
8
11
  const hookPath = (cwd, dialect, src) => join(cwd, src ? "src" : ".", dialect === "typescript" ? "datasources-register.ts" : "datasources_register.rs");
9
- const hookContents = (dialect, hook, output) => dialect === "typescript" ? `import "${specFrom(hook, join(output, "typescript/repositories/repository-registry.ts"))}";
10
- ` : `#[path = "${specFrom(hook, join(output, "rust/repositories/mod.rs"))}"]
12
+ const registryRel = (dialect, settings) => {
13
+ const casing = createCasingStrategy(dialect, settings);
14
+ const dir = repositoriesPrefix(dialect, casing);
15
+ return dialect === "typescript" ? `${dir}/${casing.convertFileName("repository_registry")}.ts` : `${dir}/mod.rs`;
16
+ };
17
+ const hookContents = (dialect, hook, output, settings = {}) => {
18
+ const casing = createCasingStrategy(dialect, settings);
19
+ return dialect === "typescript" ? `import "${specFrom(hook, join(output, registryRel(dialect, settings)))}";
20
+ ` : `#[path = "${specFrom(hook, join(output, registryRel(dialect, settings)))}"]
11
21
  pub mod repositories;
12
- pub use repositories::repository_registry::Repositories;
22
+ pub use repositories::${rustIdent(casing.convertFileName("repository_registry"))}::Repositories;
13
23
  `;
24
+ };
14
25
  const cargoWithCtor = (cargo) => /^\s*ctor\s*=/m.test(cargo) ? void 0 : cargo.includes("[dependencies]") ? cargo.replace("[dependencies]", `[dependencies]
15
26
  ctor = "0.4"`) : `${cargo.trimEnd()}
16
27
 
@@ -30,7 +41,7 @@ const optional = async (fs, path) => {
30
41
  }
31
42
  };
32
43
  const outputRoot = (cwd, output) => isAbsolute(output) ? output : join(cwd, output);
33
- const registryPath = (root, dialect) => dialect === "typescript" ? join(root, "typescript/repositories/repository-registry.ts") : join(root, "rust/repositories/mod.rs");
44
+ const registryPath = (root, dialect, settings) => join(root, registryRel(dialect, settings));
34
45
  const firstFile = async (fs, cwd, names) => {
35
46
  for (const name of names) {
36
47
  const path = join(cwd, name);
@@ -39,6 +50,7 @@ const firstFile = async (fs, cwd, names) => {
39
50
  }
40
51
  return void 0;
41
52
  };
53
+ const excludeNodeModules = " <PropertyGroup>\n <DefaultItemExcludes>$(DefaultItemExcludes);node_modules/**</DefaultItemExcludes>\n </PropertyGroup>\n";
42
54
  const wireCsharp = async (cwd, output, dryRun, deps) => {
43
55
  const entries = await deps.fs.readdir(cwd).catch(() => []);
44
56
  const target = csharpTarget(entries);
@@ -46,28 +58,34 @@ const wireCsharp = async (cwd, output, dryRun, deps) => {
46
58
  const csprojPath = join(cwd, target);
47
59
  const generated = join(outputRoot(cwd, output), "csharp");
48
60
  const rel = posix(relative(dirname(csprojPath), generated));
49
- if (!rel.startsWith("..")) return;
50
61
  const include = `${rel}/**/*.cs`;
51
62
  const existing = await optional(deps.fs, csprojPath);
52
- if (existing === void 0 || existing.includes(include)) return;
63
+ if (existing === void 0) return;
64
+ const needExclude = !existing.includes("node_modules/**");
65
+ const needInclude = rel.startsWith("..") && !existing.includes(include);
66
+ if (!needExclude && !needInclude) return;
53
67
  if (dryRun) {
54
68
  deps.log(csprojPath);
55
69
  return;
56
70
  }
57
- await deps.fs.writeFile(csprojPath, existing.replace("</Project>", ` <ItemGroup>
71
+ const patched = [
72
+ needExclude ? excludeNodeModules : "",
73
+ needInclude ? ` <ItemGroup>
58
74
  <Compile Include="${include}" />
59
75
  </ItemGroup>
60
- </Project>`));
76
+ ` : ""
77
+ ].join("");
78
+ await deps.fs.writeFile(csprojPath, existing.replace("</Project>", `${patched}</Project>`));
61
79
  deps.log(csprojPath);
62
80
  };
63
- const wireRegister = async (cwd, output, dialect, dryRun, deps) => {
81
+ const wireRegister = async (cwd, output, dialect, dryRun, deps, settings = {}) => {
64
82
  if (dialect === "csharp") return wireCsharp(cwd, output, dryRun, deps);
65
83
  const root = outputRoot(cwd, output);
66
84
  if (relative(cwd, root).startsWith("..")) return;
67
- if (await optional(deps.fs, registryPath(root, dialect)) === void 0) return;
85
+ if (await optional(deps.fs, registryPath(root, dialect, settings)) === void 0) return;
68
86
  const entries = await deps.fs.readdir(cwd).catch(() => []);
69
87
  const hook = hookPath(cwd, dialect, entries.includes("src"));
70
- const text = hookContents(dialect, hook, root);
88
+ const text = hookContents(dialect, hook, root, settings);
71
89
  if (dryRun) {
72
90
  deps.log(hook);
73
91
  return;
@@ -0,0 +1,42 @@
1
+ import { dirname, join } from "node:path";
2
+ import { catalog } from "./catalog.js";
3
+ const optional = async (fs, path) => {
4
+ try {
5
+ return await fs.readFile(path);
6
+ } catch {
7
+ return void 0;
8
+ }
9
+ };
10
+ const firstFile = async (fs, cwd, names) => {
11
+ for (const name of names) {
12
+ const path = join(cwd, name);
13
+ const text = await optional(fs, path);
14
+ if (text !== void 0) return { path, text };
15
+ }
16
+ return void 0;
17
+ };
18
+ const rustBindContents = (names) => names.map((name) => `use ${catalog[name].cargo.replaceAll("-", "_")} as _;
19
+ `).join("");
20
+ const attachedProviders = (entry) => entry.includes("datasources_providers") ? void 0 : `mod datasources_providers;
21
+ ${entry}`;
22
+ const wireRustProviders = async (cwd, names, dryRun, deps) => {
23
+ const entry = await firstFile(deps.fs, cwd, ["src/lib.rs", "src/main.rs", "lib.rs", "main.rs"]);
24
+ const bind = join(entry ? dirname(entry.path) : cwd, "datasources_providers.rs");
25
+ const text = rustBindContents(names);
26
+ if (dryRun) {
27
+ deps.log(bind);
28
+ return;
29
+ }
30
+ await deps.fs.writeFile(bind, text);
31
+ deps.log(bind);
32
+ if (!entry) return;
33
+ const next = attachedProviders(entry.text);
34
+ if (next === void 0) return;
35
+ await deps.fs.writeFile(entry.path, next);
36
+ deps.log(entry.path);
37
+ };
38
+ export {
39
+ attachedProviders,
40
+ rustBindContents,
41
+ wireRustProviders
42
+ };
@@ -10,10 +10,6 @@ const pluralSnake = (entity) => {
10
10
  parts[parts.length - 1] = pluralize.plural(parts[parts.length - 1]);
11
11
  return parts.join("_");
12
12
  };
13
- const DATASOURCE_DIR = "Types/Datasource/Generated";
14
- const VIEW_DIR = "Types/View/Generated";
15
- const DATASOURCE_VALIDATOR_DIR = `${DATASOURCE_DIR}/Validators`;
16
- const VIEW_VALIDATOR_DIR = `${VIEW_DIR}/Validators`;
17
13
  class CsharpImportGenerator {
18
14
  organizeByFeature;
19
15
  flat;
@@ -26,7 +22,7 @@ class CsharpImportGenerator {
26
22
  this.casing = createCasing(settings);
27
23
  }
28
24
  datasource(entity) {
29
- return this.underBase(this.rel(DATASOURCE_DIR, this.casedFile(entity)));
25
+ return this.underBase(this.rel(this.layered("types/datasource/generated"), this.casedFile(entity)));
30
26
  }
31
27
  datasourceRel(entity) {
32
28
  return this.datasource(entity);
@@ -37,7 +33,7 @@ class CsharpImportGenerator {
37
33
  datasourceValidator(entity) {
38
34
  return this.underBase(
39
35
  this.rel(
40
- DATASOURCE_VALIDATOR_DIR,
36
+ this.layered("types/datasource/generated/validators"),
41
37
  this.csFile(`datasource_${entity}_validator`)
42
38
  )
43
39
  );
@@ -46,7 +42,7 @@ class CsharpImportGenerator {
46
42
  return this.datasourceValidator(entity);
47
43
  }
48
44
  view(entity) {
49
- return this.underBase(this.rel(VIEW_DIR, this.csFile(entity)));
45
+ return this.underBase(this.rel(this.layered("types/view/generated"), this.csFile(entity)));
50
46
  }
51
47
  viewRel(entity) {
52
48
  return this.view(entity);
@@ -56,7 +52,7 @@ class CsharpImportGenerator {
56
52
  }
57
53
  viewValidator(entity) {
58
54
  return this.underBase(
59
- this.rel(VIEW_VALIDATOR_DIR, this.csFile(`${entity}_validator`))
55
+ this.rel(this.layered("types/view/generated/validators"), this.csFile(`${entity}_validator`))
60
56
  );
61
57
  }
62
58
  viewValidatorRel(entity) {
@@ -71,12 +67,12 @@ class CsharpImportGenerator {
71
67
  serviceCustom(name, _module) {
72
68
  const file = this.csFile(name);
73
69
  return this.underBase(
74
- this.organizeByFeature ? `Features/${this.casing.directory(featureEntity(name))}/custom/${file}` : `../custom/${file}`
70
+ this.organizeByFeature ? `${this.featuresDir(featureEntity(name))}/custom/${file}` : `../custom/${file}`
75
71
  );
76
72
  }
77
73
  serviceCustomRel(entity) {
78
74
  const file = this.csFile(entity);
79
- return this.organizeByFeature ? `Features/${this.casing.directory(entity)}/custom/${file}` : `../custom/${file}`;
75
+ return this.organizeByFeature ? `${this.featuresDir(entity)}/custom/${file}` : `../custom/${file}`;
80
76
  }
81
77
  serviceTest(entity) {
82
78
  return this.underBase(this.csFile(`${entity}_service_tests`));
@@ -102,7 +98,7 @@ class CsharpImportGenerator {
102
98
  routeCustom(name, _module) {
103
99
  const file = this.csFile(`${name}_route`);
104
100
  return this.underBase(
105
- this.organizeByFeature ? `Features/${this.casing.directory(featureEntity(name))}/custom/${file}` : `../custom/${file}`
101
+ this.organizeByFeature ? `${this.featuresDir(featureEntity(name))}/custom/${file}` : `../custom/${file}`
106
102
  );
107
103
  }
108
104
  routeTest(entity) {
@@ -149,7 +145,15 @@ class CsharpImportGenerator {
149
145
  }
150
146
  casedFile(entity) {
151
147
  const file = this.csFile(entity);
152
- return this.organizeByFeature ? `Features/${this.casing.directory(entity)}/${file}` : file;
148
+ return this.organizeByFeature ? `${this.featuresDir(entity)}/${file}` : file;
149
+ }
150
+ layered(path) {
151
+ return path.split("/").map(
152
+ (segment) => segment === "custom" ? segment : this.casing.convertDirectories(segment)
153
+ ).join("/");
154
+ }
155
+ featuresDir(entity) {
156
+ return `${this.casing.convertDirectories("features")}/${this.casing.directory(entity)}`;
153
157
  }
154
158
  rel(prefix, file) {
155
159
  if (this.organizeByFeature || this.flat) return file;
@@ -136,6 +136,17 @@ describe("CsharpImportGenerator layered (organize_by_feature unset)", () => {
136
136
  snake.datasource("notification_type"),
137
137
  "Types/Datasource/Generated/notification_type.cs",
138
138
  );
139
+ const kebabDirs = createImportGenerator(".", {
140
+ "languages.csharp.casing.directories": "Kebab",
141
+ });
142
+ assert.equal(
143
+ kebabDirs.datasource("user"),
144
+ "types/datasource/generated/User.cs",
145
+ );
146
+ assert.equal(
147
+ kebabDirs.view("user"),
148
+ "types/view/generated/User.cs",
149
+ );
139
150
  });
140
151
 
141
152
  it("cases type quals from settings independently of files", () => {
@@ -173,11 +184,11 @@ describe("CsharpImportGenerator by-feature", () => {
173
184
  });
174
185
  assert.equal(
175
186
  imports.datasource("notification_type"),
176
- "Features/notification-type/NotificationType.cs",
187
+ "features/notification-type/NotificationType.cs",
177
188
  );
178
189
  assert.equal(
179
190
  imports.datasourceRel("notification_type"),
180
- "Features/notification-type/NotificationType.cs",
191
+ "features/notification-type/NotificationType.cs",
181
192
  );
182
193
  assert.equal(imports.view("user"), "User.cs");
183
194
  assert.equal(imports.viewRel("user"), "User.cs");
@@ -200,31 +211,31 @@ describe("CsharpImportGenerator by-feature", () => {
200
211
  });
201
212
  assert.equal(
202
213
  imports.serviceCustom("user"),
203
- "Features/user/custom/User.cs",
214
+ "features/user/custom/User.cs",
204
215
  );
205
216
  assert.equal(
206
217
  imports.serviceCustom("create_card_payment"),
207
- "Features/card-payment/custom/CreateCardPayment.cs",
218
+ "features/card-payment/custom/CreateCardPayment.cs",
208
219
  );
209
220
  assert.equal(
210
221
  imports.serviceCustom("update_card_payment"),
211
- "Features/card-payment/custom/UpdateCardPayment.cs",
222
+ "features/card-payment/custom/UpdateCardPayment.cs",
212
223
  );
213
224
  assert.equal(
214
225
  imports.serviceCustomRel("notification_type"),
215
- "Features/notification-type/custom/NotificationType.cs",
226
+ "features/notification-type/custom/NotificationType.cs",
216
227
  );
217
228
  assert.equal(
218
229
  imports.routeCustom("get_health"),
219
- "Features/get-health/custom/GetHealthRoute.cs",
230
+ "features/get-health/custom/GetHealthRoute.cs",
220
231
  );
221
232
  assert.equal(
222
233
  imports.routeCustom("create_card_payment"),
223
- "Features/card-payment/custom/CreateCardPaymentRoute.cs",
234
+ "features/card-payment/custom/CreateCardPaymentRoute.cs",
224
235
  );
225
236
  assert.equal(
226
237
  imports.serviceCustom("user", "./ignored/module"),
227
- "Features/user/custom/User.cs",
238
+ "features/user/custom/User.cs",
228
239
  );
229
240
  });
230
241
 
@@ -235,11 +246,11 @@ describe("CsharpImportGenerator by-feature", () => {
235
246
  });
236
247
  assert.equal(
237
248
  imports.datasource("notification_type"),
238
- "Features/notification_type/NotificationType.cs",
249
+ "features/notification_type/NotificationType.cs",
239
250
  );
240
251
  assert.equal(
241
252
  imports.serviceCustomRel("notification_type"),
242
- "Features/notification_type/custom/NotificationType.cs",
253
+ "features/notification_type/custom/NotificationType.cs",
243
254
  );
244
255
  });
245
256
  });
@@ -15,11 +15,6 @@ const pluralSnake = (entity: string): string => {
15
15
  return parts.join("_");
16
16
  };
17
17
 
18
- const DATASOURCE_DIR = "Types/Datasource/Generated";
19
- const VIEW_DIR = "Types/View/Generated";
20
- const DATASOURCE_VALIDATOR_DIR = `${DATASOURCE_DIR}/Validators`;
21
- const VIEW_VALIDATOR_DIR = `${VIEW_DIR}/Validators`;
22
-
23
18
  export class CsharpImportGenerator implements IImportGenerator {
24
19
  private readonly organizeByFeature: boolean;
25
20
  private readonly flat: boolean;
@@ -35,7 +30,7 @@ export class CsharpImportGenerator implements IImportGenerator {
35
30
  }
36
31
 
37
32
  datasource(entity: string): string {
38
- return this.underBase(this.rel(DATASOURCE_DIR, this.casedFile(entity)));
33
+ return this.underBase(this.rel(this.layered("types/datasource/generated"), this.casedFile(entity)));
39
34
  }
40
35
 
41
36
  datasourceRel(entity: string): string {
@@ -49,7 +44,7 @@ export class CsharpImportGenerator implements IImportGenerator {
49
44
  datasourceValidator(entity: string): string {
50
45
  return this.underBase(
51
46
  this.rel(
52
- DATASOURCE_VALIDATOR_DIR,
47
+ this.layered("types/datasource/generated/validators"),
53
48
  this.csFile(`datasource_${entity}_validator`),
54
49
  ),
55
50
  );
@@ -60,7 +55,7 @@ export class CsharpImportGenerator implements IImportGenerator {
60
55
  }
61
56
 
62
57
  view(entity: string): string {
63
- return this.underBase(this.rel(VIEW_DIR, this.csFile(entity)));
58
+ return this.underBase(this.rel(this.layered("types/view/generated"), this.csFile(entity)));
64
59
  }
65
60
 
66
61
  viewRel(entity: string): string {
@@ -73,7 +68,7 @@ export class CsharpImportGenerator implements IImportGenerator {
73
68
 
74
69
  viewValidator(entity: string): string {
75
70
  return this.underBase(
76
- this.rel(VIEW_VALIDATOR_DIR, this.csFile(`${entity}_validator`)),
71
+ this.rel(this.layered("types/view/generated/validators"), this.csFile(`${entity}_validator`)),
77
72
  );
78
73
  }
79
74
 
@@ -93,7 +88,7 @@ export class CsharpImportGenerator implements IImportGenerator {
93
88
  const file = this.csFile(name);
94
89
  return this.underBase(
95
90
  this.organizeByFeature
96
- ? `Features/${this.casing.directory(featureEntity(name))}/custom/${file}`
91
+ ? `${this.featuresDir(featureEntity(name))}/custom/${file}`
97
92
  : `../custom/${file}`,
98
93
  );
99
94
  }
@@ -101,7 +96,7 @@ export class CsharpImportGenerator implements IImportGenerator {
101
96
  serviceCustomRel(entity: string): string {
102
97
  const file = this.csFile(entity);
103
98
  return this.organizeByFeature
104
- ? `Features/${this.casing.directory(entity)}/custom/${file}`
99
+ ? `${this.featuresDir(entity)}/custom/${file}`
105
100
  : `../custom/${file}`;
106
101
  }
107
102
 
@@ -137,7 +132,7 @@ export class CsharpImportGenerator implements IImportGenerator {
137
132
  const file = this.csFile(`${name}_route`);
138
133
  return this.underBase(
139
134
  this.organizeByFeature
140
- ? `Features/${this.casing.directory(featureEntity(name))}/custom/${file}`
135
+ ? `${this.featuresDir(featureEntity(name))}/custom/${file}`
141
136
  : `../custom/${file}`,
142
137
  );
143
138
  }
@@ -205,10 +200,23 @@ export class CsharpImportGenerator implements IImportGenerator {
205
200
  private casedFile(entity: string): string {
206
201
  const file = this.csFile(entity);
207
202
  return this.organizeByFeature
208
- ? `Features/${this.casing.directory(entity)}/${file}`
203
+ ? `${this.featuresDir(entity)}/${file}`
209
204
  : file;
210
205
  }
211
206
 
207
+ private layered(path: string): string {
208
+ return path
209
+ .split("/")
210
+ .map((segment) =>
211
+ segment === "custom" ? segment : this.casing.convertDirectories(segment),
212
+ )
213
+ .join("/");
214
+ }
215
+
216
+ private featuresDir(entity: string): string {
217
+ return `${this.casing.convertDirectories("features")}/${this.casing.directory(entity)}`;
218
+ }
219
+
212
220
  private rel(prefix: string, file: string): string {
213
221
  if (this.organizeByFeature || this.flat) return file;
214
222
  return `${prefix}/${file}`;
@@ -1,3 +1,4 @@
1
+ import { referenceTarget } from "./sql/common/reference-target.js";
1
2
  import { ChangeCasingMapper, NestedSelectBuilder, PluralizerMapper } from "@deterministic-code/datasources";
2
3
  const pascal = new ChangeCasingMapper("snakeCase", "pascalCase");
3
4
  const plural = new PluralizerMapper();
@@ -51,12 +52,7 @@ const build = (type, catalog, ancestors) => {
51
52
  });
52
53
  return { name: entity(type.name), key, properties, relations: [...objects, ...lists] };
53
54
  };
54
- const target = (field) => {
55
- if (Array.isArray(field.references) && field.references.length === 2)
56
- return [field.references[0] ?? "", field.references[1] ?? ""];
57
- const [table, column] = String(field.references ?? "").split(".");
58
- return [table ?? "", column ?? ""];
59
- };
55
+ const target = (field) => field.references === undefined ? ["", ""] : referenceTarget(field.references);
60
56
  const relationName = (field, table) => {
61
57
  const name = entity(field);
62
58
  return name.endsWith("Id") && name.length > 2 ? name.slice(0, -2) : entity(table);
@@ -0,0 +1,2 @@
1
+ import type { Type } from "@deterministic-code/deterministic-generators-common/parser";
2
+ export declare const inferReferenceFieldTypes: (types: Type[]) => Type[];
@@ -0,0 +1,37 @@
1
+ import { referenceTarget } from "./reference-target.js";
2
+ const needsType = (field) => !field.isArray &&
3
+ !field.isMap &&
4
+ field.references !== undefined &&
5
+ field.type === "string" &&
6
+ field.size === undefined;
7
+ const ready = (field) => field.type !== "string" || field.size !== undefined;
8
+ const lookup = (types, field) => {
9
+ const [table, column] = referenceTarget(field.references);
10
+ return types.find((type) => type.name === table)?.fields.find((item) => item.name === column);
11
+ };
12
+ export const inferReferenceFieldTypes = (types) => {
13
+ const working = types.map((type) => ({ ...type, fields: type.fields.map((field) => ({ ...field })) }));
14
+ for (let pass = 0; pass < working.length; pass++) {
15
+ let changed = false;
16
+ for (const type of working) {
17
+ type.fields = type.fields.map((field) => {
18
+ if (!needsType(field))
19
+ return field;
20
+ const parent = lookup(working, field);
21
+ if (parent === undefined || !ready(parent))
22
+ return field;
23
+ changed = true;
24
+ return {
25
+ ...field,
26
+ type: parent.type,
27
+ kind: parent.kind,
28
+ base: parent.base,
29
+ ...(parent.size !== undefined ? { size: parent.size } : {}),
30
+ };
31
+ });
32
+ }
33
+ if (!changed)
34
+ break;
35
+ }
36
+ return working;
37
+ };
@@ -0,0 +1,2 @@
1
+ import type { TypeField } from "@deterministic-code/deterministic-generators-common/parser";
2
+ export declare const referenceTarget: (references: NonNullable<TypeField["references"]>) => [string, string];
@@ -0,0 +1,6 @@
1
+ export const referenceTarget = (references) => {
2
+ if (Array.isArray(references))
3
+ return [references[0], references[1]];
4
+ const [table, col] = references.split(".");
5
+ return [table ?? "", col ?? ""];
6
+ };