@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 +42 -69
- package/dist/index.d.ts +3 -5
- package/dist/index.js +30 -81
- package/package.json +2 -2
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 `
|
|
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
|
-
|
|
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`
|
|
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
|
|
60
|
-
|
|
61
|
-
| `
|
|
62
|
-
| `
|
|
63
|
-
| `
|
|
64
|
-
| `
|
|
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:
|
|
95
|
-
-t, --tsconfig <path> Path to tsconfig.json (default: tsconfig.json
|
|
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
|
|
105
|
-
|
|
106
|
-
| `string`
|
|
107
|
-
| `number`
|
|
108
|
-
| `boolean`
|
|
109
|
-
| `DateTime` / `Date`
|
|
110
|
-
| `object` / `unknown` / `any`
|
|
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 `
|
|
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
|
|
97
|
+
import Session from '#models/session'
|
|
118
98
|
|
|
119
99
|
const config: Config = {
|
|
120
|
-
|
|
121
|
-
|
|
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(
|
|
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(
|
|
134
|
+
.primaryKey('id')
|
|
162
135
|
|
|
163
|
-
export const posts = table(
|
|
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(
|
|
143
|
+
.primaryKey('id')
|
|
171
144
|
|
|
172
145
|
export const usersRelationships = relationships(users, ({ many }) => ({
|
|
173
146
|
posts: many({
|
|
174
|
-
sourceField: [
|
|
175
|
-
destField: [
|
|
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
|
|
193
|
-
|
|
194
|
-
| `hasOne`
|
|
195
|
-
| `hasMany`
|
|
196
|
-
| `belongsTo`
|
|
197
|
-
| `manyToMany`
|
|
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 `
|
|
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
|
-
|
|
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
|
-
|
|
12
|
-
|
|
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 = "
|
|
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
|
-
|
|
37
|
+
modelsDirectory;
|
|
37
38
|
excludeModels;
|
|
38
39
|
outputFilePath;
|
|
39
|
-
|
|
40
|
-
columnTypes;
|
|
40
|
+
columnTypeOverrides;
|
|
41
41
|
tsconfigPath;
|
|
42
|
-
/**
|
|
43
|
-
|
|
42
|
+
/** Working directory used to resolve relative paths */
|
|
43
|
+
cwd;
|
|
44
44
|
/** Populated by this.loadModels() */
|
|
45
45
|
models = [];
|
|
46
|
-
constructor(configInput,
|
|
47
|
-
this.
|
|
48
|
-
this.
|
|
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(
|
|
51
|
-
this.tsconfigPath =
|
|
52
|
-
this.
|
|
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.
|
|
57
|
-
throw new Error("
|
|
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
|
|
61
|
+
`No Lucid models found in modelsDirectory. Each model file must have a default export that extends BaseModel.`
|
|
63
62
|
);
|
|
64
63
|
}
|
|
65
|
-
this.
|
|
64
|
+
this.verifyColumnTypeOverrides();
|
|
66
65
|
}
|
|
67
66
|
async loadModels() {
|
|
68
|
-
const
|
|
67
|
+
const modelsDirectoryAbsPath = path.resolve(this.cwd, this.modelsDirectory);
|
|
69
68
|
let fileNames;
|
|
70
69
|
try {
|
|
71
|
-
fileNames = await fs2.readdir(
|
|
70
|
+
fileNames = await fs2.readdir(modelsDirectoryAbsPath);
|
|
72
71
|
} catch (e) {
|
|
73
72
|
throw new Error(
|
|
74
|
-
`Could not read
|
|
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(
|
|
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
|
-
|
|
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.
|
|
105
|
+
for (const [modelName, columnOverrides] of Object.entries(this.columnTypeOverrides ?? {})) {
|
|
107
106
|
if (!modelNames.has(modelName)) {
|
|
108
107
|
throw new Error(
|
|
109
|
-
`
|
|
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
|
-
`
|
|
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,
|
|
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 ??
|
|
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.
|
|
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
|
|
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
|
-
|
|
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
|
|
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.
|
|
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": "
|
|
26
|
+
"lucid-zero": "dist/index.js"
|
|
27
27
|
},
|
|
28
28
|
"keywords": [
|
|
29
29
|
"zero",
|