@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
@@ -4,7 +4,7 @@
4
4
  "compatibility": {
5
5
  "nuxt": ">=4.0.0"
6
6
  },
7
- "version": "0.0.4",
7
+ "version": "0.0.6",
8
8
  "builder": {
9
9
  "@nuxt/module-builder": "1.0.3",
10
10
  "unbuild": "unknown"
@@ -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
- export const parseRegularTableExport = (filePath, exportName) => {
3
+ const parseSource = (filePath) => {
4
4
  const text = readFileSync(filePath, "utf8");
5
- const source = ts.createSourceFile(filePath, text, ts.ScriptTarget.Latest, true, ts.ScriptKind.TS);
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
- const toResult = (initializer) => {
15
- let objectLiteral;
16
- let tableCall;
17
- if (ts.isCallExpression(initializer)) {
18
- const args = initializer.arguments;
19
- if (args.length >= 2 && ts.isStringLiteralLike(args[0]) && ts.isObjectLiteralExpression(args[1])) {
20
- tableCall = { callee: initializer.expression.getText(), tableName: args[0].text };
21
- objectLiteral = args[1];
22
- } else if (args.length === 1 && ts.isObjectLiteralExpression(args[0])) {
23
- objectLiteral = args[0];
24
- }
25
- } else if (ts.isObjectLiteralExpression(initializer)) {
26
- objectLiteral = initializer;
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
- if (!objectLiteral) return void 0;
29
- const properties = /* @__PURE__ */ new Map();
30
- for (const property of objectLiteral.properties) {
31
- if (ts.isPropertyAssignment(property) || ts.isShorthandPropertyAssignment(property)) {
32
- properties.set(property.name.getText(), property.getText());
33
- } else if (ts.isSpreadAssignment(property)) {
34
- properties.set(`__spread_${properties.size}`, property.getText());
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
- return { tableCall, properties, imports };
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 = toResult(declaration.initializer);
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 = toResult(statement.expression);
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
- name: string;
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
- name: string;
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 (name === options.name) {
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 registered = [];
31
- await nuxt.callHook("module-kit:db:table:extend", { name, dialect, paths: registered });
32
- const extensions = registered.map((file) => ({ file, legacy: true }));
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 legacyPath = await rootResolver.resolvePath(`server/db/${name}.extend.${dialect}`);
39
- if (existsSync(legacyPath)) extensions.push({ file: legacyPath, legacy: true });
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 schemaPath = await resolver.resolvePath(schema(dialect));
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: `${name}/schema.${dialect}.ts`,
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 ${name} = ${factory}({ name: ${JSON.stringify(name)}, columns: { ${extensions.map(({ legacy }, index) => legacy ? `...(extra${index}[${JSON.stringify(name)} as keyof typeof extra${index}] ?? Object.entries(extra${index}).find(([key]) => key === 'default')?.[1] ?? {})` : `...(extra${index}[${JSON.stringify(name)} as keyof typeof extra${index}] ?? {})`).join(", ")} } })
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 { name, dialect, schemaPath, extensionFiles } = options;
63
- const base = parseRegularTableExport(schemaPath, name);
64
- if (!base?.tableCall) {
65
- throw new Error(
66
- `[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)}, { ... })\`.`
67
- );
68
- }
69
- const properties = new Map(base.properties);
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
- for (const [module, specifiers] of parsed.imports) {
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: `${name}/schema.${dialect}.ts`,
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
- export const ${name} = ${base.tableCall.callee}(${JSON.stringify(base.tableCall.tableName)}, {
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
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@frigear-nu/module-kit",
3
- "version": "0.0.4",
3
+ "version": "0.0.6",
4
4
  "type": "module",
5
5
  "private": false,
6
6
  "description": "Frigear Module Kit",