@antislop/zero-lucid-generator 1.1.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,32 +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
- | `format` | `boolean` | `false` | Format the output with [oxfmt](https://oxc.rs/docs/guide/usage/formatter) (must be installed). Respects your `.oxfmtrc.json`. |
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 |
66
-
67
- ### Formatting with oxfmt
68
-
69
- Set `format: true` to have the generated file automatically formatted on every run:
70
-
71
- ```ts
72
- const config: Config = {
73
- modelsSourcePath: './app/models',
74
- format: true,
75
- }
76
- ```
77
-
78
- Install oxfmt in your project:
79
-
80
- ```sh
81
- npm add -D oxfmt
82
- ```
83
-
84
- The formatter picks up your `.oxfmtrc.json` / `oxfmt.config.ts` automatically, so the output will always match what your own format scripts produce.
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 |
85
65
 
86
66
  ---
87
67
 
@@ -91,8 +71,8 @@ The formatter picks up your `.oxfmtrc.json` / `oxfmt.config.ts` automatically, s
91
71
  lucid-zero generate [options]
92
72
 
93
73
  Options:
94
- -c, --config <path> Path to config file (default: lucid-zero.config.ts)
95
- -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)
96
76
  ```
97
77
 
98
78
  ---
@@ -101,24 +81,24 @@ Options:
101
81
 
102
82
  `@antislop/zero-lucid-generator` maps TypeScript types to Zero types automatically:
103
83
 
104
- | TypeScript type | Zero type |
105
- |---|---|
106
- | `string` | `string()` |
107
- | `number` | `number()` |
108
- | `boolean` | `boolean()` |
109
- | `DateTime` / `Date` | `number()` (Unix ms) |
110
- | `object` / `unknown` / `any` | `json()` |
111
- | `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()` |
112
92
 
113
- 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`:
114
94
 
115
95
  ```ts
116
96
  import type { Config } from '@antislop/zero-lucid-generator'
117
- import Session from "#models/session";
97
+ import Session from '#models/session'
118
98
 
119
99
  const config: Config = {
120
- modelsSourcePath: './app/models',
121
- columnTypes: {
100
+ modelsDirectory: './app/models',
101
+ columnTypeOverrides: {
122
102
  Issue: {
123
103
  // metadata is typed as `unknown` but has a known shape
124
104
  metadata: 'json<{ priority: number; labels: string[] }>()',
@@ -143,61 +123,54 @@ Valid Zero base types: `string`, `number`, `boolean`, `json`, `enumeration`.
143
123
  ```ts
144
124
  // zero-schema.gen.ts (auto-generated — do not edit)
145
125
 
146
- import {
147
- createBuilder,
148
- createSchema,
149
- number,
150
- relationships,
151
- string,
152
- table,
153
- } from "@rocicorp/zero";
126
+ import { createBuilder, createSchema, number, relationships, string, table } from '@rocicorp/zero'
154
127
 
155
- export const users = table("users")
128
+ export const users = table('users')
156
129
  .columns({
157
130
  id: number(),
158
131
  name: string(),
159
132
  email: string(),
160
133
  })
161
- .primaryKey("id");
134
+ .primaryKey('id')
162
135
 
163
- export const posts = table("posts")
136
+ export const posts = table('posts')
164
137
  .columns({
165
138
  id: number(),
166
139
  userId: number().from('user_id'),
167
140
  title: string(),
168
141
  body: string().optional(),
169
142
  })
170
- .primaryKey("id");
143
+ .primaryKey('id')
171
144
 
172
145
  export const usersRelationships = relationships(users, ({ many }) => ({
173
146
  posts: many({
174
- sourceField: ["id"],
175
- destField: ["userId"],
147
+ sourceField: ['id'],
148
+ destField: ['userId'],
176
149
  destSchema: posts,
177
150
  }),
178
- }));
151
+ }))
179
152
 
180
153
  export const schema = createSchema({
181
154
  tables: [users, posts],
182
155
  relationships: [usersRelationships],
183
- });
156
+ })
184
157
 
185
- export type Schema = typeof schema;
158
+ export type Schema = typeof schema
186
159
 
