@frigear-nu/module-kit 0.0.3 → 0.0.5
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 +134 -19
- package/dist/module.json +1 -1
- package/dist/runtime/utils/regular-table.d.ts +31 -0
- package/dist/runtime/utils/regular-table.js +76 -0
- package/dist/runtime/utils/server.d.ts +60 -6
- package/dist/runtime/utils/server.js +88 -13
- package/package.json +3 -2
package/README.md
CHANGED
|
@@ -7,51 +7,166 @@ A simple nuxt module to simplify development of other nuxt modules within frigea
|
|
|
7
7
|
- Be a shared module kit for building nuxt modules within frigear – as generic as possible within the frigear requirements
|
|
8
8
|
- E.g building a 'user' or 'blog' or 'post' module for frigear-nu
|
|
9
9
|
|
|
10
|
-
##
|
|
10
|
+
## Utils & composables
|
|
11
11
|
|
|
12
|
-
|
|
12
|
+
All of the helpers below are exported from `@frigear-nu/module-kit/utils`.
|
|
13
|
+
|
|
14
|
+
### Module helpers
|
|
15
|
+
|
|
16
|
+
#### `registerModule`
|
|
17
|
+
|
|
18
|
+
Registers a module name against `nuxt.options.moduleKit._modules`, de-duplicating entries. Useful during a module's
|
|
19
|
+
`setup()` to keep track of which Frigear modules are active in the current Nuxt app:
|
|
13
20
|
|
|
14
21
|
```ts
|
|
15
|
-
|
|
22
|
+
import { registerModule } from '@frigear-nu/module-kit/utils'
|
|
23
|
+
|
|
24
|
+
registerModule('@frigear-nu/users', nuxt)
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
#### `defineModulePage`
|
|
28
|
+
|
|
29
|
+
Tags a `NuxtPage` with the owning module's name in `page.meta.moduleName`, so pages registered by a module can be
|
|
30
|
+
identified later (e.g. for navigation or permission checks):
|
|
31
|
+
|
|
32
|
+
```ts
|
|
33
|
+
import { defineModulePage } from '@frigear-nu/module-kit/utils'
|
|
34
|
+
|
|
35
|
+
extendPages((pages) => {
|
|
36
|
+
pages.push(defineModulePage('@frigear-nu/users', {
|
|
37
|
+
name: 'users-profile',
|
|
38
|
+
path: '/users/profile',
|
|
39
|
+
file: resolver.resolve('./runtime/pages/profile.vue'),
|
|
40
|
+
}))
|
|
41
|
+
})
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
### URL helpers
|
|
45
|
+
|
|
46
|
+
#### `moduleUrl` and `moduleApiUrl`
|
|
47
|
+
|
|
48
|
+
Build consistent, kebab-cased URLs for a module's pages and API routes. If `path` starts with `/` it is treated as
|
|
49
|
+
absolute and the module name is not prefixed:
|
|
50
|
+
|
|
51
|
+
```ts
|
|
52
|
+
import { moduleUrl, moduleApiUrl } from '@frigear-nu/module-kit/utils'
|
|
53
|
+
|
|
54
|
+
moduleUrl('OAuthServer') // => '/o-auth-server'
|
|
55
|
+
moduleUrl('OAuthServer', 'login') // => '/o-auth-server/login'
|
|
56
|
+
moduleApiUrl('OAuthServer', 'token') // => '/api/o-auth-server/token'
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
### Database schema helpers
|
|
60
|
+
|
|
61
|
+
These helpers let one module own a Drizzle table while letting other modules (and host applications) extend it with
|
|
62
|
+
additional columns, without the owning module needing to know about them.
|
|
63
|
+
|
|
64
|
+
#### `registerDatabaseTable`
|
|
65
|
+
|
|
66
|
+
Modules register a table schema using `registerDatabaseTable`. The simplest usage needs no wrapper at all — `schema`
|
|
67
|
+
can be a *regular* drizzle file:
|
|
68
|
+
|
|
69
|
+
```ts
|
|
70
|
+
// runtime/db/schema.sqlite.ts
|
|
71
|
+
import { sqliteTable, text, integer } from 'drizzle-orm/sqlite-core'
|
|
72
|
+
|
|
73
|
+
export const users = sqliteTable('users', {
|
|
74
|
+
id: integer().primaryKey({ autoIncrement: true }),
|
|
75
|
+
email: text().notNull().unique(),
|
|
76
|
+
})
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
```ts
|
|
80
|
+
import { registerDatabaseTable } from '@frigear-nu/module-kit/utils'
|
|
81
|
+
|
|
82
|
+
registerDatabaseTable(nuxt, {
|
|
16
83
|
name: 'users',
|
|
17
|
-
factory: 'defineUsersTable',
|
|
18
84
|
resolver: createResolver(import.meta.url),
|
|
19
85
|
schema: dialect => `./runtime/db/schema.${dialect}`,
|
|
20
|
-
|
|
86
|
+
})
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
#### `extendDatabaseTable`
|
|
90
|
+
|
|
91
|
+
Other modules can register column files during setup. These can be regular files too — either a full table
|
|
92
|
+
definition or a plain columns object, both without any helper wrapper:
|
|
93
|
+
|
|
94
|
+
```ts
|
|
95
|
+
import { extendDatabaseTable } from '@frigear-nu/module-kit/utils'
|
|
96
|
+
|
|
97
|
+
extendDatabaseTable(nuxt, {
|
|
98
|
+
name: 'users',
|
|
99
|
+
resolver: createResolver(import.meta.url),
|
|
100
|
+
file: dialect => `./runtime/db/extra.${dialect}`,
|
|
21
101
|
})
|
|
22
102
|
```
|
|
23
103
|
|
|
24
104
|
```ts
|
|
25
|
-
|
|
105
|
+
// runtime/db/extra.sqlite.ts
|
|
106
|
+
import { sqliteTable, text } from 'drizzle-orm/sqlite-core'
|
|
26
107
|
|
|
27
|
-
const
|
|
28
|
-
|
|
108
|
+
export const users = sqliteTable('users', {
|
|
109
|
+
locale: text().default('da'),
|
|
110
|
+
})
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
Module-kit parses each file's exported table (or plain object) matching `name`, and merges their columns directly —
|
|
114
|
+
later registrations override columns from earlier ones with the same key — before generating a single `sqliteTable(...)`
|
|
115
|
+
call with the merged columns and deduplicated imports. The base `schema` file must export a genuine table call (e.g.
|
|
116
|
+
`sqliteTable('users', { ... })`) matching `name`, since it determines which factory (`sqliteTable`/`pgTable`/etc.) and
|
|
117
|
+
SQL table name to use for the merged result.
|
|
118
|
+
|
|
119
|
+
#### Using a `factory`
|
|
120
|
+
|
|
121
|
+
Pass `factory` to `registerDatabaseTable` when you need shared defaults (e.g. timestamps) applied consistently across
|
|
122
|
+
dialects. Wrap the factory with `defineExtendableTable` so it receives a single `{ name, columns }` object instead of
|
|
123
|
+
a plain columns object — passing `name` through means the table's name only has to be maintained in one place, the
|
|
124
|
+
`registerDatabaseTable` call, instead of being duplicated as a string literal inside the factory itself. The factory
|
|
125
|
+
must spread `columns` *last*, after its own fields (including timestamps), so extended columns always end up last in
|
|
126
|
+
the generated migration:
|
|
127
|
+
|
|
128
|
+
```ts
|
|
129
|
+
import type { ColumnBuilderBase } from 'drizzle-orm'
|
|
130
|
+
import { defineExtendableTable } from '@frigear-nu/module-kit/utils'
|
|
131
|
+
|
|
132
|
+
const defineUsersTable = defineExtendableTable(({ name, columns }: { name: string, columns: Record<string, ColumnBuilderBase> }) => {
|
|
133
|
+
return sqliteTable(name, {
|
|
29
134
|
id: serial('id').primaryKey(),
|
|
30
135
|
email: text('email').notNull().unique(),
|
|
31
136
|
password: text('password').notNull(),
|
|
32
137
|
createdAt: timestamp('created_at').defaultNow().notNull(),
|
|
33
138
|
updatedAt: timestamp('updated_at').defaultNow().notNull(),
|
|
34
|
-
...
|
|
139
|
+
...columns,
|
|
35
140
|
})
|
|
36
|
-
}
|
|
141
|
+
})
|
|
37
142
|
```
|
|
38
143
|
|
|
39
|
-
|
|
40
|
-
The schema file exports the named factory, which accepts an object of additional columns and returns a table. The factory must spread the received `extra` columns *last* in the object passed to the table builder (after every one of its own fields, including timestamps), so extended columns always end up last in the generated migration, matching the order they were registered in. Other modules can register column files during setup:
|
|
41
|
-
|
|
42
144
|
```ts
|
|
43
|
-
|
|
145
|
+
registerDatabaseTable(nuxt, {
|
|
44
146
|
name: 'users',
|
|
147
|
+
factory: 'defineUsersTable',
|
|
45
148
|
resolver: createResolver(import.meta.url),
|
|
46
|
-
|
|
149
|
+
schema: dialect => `./runtime/db/schema.${dialect}`,
|
|
150
|
+
override: options.settings?.schemas?.users,
|
|
47
151
|
})
|
|
48
152
|
```
|
|
49
153
|
|
|
50
|
-
|
|
154
|
+
#### `defineTableColumns`
|
|
155
|
+
|
|
156
|
+
A typed identity helper for defining a table's extension columns. Each extension file exports column objects named
|
|
157
|
+
for their tables. For example, a Nuxt layer can extend several tables in `server/db/extend.sqlite.ts`:
|
|
51
158
|
|
|
52
159
|
```ts
|
|
53
|
-
|
|
54
|
-
|
|
160
|
+
import { defineTableColumns } from '@frigear-nu/module-kit/utils'
|
|
161
|
+
|
|
162
|
+
export const users = defineTableColumns({ locale: text().default('da') })
|
|
163
|
+
export const posts = defineTableColumns({ summary: text() })
|
|
55
164
|
```
|
|
56
165
|
|
|
57
|
-
Only the export matching the table being built is used. Existing registered files are merged in registration order
|
|
166
|
+
Only the export matching the table being built is used. Existing registered files are merged in registration order
|
|
167
|
+
(later fields replace earlier fields); missing files for a dialect are skipped. Registered files with legacy default
|
|
168
|
+
exports still work. Nuxt layer files are merged next, from base layers toward the host application. Older
|
|
169
|
+
`server/db/<name>.extend.<dialect>` files with default exports are also loaded before each layer's shared file, so a
|
|
170
|
+
shared export can override them. Pass `extensionFile: dialect => 'path/to/columns.' + dialect + '.ts'` to change the
|
|
171
|
+
per-layer file location. If `override` is a path instead of `'default'`, the host schema replaces the generated table
|
|
172
|
+
entirely; the path is resolved from the application's root directory.
|
package/dist/module.json
CHANGED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
export type ParsedTableExport = {
|
|
2
|
+
/** The callee + sql table name, present only when the export is a table factory call, e.g. `sqliteTable('users', {...})`. */
|
|
3
|
+
tableCall?: {
|
|
4
|
+
callee: string;
|
|
5
|
+
tableName: string;
|
|
6
|
+
extraArgs?: string[];
|
|
7
|
+
};
|
|
8
|
+
/** Column (or spread) entries keyed by property name, in source order. Values are the raw `key: value` source text. */
|
|
9
|
+
properties: Map<string, string>;
|
|
10
|
+
/** Named imports collected from the file, keyed by module specifier. */
|
|
11
|
+
imports: Map<string, Set<string>>;
|
|
12
|
+
};
|
|
13
|
+
/**
|
|
14
|
+
* Parses a "regular" drizzle schema file - i.e. a file that exports a table (or a plain columns object) without
|
|
15
|
+
* requiring any `defineExtendableTable`/`defineTableColumns` wrapper - and extracts the column properties and
|
|
16
|
+
* imports belonging to the export matching `exportName`. Supports:
|
|
17
|
+
*
|
|
18
|
+
* - `export const users = sqliteTable('users', { ... })` (or `pgTable`/`mysqlTable`, any 2+ arg factory call)
|
|
19
|
+
* - `export const users = { ... }` (a plain columns object)
|
|
20
|
+
* - `export const users = defineTableColumns({ ... })` (a single-argument wrapper call)
|
|
21
|
+
* - `export default { ... }` / `export default sqliteTable(...)` / `export default defineTableColumns({ ... })`
|
|
22
|
+
* when `exportName` is `'default'`
|
|
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[];
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
import ts from "typescript";
|
|
3
|
+
const parseSource = (filePath) => {
|
|
4
|
+
const text = readFileSync(filePath, "utf8");
|
|
5
|
+
return ts.createSourceFile(filePath, text, ts.ScriptTarget.Latest, true, ts.ScriptKind.TS);
|
|
6
|
+
};
|
|
7
|
+
const collectImports = (source) => {
|
|
8
|
+
const imports = /* @__PURE__ */ new Map();
|
|
9
|
+
for (const statement of source.statements) {
|
|
10
|
+
if (ts.isImportDeclaration(statement) && statement.importClause?.namedBindings && ts.isNamedImports(statement.importClause.namedBindings) && ts.isStringLiteralLike(statement.moduleSpecifier)) {
|
|
11
|
+
const specifiers = imports.get(statement.moduleSpecifier.text) ?? /* @__PURE__ */ new Set();
|
|
12
|
+
for (const element of statement.importClause.namedBindings.elements) specifiers.add(element.getText());
|
|
13
|
+
imports.set(statement.moduleSpecifier.text, specifiers);
|
|
14
|
+
}
|
|
15
|
+
}
|
|
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];
|
|
29
|
+
}
|
|
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());
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
return { tableCall, properties, imports };
|
|
43
|
+
};
|
|
44
|
+
export const parseRegularTableExport = (filePath, exportName) => {
|
|
45
|
+
const source = parseSource(filePath);
|
|
46
|
+
const imports = collectImports(source);
|
|
47
|
+
for (const statement of source.statements) {
|
|
48
|
+
if (ts.isVariableStatement(statement) && statement.modifiers?.some((modifier) => modifier.kind === ts.SyntaxKind.ExportKeyword)) {
|
|
49
|
+
for (const declaration of statement.declarationList.declarations) {
|
|
50
|
+
if (ts.isIdentifier(declaration.name) && declaration.name.text === exportName && declaration.initializer) {
|
|
51
|
+
const result = toTableResult(declaration.initializer, imports);
|
|
52
|
+
if (result) return result;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
} else if (exportName === "default" && ts.isExportAssignment(statement) && !statement.isExportEquals) {
|
|
56
|
+
const result = toTableResult(statement.expression, imports);
|
|
57
|
+
if (result) return result;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
return void 0;
|
|
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
|
+
};
|
|
@@ -16,11 +16,50 @@ declare module '@nuxt/schema' {
|
|
|
16
16
|
'module-kit:db:table:extend': TableExtenderFn;
|
|
17
17
|
}
|
|
18
18
|
}
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
19
|
+
/**
|
|
20
|
+
* A typed identity helper for defining a table's extension columns. It exists purely for type
|
|
21
|
+
* inference/clarity when authoring `server/db/extend.<dialect>.ts` files — it returns the columns
|
|
22
|
+
* object unchanged.
|
|
23
|
+
*/
|
|
24
|
+
export declare const defineTableColumns: <T extends Record<string, ColumnBuilderBase>>(columns: T) => T;
|
|
25
|
+
type TableDefinerProps = {
|
|
22
26
|
name: string;
|
|
23
|
-
|
|
27
|
+
columns: Record<string, ColumnBuilderBase>;
|
|
28
|
+
};
|
|
29
|
+
type TableDefinerFn<T> = (props: TableDefinerProps) => T;
|
|
30
|
+
/**
|
|
31
|
+
* Wraps a Drizzle table factory so it receives `{ name, columns }`, letting the factory pass
|
|
32
|
+
* `name` straight through to e.g. `sqliteTable(name, columns)` instead of hard-coding it, so the
|
|
33
|
+
* table name only has to be maintained once, in the `registerDatabaseTable` call.
|
|
34
|
+
*/
|
|
35
|
+
export declare const defineExtendableTable: <T>(table: TableDefinerFn<T>) => (props: TableDefinerProps) => T;
|
|
36
|
+
/**
|
|
37
|
+
* Lower-level primitive behind `registerDatabaseTable`. Registers a callback on the
|
|
38
|
+
* `hub:db:schema:extend` hook, which receives the current `dialect` and the `paths` array of
|
|
39
|
+
* schema files to include in the generated database schema. Most modules should use
|
|
40
|
+
* `registerDatabaseTable` instead, unless they need full control over how their schema file is
|
|
41
|
+
* built.
|
|
42
|
+
*/
|
|
43
|
+
export declare const onDatabaseSchemaExtend: (nuxt: Nuxt, extender: DbExtenderFn) => Promise<() => void>;
|
|
44
|
+
type DatabaseTableOptions = {
|
|
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[];
|
|
55
|
+
/**
|
|
56
|
+
* The exported factory function name to call with merged extension columns (e.g. `defineUsersTable`), as produced
|
|
57
|
+
* by `defineExtendableTable`. When omitted, `schema` (and every registered/layer extension file) is treated as a
|
|
58
|
+
* "regular" drizzle file - e.g. `export const users = sqliteTable('users', { ... })` - and its columns are merged
|
|
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.
|
|
61
|
+
*/
|
|
62
|
+
factory?: string;
|
|
24
63
|
resolver: ReturnType<typeof createResolver>;
|
|
25
64
|
schema: (dialect: string) => string;
|
|
26
65
|
override?: string;
|
|
@@ -31,6 +70,21 @@ type TableExtensionOptions = {
|
|
|
31
70
|
resolver: ReturnType<typeof createResolver>;
|
|
32
71
|
file: (dialect: string) => string;
|
|
33
72
|
};
|
|
34
|
-
|
|
35
|
-
|
|
73
|
+
/**
|
|
74
|
+
* Registers a column file to be merged into a named table whenever its schema is generated (see
|
|
75
|
+
* `registerDatabaseTable`). Fires the `module-kit:db:table:extend` hook internally, which
|
|
76
|
+
* `registerDatabaseTable` listens to.
|
|
77
|
+
*/
|
|
78
|
+
export declare const extendDatabaseTable: (nuxt: Nuxt, options: TableExtensionOptions) => void;
|
|
79
|
+
/**
|
|
80
|
+
* The table-owning module registers its schema with `registerDatabaseTable`. It generates the
|
|
81
|
+
* table by importing the named `factory` from `schema`, merging in any columns registered
|
|
82
|
+
* through `extendDatabaseTable` and any Nuxt-layer extension files, then calling the factory with
|
|
83
|
+
* `{ name, columns }`.
|
|
84
|
+
*
|
|
85
|
+
* When `name` is an array, or omitted entirely, every (named or auto-discovered) table is generated into a single
|
|
86
|
+
* file - this lets tables declared in the same `schema` file reference each other directly (e.g. a foreign key
|
|
87
|
+
* `.references(() => users.id)` from a `userPasskeys` table), since they share the same module scope.
|
|
88
|
+
*/
|
|
89
|
+
export declare const registerDatabaseTable: (nuxt: Nuxt, options: DatabaseTableOptions) => Promise<() => void>;
|
|
36
90
|
export {};
|
|
@@ -1,12 +1,18 @@
|
|
|
1
1
|
import { addTemplate, createResolver } from "@nuxt/kit";
|
|
2
2
|
import { existsSync } from "node:fs";
|
|
3
|
-
|
|
3
|
+
import { listRegularTableExportNames, parseRegularTableExport } from "./regular-table.js";
|
|
4
|
+
export const defineTableColumns = (columns) => {
|
|
4
5
|
return columns;
|
|
5
6
|
};
|
|
6
|
-
export const
|
|
7
|
+
export const defineExtendableTable = (table) => {
|
|
8
|
+
return (props) => {
|
|
9
|
+
return table(props);
|
|
10
|
+
};
|
|
11
|
+
};
|
|
12
|
+
export const onDatabaseSchemaExtend = async (nuxt, extender) => {
|
|
7
13
|
return nuxt.hook("hub:db:schema:extend", extender);
|
|
8
14
|
};
|
|
9
|
-
export const
|
|
15
|
+
export const extendDatabaseTable = (nuxt, options) => {
|
|
10
16
|
nuxt.hook("module-kit:db:table:extend", async ({ name, dialect, paths }) => {
|
|
11
17
|
if (name === options.name) {
|
|
12
18
|
const file = await options.resolver.resolvePath(options.file(dialect));
|
|
@@ -14,35 +20,104 @@ export const registerDatabaseTableExtension = (nuxt, options) => {
|
|
|
14
20
|
}
|
|
15
21
|
});
|
|
16
22
|
};
|
|
17
|
-
export const
|
|
23
|
+
export const registerDatabaseTable = (nuxt, options) => {
|
|
18
24
|
const { name, factory, resolver, schema, override, extensionFile } = options;
|
|
19
|
-
|
|
25
|
+
if (factory && (name === void 0 || Array.isArray(name))) {
|
|
26
|
+
throw new Error(
|
|
27
|
+
'[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.'
|
|
28
|
+
);
|
|
29
|
+
}
|
|
30
|
+
return onDatabaseSchemaExtend(nuxt, async ({ dialect, paths }) => {
|
|
20
31
|
if (override && override !== "default") {
|
|
21
32
|
paths.push(await resolver.resolvePath(override, { cwd: nuxt.options.rootDir }));
|
|
22
33
|
return;
|
|
23
34
|
}
|
|
24
|
-
const
|
|
25
|
-
|
|
26
|
-
|
|
35
|
+
const schemaPath = await resolver.resolvePath(schema(dialect));
|
|
36
|
+
const names = name === void 0 ? listRegularTableExportNames(schemaPath) : Array.isArray(name) ? name : [name];
|
|
37
|
+
if (names.length === 0) {
|
|
38
|
+
throw new Error(
|
|
39
|
+
`[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', { ... })\`.`
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
const extensionsByName = /* @__PURE__ */ new Map();
|
|
43
|
+
for (const tableName2 of names) {
|
|
44
|
+
const registered = [];
|
|
45
|
+
await nuxt.callHook("module-kit:db:table:extend", { name: tableName2, dialect, paths: registered });
|
|
46
|
+
extensionsByName.set(tableName2, registered.map((file) => ({ file, legacy: true })));
|
|
47
|
+
}
|
|
27
48
|
const layerRoots = [...new Set(nuxt.options._layers?.map((layer) => layer.config.rootDir).reverse() ?? [])];
|
|
28
49
|
for (const root of [...layerRoots.filter((root2) => root2 !== nuxt.options.rootDir), nuxt.options.rootDir]) {
|
|
29
50
|
const rootResolver = createResolver(root);
|
|
30
51
|
const extensionPath = await rootResolver.resolvePath(extensionFile?.(dialect) ?? `server/db/extend.${dialect}.ts`);
|
|
31
52
|
if (!extensionFile) {
|
|
32
|
-
const
|
|
33
|
-
|
|
53
|
+
for (const tableName2 of names) {
|
|
54
|
+
const legacyPath = await rootResolver.resolvePath(`server/db/${tableName2}.extend.${dialect}`);
|
|
55
|
+
if (existsSync(legacyPath)) extensionsByName.get(tableName2).push({ file: legacyPath, legacy: true });
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
if (existsSync(extensionPath)) {
|
|
59
|
+
for (const tableName2 of names) extensionsByName.get(tableName2).push({ file: extensionPath, legacy: false });
|
|
34
60
|
}
|
|
35
|
-
if (existsSync(extensionPath)) extensions.push({ file: extensionPath, legacy: false });
|
|
36
61
|
}
|
|
62
|
+
if (!factory) {
|
|
63
|
+
const template2 = buildRegularTableTemplate({ names, dialect, schemaPath, extensionsByName });
|
|
64
|
+
paths.push(template2.dst);
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
const tableName = names[0];
|
|
68
|
+
const extensions = extensionsByName.get(tableName);
|
|
37
69
|
const template = addTemplate({
|
|
38
|
-
filename: `${
|
|
70
|
+
filename: `${tableName}/schema.${dialect}.ts`,
|
|
39
71
|
write: true,
|
|
40
72
|
getContents: () => `
|
|
41
73
|
import { ${factory} } from ${JSON.stringify(resolver.resolve(schema(dialect)))}
|
|
42
74
|
${extensions.map(({ file }, index) => `import * as extra${index} from ${JSON.stringify(file)}`).join("\n")}
|
|
43
|
-
export const ${
|
|
75
|
+
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(", ")} } })
|
|
44
76
|
`
|
|
45
77
|
});
|
|
46
78
|
paths.push(template.dst);
|
|
47
79
|
});
|
|
48
80
|
};
|
|
81
|
+
const buildRegularTableTemplate = (options) => {
|
|
82
|
+
const { names, dialect, schemaPath, extensionsByName } = options;
|
|
83
|
+
const importsByModule = /* @__PURE__ */ new Map();
|
|
84
|
+
const tables = names.map((name) => {
|
|
85
|
+
const base = parseRegularTableExport(schemaPath, name);
|
|
86
|
+
if (!base?.tableCall) {
|
|
87
|
+
throw new Error(
|
|
88
|
+
`[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)}, { ... })\`.`
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
const properties = new Map(base.properties);
|
|
92
|
+
for (const [module, specifiers] of base.imports) {
|
|
93
|
+
const set = importsByModule.get(module) ?? /* @__PURE__ */ new Set();
|
|
94
|
+
for (const specifier of specifiers) set.add(specifier);
|
|
95
|
+
importsByModule.set(module, set);
|
|
96
|
+
}
|
|
97
|
+
for (const { file } of extensionsByName.get(name) ?? []) {
|
|
98
|
+
const parsed = parseRegularTableExport(file, name);
|
|
99
|
+
if (!parsed) continue;
|
|
100
|
+
for (const [key, value] of parsed.properties) {
|
|
101
|
+
properties.delete(key);
|
|
102
|
+
properties.set(key, value);
|
|
103
|
+
}
|
|
104
|
+
for (const [module, specifiers] of parsed.imports) {
|
|
105
|
+
const set = importsByModule.get(module) ?? /* @__PURE__ */ new Set();
|
|
106
|
+
for (const specifier of specifiers) set.add(specifier);
|
|
107
|
+
importsByModule.set(module, set);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
return { name, tableCall: base.tableCall, properties };
|
|
111
|
+
});
|
|
112
|
+
return addTemplate({
|
|
113
|
+
filename: `${names.join("+")}/schema.${dialect}.ts`,
|
|
114
|
+
write: true,
|
|
115
|
+
getContents: () => `
|
|
116
|
+
${[...importsByModule].map(([module, specifiers]) => `import { ${[...specifiers].join(", ")} } from ${JSON.stringify(module)}`).join("\n")}
|
|
117
|
+
${tables.map(({ name, tableCall, properties }) => `
|
|
118
|
+
export const ${name} = ${tableCall.callee}(${JSON.stringify(tableCall.tableName)}, {
|
|
119
|
+
${[...properties.values()].map((property) => ` ${property},`).join("\n")}
|
|
120
|
+
}${tableCall.extraArgs?.length ? `, ${tableCall.extraArgs.join(", ")}` : ""})
|
|
121
|
+
`).join("")}`
|
|
122
|
+
});
|
|
123
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@frigear-nu/module-kit",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.5",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"private": false,
|
|
6
6
|
"description": "Frigear Module Kit",
|
|
@@ -38,8 +38,10 @@
|
|
|
38
38
|
],
|
|
39
39
|
"dependencies": {
|
|
40
40
|
"@nuxt/kit": "^4.5.2",
|
|
41
|
+
"@nuxthub/core": "^0.10.8",
|
|
41
42
|
"defu": "^6.1.7",
|
|
42
43
|
"scule": "^1.3.0",
|
|
44
|
+
"typescript": "^6.0.3",
|
|
43
45
|
"ufo": "^1.6.4",
|
|
44
46
|
"zod": "^4.6.5"
|
|
45
47
|
},
|
|
@@ -53,7 +55,6 @@
|
|
|
53
55
|
"changelogen": "^0.6.2",
|
|
54
56
|
"eslint": "^10.10.0",
|
|
55
57
|
"nuxt": "^4.5.2",
|
|
56
|
-
"typescript": "^6.0.3",
|
|
57
58
|
"vitest": "^5.0.1",
|
|
58
59
|
"vue-tsc": "^3.3.11"
|
|
59
60
|
},
|