@antislop/zero-lucid-generator 1.0.0 → 1.1.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 CHANGED
@@ -26,13 +26,13 @@ npm add -D @antislop/zero-lucid-generator
26
26
 
27
27
  ### 1. Create a config file
28
28
 
29
- Create `lucid-zero.config.ts` at the root of your project:
29
+ Create `config/lucid_zero.ts`:
30
30
 
31
31
  ```ts
32
32
  import type { Config } from '@antislop/zero-lucid-generator'
33
33
 
34
34
  const config: Config = {
35
- modelsSourcePath: './app/models',
35
+ modelsDirectory: './app/models', // Optional; this is the default
36
36
  }
37
37
 
38
38
  export default config
@@ -44,7 +44,7 @@ export default config
44
44
  npx lucid-zero generate
45
45
  ```
46
46
 
47
- This writes `zero-schema.gen.ts` next to your config file.
47
+ Run this from your project root. This writes `zero-schema.gen.ts` in the current working directory.
48
48
 
49
49
  ### 3. Import the schema
50
50
 
@@ -56,13 +56,12 @@ import { schema, zql } from './zero-schema.gen.js'
56
56
 
57
57
  ## Config options
58
58
 
59
- | Option | Type | Default | Description |
60
- |---|---|---|---|
61
- | `modelsSourcePath` | `string` | **required** | Path to your models directory, relative to the config file |
62
- | `output` | `string` | `zero-schema.gen.ts` | Output file path, relative to the config file |
63
- | `prettier` | `boolean` | `false` | Format the output with Prettier (must be installed) |
64
- | `excludeModels` | `LucidModel[]` | `[]` | Models to exclude from the generated schema |
65
- | `columnTypes` | `Record<string, Record<string, string>>` | `{}` | Override the inferred Zero type for specific columns |
59
+ | Option | Type | Default | Description |
60
+ | --------------------- | ---------------------------------------- | -------------------- | ------------------------------------------------------------------------ |
61
+ | `modelsDirectory` | `string` | `./app/models` | Path to your models directory, relative to the current working directory |
62
+ | `outputPath` | `string` | `zero-schema.gen.ts` | Output file path, relative to the current working directory |
63
+ | `excludeModels` | `LucidModel[]` | `[]` | Models to exclude from the generated schema |
64
+ | `columnTypeOverrides` | `Record<string, Record<string, string>>` | `{}` | Override the inferred Zero type for specific columns |
66
65
 
67
66
  ---
68
67
 
@@ -72,8 +71,8 @@ import { schema, zql } from './zero-schema.gen.js'
72
71
  lucid-zero generate [options]
73
72
 
74
73
  Options:
75
- -c, --config <path> Path to config file (default: lucid-zero.config.ts)
76
- -t, --tsconfig <path> Path to tsconfig.json (default: tsconfig.json next to config)
74
+ -c, --config <path> Path to config file (default: config/lucid_zero.ts)
75
+ -t, --tsconfig <path> Path to tsconfig.json (default: tsconfig.json in cwd)
77
76
  ```
78
77
 
79
78
  ---
@@ -82,24 +81,24 @@ Options:
82
81
 
83
82
  `@antislop/zero-lucid-generator` maps TypeScript types to Zero types automatically:
84
83
 
85
- | TypeScript type | Zero type |
86
- |---|---|
87
- | `string` | `string()` |
88
- | `number` | `number()` |
89
- | `boolean` | `boolean()` |
90
- | `DateTime` / `Date` | `number()` (Unix ms) |
91
- | `object` / `unknown` / `any` | `json()` |
92
- | `T \| null` or `T \| undefined` | `.optional()` |
84
+ | TypeScript type | Zero type |
85
+ | ------------------------------- | -------------------- |
86
+ | `string` | `string()` |
87
+ | `number` | `number()` |
88
+ | `boolean` | `boolean()` |
89
+ | `DateTime` / `Date` | `number()` (Unix ms) |
90
+ | `object` / `unknown` / `any` | `json()` |
91
+ | `T \| null` or `T \| undefined` | `.optional()` |
93
92
 
94
- When inference isn't accurate — for example a JSON column with a known shape — use `columnTypes`:
93
+ When inference isn't accurate — for example a JSON column with a known shape — use `columnTypeOverrides`:
95
94
 
96
95
  ```ts