187
- export const zql = createBuilder(schema);
160
+ export const zql = createBuilder(schema)
188
161
  ```
189
162
 
190
163
  ## Supported relation types
191
164
 
192
- | Lucid relation | Zero mapping |
193
- |---|---|
194
- | `hasOne` | `one(...)` |
195
- | `hasMany` | `many(...)` |
196
- | `belongsTo` | `one(...)` |
197
- | `manyToMany` | `many([...chain])` via pivot table |
198
- | `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])` |
199
172
 
200
- 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.
201
174
 
202
175
  ---
203
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,10 +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
- /** Format the output file with oxfmt. Requires oxfmt to be installed. */
13
- format?: boolean;
14
- columnTypes?: Record<string, Record<string, string>>;
11
+ outputPath?: string;
12
+ columnTypeOverrides?: Record<string, Record<string, string>>;
15
13
  };
16
14
 
17
15
  export type { ConfigInput as Config };
package/dist/index.js CHANGED
@@ -30,48 +30,47 @@ async function getDefaultExportFromModulePath(modulePath) {
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.format ?? 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("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
- `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
- `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,7 +80,7 @@ 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
86
  console.warn(`Failed to import ${fileName}, skipping: ${String(err)}`);
@@ -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
- `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
- `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
  }
@@ -141,7 +140,7 @@ var ConfigLoader = class {
141
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),
@@ -226,7 +225,7 @@ var SchemaTransformer = class {
226
225
  zeroType = tsTypeToZeroType(tsType);
227
226
  } else {
228
227
  console.warn(
229
- `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
  }
@@ -350,53 +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
- import { spawnSync } from "child_process";
356
- import { createRequire } from "module";
357
- import path2 from "path";
358
- function resolveOxfmtBin() {
359
- try {
360
- const req = createRequire(process.cwd() + "/package.json");
361
- const pkgMain = req.resolve("oxfmt");
362
- const pkgRoot = path2.join(pkgMain, "..", "..");
363
- return path2.join(pkgRoot, "bin", "oxfmt");
364
- } catch {
365
- return null;
366
- }
367
- }
368
- var Formatter = class {
369
- /**
370
- * Formats `code` by piping it through the oxfmt CLI with `--stdin-filepath`
371
- * set to the output file's absolute path. This lets oxfmt walk up the
372
- * directory tree and discover `.oxfmtrc.json` / `oxfmt.config.ts` in the
373
- * user's project.
374
- */
375
- static async format(code, outputFilePath) {
376
- const bin = resolveOxfmtBin();
377
- if (!bin) {
378
- console.warn("lucid-zero: oxfmt not found \u2014 skipping formatting");
379
- return code;
380
- }
381
- const result = spawnSync(
382
- process.execPath,
383
- [bin, `--stdin-filepath=${outputFilePath}`],
384
- {
385
- input: code,
386
- encoding: "utf8",
387
- cwd: process.cwd()
388
- }
389
- );
390
- if (result.status !== 0) {
391
- console.warn(`lucid-zero: oxfmt formatting failed \u2014 skipping
392
- ${result.stderr ?? ""}`);
393
- return code;
394
- }
395
- return result.stdout;
396
- }
397
- };
398
-
399
- // src/code-generator.ts
400
352
  var HEADER = `// This file was automatically generated by lucid-zero.
401
353
  // Do NOT edit this file manually \u2014 it will be overwritten on the next run.`;
402
354
  var CodeGenerator = class {
@@ -407,10 +359,7 @@ var CodeGenerator = class {
407
359
  this.schema = schema;
408
360
  }
409
361
  async generateToOutputFile() {
410
- let code = this.generate();
411
- if (this.config.formatOutputFile) {
412
- code = await Formatter.format(code, this.config.outputFilePath);
413
- }
362
+ const code = this.generate();
414
363
  fs3.writeFileSync(this.config.outputFilePath, code);
415
364
  }
416
365
  generate() {
@@ -562,7 +511,7 @@ async function run(options) {
562
511
  try {
563
512
  const config = await ConfigLoader.load(options);
564
513
  await config.verify();
565
- const schemaTransformer = new SchemaTransformer(config, options.tsconfigPath);
514
+ const schemaTransformer = new SchemaTransformer(config);
566
515
  const transformedSchema = schemaTransformer.transform();
567
516
  const codeGenerator = new CodeGenerator(config, transformedSchema);
568
517
  await codeGenerator.generateToOutputFile();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@antislop/zero-lucid-generator",
3
- "version": "1.1.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",
@@ -23,7 +23,7 @@
23
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
28
  "keywords": [
29
29
  "zero",