@frigear-nu/module-kit 0.0.4 → 0.0.6
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
|
@@ -116,6 +116,38 @@ call with the merged columns and deduplicated imports. The base `schema` file mu
|
|
|
116
116
|
`sqliteTable('users', { ... })`) matching `name`, since it determines which factory (`sqliteTable`/`pgTable`/etc.) and
|
|
117
117
|
SQL table name to use for the merged result.
|
|
118
118
|
|
|
119
|
+
`name` also accepts an array, so a single `file` can export extension columns for several tables in one go, the same
|
|
120
|
+
way `registerDatabaseTable` does:
|
|
121
|
+
|
|
122
|
+
```ts
|
|
123
|
+
extendDatabaseTable(nuxt, {
|
|
124
|
+
name: ['users', 'userPasskeys'],
|
|
125
|
+
resolver: createResolver(import.meta.url),
|
|
126
|
+
file: dialect => `./runtime/db/extra.${dialect}`,
|
|
127
|
+
})
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
```ts
|
|
131
|
+
// runtime/db/extra.sqlite.ts
|
|
132
|
+
export const users = sqliteTable('users', {
|
|
133
|
+
locale: text().default('da'),
|
|
134
|
+
})
|
|
135
|
+
export const userPasskeys = sqliteTable('user_passkeys', {
|
|
136
|
+
label: text(),
|
|
137
|
+
})
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
`name` can also be omitted entirely, in which case `file` is registered against every table that generates a schema -
|
|
141
|
+
whichever table(s) it actually has a matching export for pick up its columns, exactly as the shared Nuxt-layer
|
|
142
|
+
`server/db/extend.<dialect>.ts` files already do; tables with no matching export in `file` are left untouched:
|
|
143
|
+
|
|
144
|
+
```ts
|
|
145
|
+
extendDatabaseTable(nuxt, {
|
|
146
|
+
resolver: createResolver(import.meta.url),
|
|
147
|
+
file: dialect => `./runtime/db/extra.${dialect}`,
|
|
148
|
+
})
|
|
149
|
+
```
|
|
150
|
+
|
|
119
151
|
#### Using a `factory`
|
|
120
152
|
|
|
121
153
|
Pass `factory` to `registerDatabaseTable` when you need shared defaults (e.g. timestamps) applied consistently across
|
package/dist/module.json
CHANGED
|
@@ -3,6 +3,7 @@ export type ParsedTableExport = {
|
|
|
3
3
|
tableCall?: {
|
|
4
4
|
callee: string;
|
|
5
5
|
tableName: string;
|
|
6
|
+
extraArgs?: string[];
|
|
6
7
|
};
|
|
7
8
|
/** Column (or spread) entries keyed by property name, in source order. Values are the raw `key: value` source text. */
|
|
8
9
|
properties: Map<string, string>;
|
|
@@ -21,3 +22,10 @@ export type ParsedTableExport = {
|
|
|
21
22
|
* when `exportName` is `'default'`
|
|
22
23
|
*/
|
|
23
24
|
export declare const parseRegularTableExport: (filePath: string, exportName: string) => ParsedTableExport | undefined;
|
|
25
|
+
/**
|
|
26
|
+
* Lists the names of every named export in a "regular" drizzle schema file that looks like a table (or columns)
|
|
27
|
+
* export - i.e. every export that `parseRegularTableExport` would be able to parse. Used by `registerDatabaseTable`
|
|
28
|
+
* to auto-discover table names when none are explicitly provided. Does not include the `default` export, since it
|
|
29
|
+
* has no name to register a table under.
|
|
30
|
+
*/
|
|
31
|
+
export declare const listRegularTableExportNames: (filePath: string) => string[];
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import { readFileSync } from "node:fs";
|
|
2
2
|
import ts from "typescript";
|
|
3
|
-
|
|
3
|
+
const parseSource = (filePath) => {
|
|
4
4
|
const text = readFileSync(filePath, "utf8");
|
|
5
|
-
|
|
5
|
+
return ts.createSourceFile(filePath, text, ts.ScriptTarget.Latest, true, ts.ScriptKind.TS);
|
|
6
|
+
};
|
|
7
|
+
const collectImports = (source) => {
|
|
6
8
|
const imports = /* @__PURE__ */ new Map();
|
|
7
9
|
for (const statement of source.statements) {
|
|
8
10
|
if (ts.isImportDeclaration(statement) && statement.importClause?.namedBindings && ts.isNamedImports(statement.importClause.namedBindings) && ts.isStringLiteralLike(statement.moduleSpecifier)) {
|
|
@@ -11,43 +13,64 @@ export const parseRegularTableExport = (filePath, exportName) => {
|
|
|
11
13
|
imports.set(statement.moduleSpecifier.text, specifiers);
|
|
12
14
|
}
|
|
13
15
|
}
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
} else if (ts.isObjectLiteralExpression(
|
|
26
|
-
objectLiteral =
|
|
16
|
+
return imports;
|
|
17
|
+
};
|
|
18
|
+
const toTableResult = (initializer, imports) => {
|
|
19
|
+
let objectLiteral;
|
|
20
|
+
let tableCall;
|
|
21
|
+
if (ts.isCallExpression(initializer)) {
|
|
22
|
+
const args = initializer.arguments;
|
|
23
|
+
if (args.length >= 2 && ts.isStringLiteralLike(args[0]) && ts.isObjectLiteralExpression(args[1])) {
|
|
24
|
+
const extraArgs = args.length > 2 ? args.slice(2).map((arg) => arg.getText()) : void 0;
|
|
25
|
+
tableCall = { callee: initializer.expression.getText(), tableName: args[0].text, extraArgs };
|
|
26
|
+
objectLiteral = args[1];
|
|
27
|
+
} else if (args.length === 1 && ts.isObjectLiteralExpression(args[0])) {
|
|
28
|
+
objectLiteral = args[0];
|
|
27
29
|
}
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
30
|
+
} else if (ts.isObjectLiteralExpression(initializer)) {
|
|
31
|
+
objectLiteral = initializer;
|
|
32
|
+
}
|
|
33
|
+
if (!objectLiteral) return void 0;
|
|
34
|
+
const properties = /* @__PURE__ */ new Map();
|
|
35
|
+
for (const property of objectLiteral.properties) {
|
|
36
|
+
if (ts.isPropertyAssignment(property) || ts.isShorthandPropertyAssignment(property)) {
|
|
37
|
+
properties.set(property.name.getText(), property.getText());
|
|
38
|
+
} else if (ts.isSpreadAssignment(property)) {
|
|
39
|
+
properties.set(`__spread_${properties.size}`, property.getText());
|
|
36
40
|
}
|
|
37
|
-
|
|
38
|
-
};
|
|
41
|
+
}
|
|
42
|
+
return { tableCall, properties, imports };
|
|
43
|
+
};
|
|
44
|
+
export const parseRegularTableExport = (filePath, exportName) => {
|
|
45
|
+
const source = parseSource(filePath);
|
|
46
|
+
const imports = collectImports(source);
|
|
39
47
|
for (const statement of source.statements) {
|
|
40
48
|
if (ts.isVariableStatement(statement) && statement.modifiers?.some((modifier) => modifier.kind === ts.SyntaxKind.ExportKeyword)) {
|
|
41
49
|
for (const declaration of statement.declarationList.declarations) {
|
|
42
50
|
if (ts.isIdentifier(declaration.name) && declaration.name.text === exportName && declaration.initializer) {
|
|
43
|
-
const result =
|
|
51
|
+
const result = toTableResult(declaration.initializer, imports);
|
|
44
52
|
if (result) return result;
|
|
45
53
|
}
|
|
46
54
|
}
|
|
47
55
|
} else if (exportName === "default" && ts.isExportAssignment(statement) && !statement.isExportEquals) {
|
|
48
|
-
const result =
|
|
56
|
+
const result = toTableResult(statement.expression, imports);
|
|
49
57
|
if (result) return result;
|
|
50
58
|
}
|
|
51
59
|
}
|
|
52
60
|
return void 0;
|
|
53
61
|
};
|
|
62
|
+
export const listRegularTableExportNames = (filePath) => {
|
|
63
|
+
const source = parseSource(filePath);
|
|
64
|
+
const imports = collectImports(source);
|
|
65
|
+
const names = [];
|
|
66
|
+
for (const statement of source.statements) {
|
|
67
|
+
if (ts.isVariableStatement(statement) && statement.modifiers?.some((modifier) => modifier.kind === ts.SyntaxKind.ExportKeyword)) {
|
|
68
|
+
for (const declaration of statement.declarationList.declarations) {
|
|
69
|
+
if (ts.isIdentifier(declaration.name) && declaration.initializer && toTableResult(declaration.initializer, imports)) {
|
|
70
|
+
names.push(declaration.name.text);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
return names;
|
|
76
|
+
};
|
|
@@ -42,12 +42,22 @@ export declare const defineExtendableTable: <T>(table: TableDefinerFn<T>) => (pr
|
|
|
42
42
|
*/
|
|
43
43
|
export declare const onDatabaseSchemaExtend: (nuxt: Nuxt, extender: DbExtenderFn) => Promise<() => void>;
|
|
44
44
|
type DatabaseTableOptions = {
|
|
45
|
-
|
|
45
|
+
/**
|
|
46
|
+
* The name of the table's export (e.g. `users`). Pass an array of names (e.g. `['users', 'userPasskeys']`) to
|
|
47
|
+
* register several tables that are exported from the same `schema` file in one go - they're generated into a
|
|
48
|
+
* single file so that tables can reference each other (e.g. a foreign key `.references(() => users.id)`). An
|
|
49
|
+
* array of names (or omitting `name` entirely) is only supported when `factory` is omitted.
|
|
50
|
+
*
|
|
51
|
+
* When omitted, every table-like export in `schema` (and its extension files) is registered automatically - this
|
|
52
|
+
* is equivalent to passing an array of every such export's name.
|
|
53
|
+
*/
|
|
54
|
+
name?: string | string[];
|
|
46
55
|
/**
|
|
47
56
|
* The exported factory function name to call with merged extension columns (e.g. `defineUsersTable`), as produced
|
|
48
57
|
* by `defineExtendableTable`. When omitted, `schema` (and every registered/layer extension file) is treated as a
|
|
49
58
|
* "regular" drizzle file - e.g. `export const users = sqliteTable('users', { ... })` - and its columns are merged
|
|
50
|
-
* directly, without requiring any helper wrapper.
|
|
59
|
+
* directly, without requiring any helper wrapper. Only supported when `name` is a single string, since the
|
|
60
|
+
* factory is called once with a single table's columns.
|
|
51
61
|
*/
|
|
52
62
|
factory?: string;
|
|
53
63
|
resolver: ReturnType<typeof createResolver>;
|
|
@@ -56,7 +66,16 @@ type DatabaseTableOptions = {
|
|
|
56
66
|
extensionFile?: (dialect: string) => string;
|
|
57
67
|
};
|
|
58
68
|
type TableExtensionOptions = {
|
|
59
|
-
|
|
69
|
+
/**
|
|
70
|
+
* The name of the table to extend (e.g. `users`). Pass an array of names (e.g. `['users', 'userPasskeys']`) when
|
|
71
|
+
* `file` exports columns for several tables at once - it's resolved and parsed once per matching table, same as
|
|
72
|
+
* if `extendDatabaseTable` had been called separately for each name with the same `file`.
|
|
73
|
+
*
|
|
74
|
+
* When omitted, `file` is registered against every table - whichever table(s) it actually has a matching export
|
|
75
|
+
* for (parsed the same way as the shared Nuxt-layer `server/db/extend.<dialect>.ts` files) pick up its columns;
|
|
76
|
+
* tables it has no matching export for are unaffected.
|
|
77
|
+
*/
|
|
78
|
+
name?: string | string[];
|
|
60
79
|
resolver: ReturnType<typeof createResolver>;
|
|
61
80
|
file: (dialect: string) => string;
|
|
62
81
|
};
|
|
@@ -71,6 +90,10 @@ export declare const extendDatabaseTable: (nuxt: Nuxt, options: TableExtensionOp
|
|
|
71
90
|
* table by importing the named `factory` from `schema`, merging in any columns registered
|
|
72
91
|
* through `extendDatabaseTable` and any Nuxt-layer extension files, then calling the factory with
|
|
73
92
|
* `{ name, columns }`.
|
|
93
|
+
*
|
|
94
|
+
* When `name` is an array, or omitted entirely, every (named or auto-discovered) table is generated into a single
|
|
95
|
+
* file - this lets tables declared in the same `schema` file reference each other directly (e.g. a foreign key
|
|
96
|
+
* `.references(() => users.id)` from a `userPasskeys` table), since they share the same module scope.
|
|
74
97
|
*/
|
|
75
98
|
export declare const registerDatabaseTable: (nuxt: Nuxt, options: DatabaseTableOptions) => Promise<() => void>;
|
|
76
99
|
export {};
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { addTemplate, createResolver } from "@nuxt/kit";
|
|
2
2
|
import { existsSync } from "node:fs";
|
|
3
|
-
import { parseRegularTableExport } from "./regular-table.js";
|
|
3
|
+
import { listRegularTableExportNames, parseRegularTableExport } from "./regular-table.js";
|
|
4
4
|
export const defineTableColumns = (columns) => {
|
|
5
5
|
return columns;
|
|
6
6
|
};
|
|
@@ -13,8 +13,9 @@ export const onDatabaseSchemaExtend = async (nuxt, extender) => {
|
|
|
13
13
|
return nuxt.hook("hub:db:schema:extend", extender);
|
|
14
14
|
};
|
|
15
15
|
export const extendDatabaseTable = (nuxt, options) => {
|
|
16
|
+
const names = options.name === void 0 ? void 0 : Array.isArray(options.name) ? options.name : [options.name];
|
|
16
17
|
nuxt.hook("module-kit:db:table:extend", async ({ name, dialect, paths }) => {
|
|
17
|
-
if (
|
|
18
|
+
if (names === void 0 || names.includes(name)) {
|
|
18
19
|
const file = await options.resolver.resolvePath(options.file(dialect));
|
|
19
20
|
if (existsSync(file)) paths.push(file);
|
|
20
21
|
}
|
|
@@ -22,73 +23,102 @@ export const extendDatabaseTable = (nuxt, options) => {
|
|
|
22
23
|
};
|
|
23
24
|
export const registerDatabaseTable = (nuxt, options) => {
|
|
24
25
|
const { name, factory, resolver, schema, override, extensionFile } = options;
|
|
26
|
+
if (factory && (name === void 0 || Array.isArray(name))) {
|
|
27
|
+
throw new Error(
|
|
28
|
+
'[module-kit] registerDatabaseTable requires a single "name" (not an array, and not omitted) when "factory" is set. Omit "factory" to register multiple (or auto-discovered) tables from the same regular drizzle schema file.'
|
|
29
|
+
);
|
|
30
|
+
}
|
|
25
31
|
return onDatabaseSchemaExtend(nuxt, async ({ dialect, paths }) => {
|
|
26
32
|
if (override && override !== "default") {
|
|
27
33
|
paths.push(await resolver.resolvePath(override, { cwd: nuxt.options.rootDir }));
|
|
28
34
|
return;
|
|
29
35
|
}
|
|
30
|
-
const
|
|
31
|
-
|
|
32
|
-
|
|
36
|
+
const schemaPath = await resolver.resolvePath(schema(dialect));
|
|
37
|
+
const names = name === void 0 ? listRegularTableExportNames(schemaPath) : Array.isArray(name) ? name : [name];
|
|
38
|
+
if (names.length === 0) {
|
|
39
|
+
throw new Error(
|
|
40
|
+
`[module-kit] Could not find any table exports in "${schemaPath}" to register. "schema" must be a regular drizzle file, e.g. \`export const users = sqliteTable('users', { ... })\`.`
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
const extensionsByName = /* @__PURE__ */ new Map();
|
|
44
|
+
for (const tableName2 of names) {
|
|
45
|
+
const registered = [];
|
|
46
|
+
await nuxt.callHook("module-kit:db:table:extend", { name: tableName2, dialect, paths: registered });
|
|
47
|
+
extensionsByName.set(tableName2, registered.map((file) => ({ file, legacy: true })));
|
|
48
|
+
}
|
|
33
49
|
const layerRoots = [...new Set(nuxt.options._layers?.map((layer) => layer.config.rootDir).reverse() ?? [])];
|
|
34
50
|
for (const root of [...layerRoots.filter((root2) => root2 !== nuxt.options.rootDir), nuxt.options.rootDir]) {
|
|
35
51
|
const rootResolver = createResolver(root);
|
|
36
52
|
const extensionPath = await rootResolver.resolvePath(extensionFile?.(dialect) ?? `server/db/extend.${dialect}.ts`);
|
|
37
53
|
if (!extensionFile) {
|
|
38
|
-
const
|
|
39
|
-
|
|
54
|
+
for (const tableName2 of names) {
|
|
55
|
+
const legacyPath = await rootResolver.resolvePath(`server/db/${tableName2}.extend.${dialect}`);
|
|
56
|
+
if (existsSync(legacyPath)) extensionsByName.get(tableName2).push({ file: legacyPath, legacy: true });
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
if (existsSync(extensionPath)) {
|
|
60
|
+
for (const tableName2 of names) extensionsByName.get(tableName2).push({ file: extensionPath, legacy: false });
|
|
40
61
|
}
|
|
41
|
-
if (existsSync(extensionPath)) extensions.push({ file: extensionPath, legacy: false });
|
|
42
62
|
}
|
|
43
63
|
if (!factory) {
|
|
44
|
-
const
|
|
45
|
-
const template2 = buildRegularTableTemplate({ name, dialect, schemaPath, extensionFiles: extensions.map(({ file }) => file) });
|
|
64
|
+
const template2 = buildRegularTableTemplate({ names, dialect, schemaPath, extensionsByName });
|
|
46
65
|
paths.push(template2.dst);
|
|
47
66
|
return;
|
|
48
67
|
}
|
|
68
|
+
const tableName = names[0];
|
|
69
|
+
const extensions = extensionsByName.get(tableName);
|
|
49
70
|
const template = addTemplate({
|
|
50
|
-
filename: `${
|
|
71
|
+
filename: `${tableName}/schema.${dialect}.ts`,
|
|
51
72
|
write: true,
|
|
52
73
|
getContents: () => `
|
|
53
74
|
import { ${factory} } from ${JSON.stringify(resolver.resolve(schema(dialect)))}
|
|
54
75
|
${extensions.map(({ file }, index) => `import * as extra${index} from ${JSON.stringify(file)}`).join("\n")}
|
|
55
|
-
export const ${
|
|
76
|
+
export const ${tableName} = ${factory}({ name: ${JSON.stringify(tableName)}, columns: { ${extensions.map(({ legacy }, index) => legacy ? `...(extra${index}[${JSON.stringify(tableName)} as keyof typeof extra${index}] ?? Object.entries(extra${index}).find(([key]) => key === 'default')?.[1] ?? {})` : `...(extra${index}[${JSON.stringify(tableName)} as keyof typeof extra${index}] ?? {})`).join(", ")} } })
|
|
56
77
|
`
|
|
57
78
|
});
|
|
58
79
|
paths.push(template.dst);
|
|
59
80
|
});
|
|
60
81
|
};
|
|
61
82
|
const buildRegularTableTemplate = (options) => {
|
|
62
|
-
const {
|
|
63
|
-
const
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
const importsByModule = new Map([...base.imports].map(([module, specifiers]) => [module, new Set(specifiers)]));
|
|
71
|
-
for (const file of extensionFiles) {
|
|
72
|
-
const parsed = parseRegularTableExport(file, name);
|
|
73
|
-
if (!parsed) continue;
|
|
74
|
-
for (const [key, value] of parsed.properties) {
|
|
75
|
-
properties.delete(key);
|
|
76
|
-
properties.set(key, value);
|
|
83
|
+
const { names, dialect, schemaPath, extensionsByName } = options;
|
|
84
|
+
const importsByModule = /* @__PURE__ */ new Map();
|
|
85
|
+
const tables = names.map((name) => {
|
|
86
|
+
const base = parseRegularTableExport(schemaPath, name);
|
|
87
|
+
if (!base?.tableCall) {
|
|
88
|
+
throw new Error(
|
|
89
|
+
`[module-kit] Could not find a table export named "${name}" in "${schemaPath}". When "factory" is omitted, "schema" must be a regular drizzle file, e.g. \`export const ${name} = sqliteTable(${JSON.stringify(name)}, { ... })\`.`
|
|
90
|
+
);
|
|
77
91
|
}
|
|
78
|
-
|
|
92
|
+
const properties = new Map(base.properties);
|
|
93
|
+
for (const [module, specifiers] of base.imports) {
|
|
79
94
|
const set = importsByModule.get(module) ?? /* @__PURE__ */ new Set();
|
|
80
95
|
for (const specifier of specifiers) set.add(specifier);
|
|
81
96
|
importsByModule.set(module, set);
|
|
82
97
|
}
|
|
83
|
-
|
|
98
|
+
for (const { file } of extensionsByName.get(name) ?? []) {
|
|
99
|
+
const parsed = parseRegularTableExport(file, name);
|
|
100
|
+
if (!parsed) continue;
|
|
101
|
+
for (const [key, value] of parsed.properties) {
|
|
102
|
+
properties.delete(key);
|
|
103
|
+
properties.set(key, value);
|
|
104
|
+
}
|
|
105
|
+
for (const [module, specifiers] of parsed.imports) {
|
|
106
|
+
const set = importsByModule.get(module) ?? /* @__PURE__ */ new Set();
|
|
107
|
+
for (const specifier of specifiers) set.add(specifier);
|
|
108
|
+
importsByModule.set(module, set);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
return { name, tableCall: base.tableCall, properties };
|
|
112
|
+
});
|
|
84
113
|
return addTemplate({
|
|
85
|
-
filename: `${
|
|
114
|
+
filename: `${names.join("+")}/schema.${dialect}.ts`,
|
|
86
115
|
write: true,
|
|
87
116
|
getContents: () => `
|
|
88
117
|
${[...importsByModule].map(([module, specifiers]) => `import { ${[...specifiers].join(", ")} } from ${JSON.stringify(module)}`).join("\n")}
|
|
89
|
-
|
|
118
|
+
${tables.map(({ name, tableCall, properties }) => `
|
|
119
|
+
export const ${name} = ${tableCall.callee}(${JSON.stringify(tableCall.tableName)}, {
|
|
90
120
|
${[...properties.values()].map((property) => ` ${property},`).join("\n")}
|
|
91
|
-
})
|
|
92
|
-
`
|
|
121
|
+
}${tableCall.extraArgs?.length ? `, ${tableCall.extraArgs.join(", ")}` : ""})
|
|
122
|
+
`).join("")}`
|
|
93
123
|
});
|
|
94
124
|
};
|