97
96
  import type { Config } from '@antislop/zero-lucid-generator'
98
- import Session from "#models/session";
97
+ import Session from '#models/session'
99
98
 
100
99
  const config: Config = {
101
- modelsSourcePath: './app/models',
102
- columnTypes: {
100
+ modelsDirectory: './app/models',
101
+ columnTypeOverrides: {
103
102
  Issue: {
104
103
  // metadata is typed as `unknown` but has a known shape
105
104
  metadata: 'json<{ priority: number; labels: string[] }>()',
@@ -124,61 +123,54 @@ Valid Zero base types: `string`, `number`, `boolean`, `json`, `enumeration`.
124
123
  ```ts
125
124
  // zero-schema.gen.ts (auto-generated — do not edit)
126
125
 
127
- import {
128
- createBuilder,
129
- createSchema,
130
- number,
131
- relationships,
132
- string,
133
- table,
134
- } from "@rocicorp/zero";
126
+ import { createBuilder, createSchema, number, relationships, string, table } from '@rocicorp/zero'
135
127
 
136
- export const users = table("users")
128
+ export const users = table('users')
137
129
  .columns({
138
130
  id: number(),
139
131
  name: string(),
140
132
  email: string(),
141
133
  })
142
- .primaryKey("id");
134
+ .primaryKey('id')
143
135
 
144
- export const posts = table("posts")
136
+ export const posts = table('posts')
145
137
  .columns({
146
138
  id: number(),
147
139
  userId: number().from('user_id'),
148
140
  title: string(),
149
141
  body: string().optional(),
150
142
  })
151
- .primaryKey("id");
143
+ .primaryKey('id')
152
144
 
153
145
  export const usersRelationships = relationships(users, ({ many }) => ({
154
146
  posts: many({
155
- sourceField: ["id"],
156
- destField: ["userId"],
147
+ sourceField: ['id'],
148
+ destField: ['userId'],
157
149
  destSchema: posts,
158
150
  }),
159
- }));
151
+ }))
160
152
 
161
153
  export const schema = createSchema({
162
154
  tables: [users, posts],
163
155
  relationships: [usersRelationships],
164
- });
156
+ })
165
157
 
166
- export type Schema = typeof schema;
158
+ export type Schema = typeof schema
167
159
 
168
- export const zql = createBuilder(schema);
160
+ export const zql = createBuilder(schema)
169
161
  ```
170
162
 
171
163
  ## Supported relation types
172
164
 
173
- | Lucid relation | Zero mapping |
174
- |---|---|
175
- | `hasOne` | `one(...)` |
176
- | `hasMany` | `many(...)` |
177
- | `belongsTo` | `one(...)` |
178
- | `manyToMany` | `many([...chain])` via pivot table |
179
- | `hasManyThrough` | `many([...chain])` |
165
+ | Lucid relation | Zero mapping |
166
+ | ---------------- | ---------------------------------- |
167
+ | `hasOne` | `one(...)` |
168
+ | `hasMany` | `many(...)` |
169
+ | `belongsTo` | `one(...)` |
170
+ | `manyToMany` | `many([...chain])` via pivot table |
171
+ | `hasManyThrough` | `many([...chain])` |
180
172
 
181
- Relations pointing to a model not found in `modelsSourcePath` are skipped with a warning.
173
+ Relations pointing to a model not found in `modelsDirectory` are skipped with a warning.
182
174
 
183
175
  ---
184
176
 
package/dist/index.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  type ConfigInput = {
3
- modelsSourcePath: string;
3
+ modelsDirectory?: string;
4
4
  /**
5
5
  * Models to exclude from the generated schema.
6
6
  * Typed as a constructor array rather than `LucidModel[]` so that model
@@ -8,9 +8,8 @@ type ConfigInput = {
8
8
  * without a structural mismatch from duplicate copies of the package.
9
9
  */
10
10
  excludeModels?: (abstract new (...args: any[]) => any)[];
11
- output?: string;
12
- prettier?: boolean;
13
- columnTypes?: Record<string, Record<string, string>>;
11
+ outputPath?: string;
12
+ columnTypeOverrides?: Record<string, Record<string, string>>;
14
13
  };
15
14
 
16
15
  export type { ConfigInput as Config };
package/dist/index.js CHANGED
@@ -17,61 +17,60 @@ async function getDefaultExportFromModulePath(modulePath) {
17
17
  try {
18
18
  await fs.access(modulePath);
19
19
  } catch {
20
- throw new Error(`lucid-zero: Module not found at ${modulePath}`);
20
+ throw new Error(`Module not found at ${modulePath}`);
21
21
  }
22
22
  const { tsImport } = await import("tsx/esm/api");
23
23
  const moduleUrl = url.pathToFileURL(modulePath).href;
24
24
  const module = await tsImport(moduleUrl, { parentURL: import.meta.url });
25
25
  const defaultExport = module.default;
26
26
  if (!defaultExport) {
27
- throw new Error(`lucid-zero: Module at ${modulePath} does not have a default export`);
27
+ throw new Error(`Module at ${modulePath} does not have a default export`);
28
28
  }
29
29
  return defaultExport;
30
30
  }
31
31
 
32
32
  // src/config.ts
33
- var DEFAULT_CONFIG_FILE_PATH = "lucid-zero.config.ts";
33
+ var DEFAULT_CONFIG_FILE_PATH = "config/lucid_zero.ts";
34
34
  var DEFAULT_OUTPUT_FILE_PATH = "zero-schema.gen.ts";
35
+ var DEFAULT_MODELS_DIRECTORY = "./app/models";
35
36
  var Config = class {
36
- modelsSourcePath;
37
+ modelsDirectory;
37
38
  excludeModels;
38
39
  outputFilePath;
39
- formatOutputFile;
40
- columnTypes;
40
+ columnTypeOverrides;
41
41
  tsconfigPath;
42
- /** Directory containing the config file — used to resolve relative paths */
43
- configDir;
42
+ /** Working directory used to resolve relative paths */
43
+ cwd;
44
44
  /** Populated by this.loadModels() */
45
45
  models = [];
46
- constructor(configInput, configDir = process.cwd(), tsconfigPath) {
47
- this.configDir = configDir;
48
- this.modelsSourcePath = configInput.modelsSourcePath;
46
+ constructor(configInput, tsconfigPath) {
47
+ this.cwd = process.cwd();
48
+ this.modelsDirectory = configInput.modelsDirectory ?? DEFAULT_MODELS_DIRECTORY;
49
49
  this.excludeModels = configInput.excludeModels ?? [];
50
- this.outputFilePath = path.resolve(configDir, configInput.output ?? DEFAULT_OUTPUT_FILE_PATH);
51
- this.tsconfigPath = tsconfigPath ?? path.resolve(configDir, "tsconfig.json");
52
- this.formatOutputFile = configInput.prettier ?? false;
53
- this.columnTypes = configInput.columnTypes ?? {};
50
+ this.outputFilePath = path.resolve(this.cwd, configInput.outputPath ?? DEFAULT_OUTPUT_FILE_PATH);
51
+ this.tsconfigPath = path.resolve(this.cwd, tsconfigPath ?? "tsconfig.json");
52
+ this.columnTypeOverrides = configInput.columnTypeOverrides ?? {};
54
53
  }
55
54
  async verify() {
56
- if (!this.modelsSourcePath) {
57
- throw new Error("lucid-zero: modelsSourcePath is required");
55
+ if (!this.modelsDirectory) {
56
+ throw new Error("modelsDirectory must not be empty");
58
57
  }
59
58
  await this.loadModels();
60
59
  if (this.models.length === 0) {
61
60
  throw new Error(
62
- `lucid-zero: No Lucid models found in modelsSourcePath. Each model file must have a default export that extends BaseModel.`
61
+ `No Lucid models found in modelsDirectory. Each model file must have a default export that extends BaseModel.`
63
62
  );
64
63
  }
65
- this.verifyColumnTypes();
64
+ this.verifyColumnTypeOverrides();
66
65
  }
67
66
  async loadModels() {
68
- const modelsSourceAbsPath = path.resolve(this.configDir, this.modelsSourcePath);
67
+ const modelsDirectoryAbsPath = path.resolve(this.cwd, this.modelsDirectory);
69
68
  let fileNames;
70
69
  try {
71
- fileNames = await fs2.readdir(modelsSourceAbsPath);
70
+ fileNames = await fs2.readdir(modelsDirectoryAbsPath);
72
71
  } catch (e) {
73
72
  throw new Error(
74
- `lucid-zero: Could not read modelsSourcePath at ${modelsSourceAbsPath}. Does the directory exist?Error: ${e}`
73
+ `Could not read modelsDirectory at ${modelsDirectoryAbsPath}. Does the directory exist?Error: ${e}`
75
74
  );
76
75
  }
77
76
  const tsFileNames = fileNames.filter((f) => f.endsWith(".ts") && !f.endsWith(".d.ts"));
@@ -81,10 +80,10 @@ var Config = class {
81
80
  let defaultExport;
82
81
  try {
83
82
  defaultExport = await getDefaultExportFromModulePath(
84
- path.join(modelsSourceAbsPath, fileName)
83
+ path.join(modelsDirectoryAbsPath, fileName)
85
84
  );
86
85
  } catch (err) {
87
- console.warn(`lucid-zero: Failed to import ${fileName}, skipping: ${String(err)}`);
86
+ console.warn(`Failed to import ${fileName}, skipping: ${String(err)}`);
88
87
  return null;
89
88
  }
90
89
  if (!this.isLucidModel(defaultExport)) {
@@ -100,20 +99,20 @@ var Config = class {
100
99
  );
101
100
  this.models = results.filter((m) => m !== null);
102
101
  }
103
- verifyColumnTypes() {
102
+ verifyColumnTypeOverrides() {
104
103
  const validZeroTypes = ["string", "number", "boolean", "json", "enumeration"];
105
104
  const modelNames = new Set(this.models.map((m) => m.name));
106
- for (const [modelName, columnOverrides] of Object.entries(this.columnTypes ?? {})) {
105
+ for (const [modelName, columnOverrides] of Object.entries(this.columnTypeOverrides ?? {})) {
107
106
  if (!modelNames.has(modelName)) {
108
107
  throw new Error(
109
- `lucid-zero: columnTypes has unknown model "${modelName}". Known models: ${[...modelNames].join(", ")}`
108
+ `columnTypeOverrides has unknown model "${modelName}". Known models: ${[...modelNames].join(", ")}`
110
109
  );
111
110
  }
112
111
  for (const [attributeName, typeString] of Object.entries(columnOverrides)) {
113
112
  const baseTypeName = typeString.match(/^([a-z]+)/)?.[1];
114
113
  if (!baseTypeName || !validZeroTypes.includes(baseTypeName)) {
115
114
  throw new Error(
116
- `lucid-zero: columnTypes override for ${modelName}.${attributeName} has unrecognised type "${typeString}". Expected one of: ${validZeroTypes.join(", ")}`
115
+ `columnTypeOverrides override for ${modelName}.${attributeName} has unrecognised type "${typeString}". Expected one of: ${validZeroTypes.join(", ")}`
117
116
  );
118
117
  }
119
118
  }
@@ -137,11 +136,11 @@ var ConfigLoader = class {
137
136
  defaultExport = await getDefaultExportFromModulePath(absoluteConfigPath);
138
137
  } catch (err) {
139
138
  throw new Error(
140
- `lucid-zero: Failed to import config at ${absoluteConfigPath}
141
- Error: ${String(err)}`
139
+ `Failed to import config at ${absoluteConfigPath}
140
+ Error: ${err instanceof Error ? err.message : String(err)}`
142
141
  );
143
142
  }
144
- const config = new Config(defaultExport, path.dirname(absoluteConfigPath), opts.tsconfigPath);
143
+ const config = new Config(defaultExport, opts.tsconfigPath);
145
144
  return config;
146
145
  }
147
146
  };
@@ -166,13 +165,13 @@ var SchemaTransformer = class {
166
165
  project;
167
166
  constructor(config, tsconfigPath) {
168
167
  this.config = config;
169
- this.project = new Project({ tsConfigFilePath: tsconfigPath ?? "tsconfig.json" });
168
+ this.project = new Project({ tsConfigFilePath: tsconfigPath ?? config.tsconfigPath });
170
169
  }
171
170
  transform() {
172
171
  return {
173
172
  models: this.config.models.map((model) => {
174
173
  const columnNameAndTypePairs = this.extractColumnNameAndType(model);
175
- const columnOverrides = this.config.columnTypes?.[model.name];
174
+ const columnOverrides = this.config.columnTypeOverrides?.[model.name];
176
175
  return {
177
176
  tableName: model.table,
178
177
  columns: this.mapColumns(columnNameAndTypePairs, columnOverrides, model),
@@ -190,12 +189,12 @@ var SchemaTransformer = class {
190
189
  });
191
190
  if (!modelSourceFile) {
192
191
  throw new Error(
193
- `lucid-zero: Could not find source file for model ${model.name} in ${this.project.getSourceFiles().map((sourceFile) => sourceFile.getFilePath()).join(", ")}`
192
+ `Could not find source file for model ${model.name} in ${this.project.getSourceFiles().map((sourceFile) => sourceFile.getFilePath()).join(", ")}`
194
193
  );
195
194
  }
196
195
  const classDeclaration = modelSourceFile.getClass(model.name);
197
196
  if (!classDeclaration) {
198
- throw new Error(`lucid-zero: Could not find class declaration for model ${model.name}`);
197
+ throw new Error(`Could not find class declaration for model ${model.name}`);
199
198
  }
200
199
  let currentClass = classDeclaration;
201
200
  while (currentClass) {
@@ -226,7 +225,7 @@ var SchemaTransformer = class {
226
225
  zeroType = tsTypeToZeroType(tsType);
227
226
  } else {
228
227
  console.warn(
229
- `lucid-zero: Could not infer Zero type for ${model.name}.${attributeName}. Falling back to json(). Add a columnTypes override if this is wrong.`
228
+ `Could not infer Zero type for ${model.name}.${attributeName}. Falling back to json(). Add a columnTypeOverrides entry if this is wrong.`
230
229
  );
231
230
  zeroType = "json()";
232
231
  }
@@ -247,13 +246,13 @@ var SchemaTransformer = class {
247
246
  relation.boot();
248
247
  } catch (err) {
249
248
  throw new Error(
250
- `lucid-zero: Failed to boot relation "${relationName}" on ${model.name}: ${String(err)}`
249
+ `Failed to boot relation "${relationName}" on ${model.name}: ${String(err)}`
251
250
  );
252
251
  }
253
252
  const relatedModel = relation.relatedModel();
254
253
  if (!allModels.some((m) => m.name === relatedModel.name)) {
255
254
  console.info(
256
- `lucid-zero: Skipping relation "${relationName}" on ${model.name} \u2014 ${relatedModel.name} is not included.`
255
+ `Skipping relation "${relationName}" on ${model.name} \u2014 ${relatedModel.name} is not included.`
257
256
  );
258
257
  continue;
259
258
  }
@@ -285,7 +284,7 @@ var SchemaTransformer = class {
285
284
  case "manyToMany": {
286
285
  if (!relation.pivotTable || !relation.pivotForeignKey || !relation.pivotRelatedForeignKey) {
287
286
  throw new Error(
288
- `lucid-zero: manyToMany relation "${relationName}" on ${model.name} \u2014 pivot table fields could not be resolved after boot().`
287
+ `manyToMany relation "${relationName}" on ${model.name} \u2014 pivot table fields could not be resolved after boot().`
289
288
  );
290
289
  }
291
290
  relationships[relationName] = {
@@ -310,7 +309,7 @@ var SchemaTransformer = class {
310
309
  const throughRel = relation;
311
310
  if (!throughRel.throughModel) {
312
311
  throw new Error(
313
- `lucid-zero: hasManyThrough relation "${relationName}" on ${model.name} \u2014 throughModel could not be resolved after boot().`
312
+ `hasManyThrough relation "${relationName}" on ${model.name} \u2014 throughModel could not be resolved after boot().`
314
313
  );
315
314
  }
316
315
  const throughModel = throughRel.throughModel();
@@ -350,39 +349,6 @@ function tsTypeToZeroType(type) {
350
349
 
351
350
  // src/code-generator.ts
352
351
  import fs3 from "fs";
353
-
354
- // src/formatter.ts
355
- var Formatter = class _Formatter {
356
- static async loadPrettier() {
357
- let prettier;
358
- try {
359
- const { createRequire } = await import("module");
360
- const req = createRequire(process.cwd() + "/package.json");
361
- const prettierPath = req.resolve("prettier");
362
- const { pathToFileURL } = await import("url");
363
- prettier = await import(pathToFileURL(prettierPath).href);
364
- return prettier;
365
- } catch {
366
- return null;
367
- }
368
- }
369
- static async format(code) {
370
- const prettier = await _Formatter.loadPrettier();
371
- if (!prettier) {
372
- console.warn("lucid-zero: prettier not found \u2014 skipping formatting");
373
- return code;
374
- }
375
- try {
376
- const options = await prettier.resolveConfig(process.cwd());
377
- return await prettier.format(code, { ...options, parser: "typescript" });
378
- } catch {
379
- console.warn("lucid-zero: prettier formatting failed \u2014 skipping");
380
- return code;
381
- }
382
- }
383
- };
384
-
385
- // src/code-generator.ts
386
352
  var HEADER = `// This file was automatically generated by lucid-zero.
387
353
  // Do NOT edit this file manually \u2014 it will be overwritten on the next run.`;
388
354
  var CodeGenerator = class {
@@ -393,10 +359,7 @@ var CodeGenerator = class {
393
359
  this.schema = schema;
394
360
  }
395
361
  async generateToOutputFile() {
396
- let code = this.generate();
397
- if (this.config.formatOutputFile) {
398
- code = await Formatter.format(code);
399
- }
362
+ const code = this.generate();
400
363
  fs3.writeFileSync(this.config.outputFilePath, code);
401
364
  }
402
365
  generate() {
@@ -548,7 +511,7 @@ async function run(options) {
548
511
  try {
549
512
  const config = await ConfigLoader.load(options);
550
513
  await config.verify();
551
- const schemaTransformer = new SchemaTransformer(config, options.tsconfigPath);
514
+ const schemaTransformer = new SchemaTransformer(config);
552
515
  const transformedSchema = schemaTransformer.transform();
553
516
  const codeGenerator = new CodeGenerator(config, transformedSchema);
554
517
  await codeGenerator.generateToOutputFile();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@antislop/zero-lucid-generator",
3
- "version": "1.0.0",
3
+ "version": "1.1.1",
4
4
  "description": "Generate Zero schemas from Lucid ORM models",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",
@@ -19,13 +19,20 @@
19
19
  "check-types": "tsc --noEmit",
20
20
  "lint": "eslint src tests",
21
21
  "lint:fix": "eslint src tests --fix",
22
- "format": "prettier --write \"src/**/*.ts\" \"tests/**/*.ts\"",
23
- "format:check": "prettier --check \"src/**/*.ts\" \"tests/**/*.ts\""
22
+ "format": "oxfmt \"src/**/*.ts\" \"tests/**/*.ts\"",
23
+ "format:check": "oxfmt --check \"src/**/*.ts\" \"tests/**/*.ts\""
24
24
  },
25
25
  "bin": {
26
- "lucid-zero": "./dist/index.js"
26
+ "lucid-zero": "dist/index.js"
27
27
  },
28
- "keywords": ["zero", "lucid", "adonisjs", "schema", "generator", "rocicorp"],
28
+ "keywords": [
29
+ "zero",
30
+ "lucid",
31
+ "adonisjs",
32
+ "schema",
33
+ "generator",
34
+ "rocicorp"
35
+ ],
29
36
  "author": "antislop",
30
37
  "license": "ISC",
31
38
  "publishConfig": {
@@ -43,8 +50,7 @@
43
50
  "@typescript-eslint/parser": "^8.61.0",
44
51
  "@vitest/coverage-v8": "^4.1.8",
45
52
  "eslint": "^10.4.1",
46
- "eslint-config-prettier": "^10.1.8",
47
- "prettier": "^3.8.4",
53
+ "oxfmt": "^0.54.0",
48
54
  "tsup": "^8.5.1",
49
55
  "tsx": "^4.22.4",
50
56
  "typescript": "^6.0.3",