@lucashw68/nsdb 1.0.0-rc.2

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.
Files changed (49) hide show
  1. package/CHANGELOG.md +34 -0
  2. package/GET_STARTED.md +709 -0
  3. package/LICENSE +21 -0
  4. package/README.md +159 -0
  5. package/cli/index.js +83 -0
  6. package/helpers/args.js +22 -0
  7. package/helpers/config.js +142 -0
  8. package/helpers/generated.js +48 -0
  9. package/helpers/io.js +39 -0
  10. package/helpers/metadata.js +19 -0
  11. package/helpers/names.js +16 -0
  12. package/helpers/relations.js +101 -0
  13. package/helpers/shell.js +15 -0
  14. package/helpers/tables.js +79 -0
  15. package/helpers/ts.js +37 -0
  16. package/module.ts +151 -0
  17. package/nsdb.config.example.mjs +39 -0
  18. package/nsdb.config.example.ts +42 -0
  19. package/package.json +114 -0
  20. package/runtime/components/Form/NsdbRelationSelect.vue +258 -0
  21. package/runtime/components/NsdbForm.vue +865 -0
  22. package/runtime/components/NsdbList.vue +961 -0
  23. package/runtime/composables/useNsdbProfile.ts +119 -0
  24. package/runtime/composables/useNsdbSchemas.ts +176 -0
  25. package/runtime/composables/useSupabaseApi.ts +177 -0
  26. package/runtime/composables/useSupabaseApiStorage.ts +337 -0
  27. package/runtime/composables/useSupabaseModels.ts +412 -0
  28. package/runtime/query.ts +126 -0
  29. package/runtime/stores/createDbStore.ts +439 -0
  30. package/runtime/stores/createSingletonDbStore.ts +67 -0
  31. package/runtime/utils/dataFreshness.ts +47 -0
  32. package/runtime/utils/storage.ts +41 -0
  33. package/scripts/clear.js +64 -0
  34. package/scripts/generate-composables.js +100 -0
  35. package/scripts/generate-enums.js +106 -0
  36. package/scripts/generate-metadata.js +165 -0
  37. package/scripts/generate-models.js +164 -0
  38. package/scripts/generate-schemas.js +443 -0
  39. package/scripts/generate-stores.js +90 -0
  40. package/scripts/generate-types.js +196 -0
  41. package/scripts/init.js +225 -0
  42. package/templates/model.template.ts +48 -0
  43. package/templates/schema.template.ts +13 -0
  44. package/templates/useNsdbModel.template.ts +9 -0
  45. package/types/config.ts +50 -0
  46. package/types/entities.ts +66 -0
  47. package/types/index.ts +14 -0
  48. package/types/list.ts +78 -0
  49. package/types/model.ts +57 -0
@@ -0,0 +1,100 @@
1
+ #!/usr/bin/env node
2
+ import path from 'path'
3
+ import { parseArgs } from '../helpers/args.js'
4
+ import { getOption, loadNsdbConfig } from '../helpers/config.js'
5
+ import { exists, readText, writeText, ensureDir } from '../helpers/io.js'
6
+ import {
7
+ createTsProject,
8
+ addSourceFile,
9
+ loadDatabaseAlias,
10
+ getPublicTablesType
11
+ } from '../helpers/ts.js'
12
+ import { modelHookName } from '../helpers/names.js'
13
+ import { markGenerated } from '../helpers/generated.js'
14
+ import { selectTableProperties } from '../helpers/tables.js'
15
+
16
+ export async function main() {
17
+ const parsedArguments = parseArgs()
18
+ const currentWorkingDirectory = process.cwd()
19
+ const { config } = await loadNsdbConfig(currentWorkingDirectory, parsedArguments.get('config', ''))
20
+
21
+ const typesFilePath = path.resolve(
22
+ currentWorkingDirectory,
23
+ getOption(parsedArguments, config, 'types', 'paths.types')
24
+ )
25
+
26
+ const outputDirectory = path.resolve(
27
+ currentWorkingDirectory,
28
+ getOption(parsedArguments, config, 'outDir', 'paths.composables')
29
+ )
30
+
31
+ const outputFilePath = path.join(outputDirectory, 'useNsdbModels.ts')
32
+
33
+ const templateFilePath = path.resolve(
34
+ currentWorkingDirectory,
35
+ getOption(parsedArguments, config, 'template', 'templates.useNsdbModel')
36
+ )
37
+
38
+ if (!exists(typesFilePath)) {
39
+ console.error(`❌ Missing types file: ${typesFilePath}`)
40
+ process.exit(1)
41
+ }
42
+
43
+ if (!exists(templateFilePath)) {
44
+ console.error(`❌ Missing template: ${templateFilePath}`)
45
+ process.exit(1)
46
+ }
47
+
48
+ ensureDir(outputDirectory)
49
+
50
+ const project = createTsProject()
51
+ const sourceFile = addSourceFile(project, typesFilePath)
52
+ const databaseAlias = loadDatabaseAlias(sourceFile)
53
+
54
+ if (!databaseAlias) {
55
+ console.error('❌ Type alias "Database" not found')
56
+ process.exit(1)
57
+ }
58
+
59
+ const tablesType = getPublicTablesType(databaseAlias)
60
+ if (!tablesType) {
61
+ console.error('❌ Database["public"]["Tables"] not found')
62
+ process.exit(1)
63
+ }
64
+
65
+ const templateContent = readText(templateFilePath)
66
+
67
+ const importLines = []
68
+ const caseLines = []
69
+
70
+ const tableProperties = selectTableProperties(tablesType, config.tables)
71
+ for (const tableProperty of tableProperties) {
72
+ const tableName = tableProperty.getName()
73
+ const hookName = modelHookName(tableName) // ex: playlists -> usePlaylists
74
+
75
+ importLines.push(
76
+ `import { ${hookName} } from '~~/nsdb/models/${tableName}'`
77
+ )
78
+
79
+ caseLines.push(
80
+ `\t\tcase '${tableName}':\n\t\t\treturn ${hookName}(opts)`
81
+ )
82
+ }
83
+
84
+ const finalContent =
85
+ templateContent
86
+ .replace('// __IMPORTS__', importLines.join('\n'))
87
+ .replace('// __CASES__', caseLines.join('\n'))
88
+ .trimEnd() + '\n'
89
+
90
+ writeText(outputFilePath, markGenerated(finalContent))
91
+ console.log('✅ useNsdbModels:', path.relative(currentWorkingDirectory, outputFilePath))
92
+ }
93
+
94
+ if (import.meta.url === `file://${process.argv[1]}`) {
95
+ main().catch((error) => {
96
+ console.error('❌ Unexpected error while generating composables.')
97
+ console.error(error)
98
+ process.exit(1)
99
+ })
100
+ }
@@ -0,0 +1,106 @@
1
+ #!/usr/bin/env node
2
+ import path from 'path'
3
+ import { exists, ensureDir, writeText } from '../helpers/io.js'
4
+ import { parseArgs } from '../helpers/args.js'
5
+ import { getOption, loadNsdbConfig } from '../helpers/config.js'
6
+ import {
7
+ createTsProject,
8
+ addSourceFile,
9
+ loadDatabaseAlias,
10
+ getPublicEnumsType
11
+ } from '../helpers/ts.js'
12
+ import { toPascal } from '../helpers/names.js'
13
+ import { GENERATED_FILE_MARKER } from '../helpers/generated.js'
14
+
15
+ /**
16
+ * Build the metadata needed to emit strongly typed enums derived from Database["public"]["Enums"].
17
+ */
18
+ function extractEnumDescriptors(sourceFile, enumsType) {
19
+ const enumProperties = enumsType.getProperties()
20
+ return enumProperties.map((enumProperty) => {
21
+ const enumName = enumProperty.getName()
22
+ const pascalName = toPascal(enumName)
23
+ const enumType = enumProperty.getTypeAtLocation(sourceFile)
24
+ const unionMembers = enumType.isUnion() ? enumType.getUnionTypes() : []
25
+ const literalValues = unionMembers
26
+ .map((unionMember) => (
27
+ typeof unionMember.getLiteralValue === 'function'
28
+ ? unionMember.getLiteralValue()
29
+ : undefined
30
+ ))
31
+ .filter((literalValue) => typeof literalValue === 'string')
32
+
33
+ return { enumName, pascalName, literalValues }
34
+ })
35
+ }
36
+
37
+ function buildContent(databaseImportPath, descriptors) {
38
+ const headerLines = [
39
+ GENERATED_FILE_MARKER,
40
+ `// Source: Database["public"]["Enums"]`,
41
+ ``,
42
+ `import type { Database } from '${databaseImportPath}'`,
43
+ ``,
44
+ ]
45
+
46
+ const enumBlocks = descriptors.flatMap((descriptor) => ([
47
+ `// Enum: ${descriptor.enumName}`,
48
+ `export type ${descriptor.pascalName} = Database['public']['Enums']['${descriptor.enumName}']`,
49
+ descriptor.literalValues.length
50
+ ? `export const ${descriptor.pascalName}Values = ${JSON.stringify(descriptor.literalValues)} as const`
51
+ : `export const ${descriptor.pascalName}Values = [] as const`,
52
+ ``,
53
+ ]))
54
+
55
+ const enumMapLines = [
56
+ `export const Enums = {`,
57
+ ...descriptors.map((descriptor) => `\t'${descriptor.enumName}': ${descriptor.pascalName}Values`),
58
+ `} as const`,
59
+ ``,
60
+ ]
61
+
62
+ return [...headerLines, ...enumBlocks, ...enumMapLines].join('\n')
63
+ }
64
+
65
+ export async function main() {
66
+ const parsedArguments = parseArgs()
67
+ const currentWorkingDirectory = process.cwd()
68
+ const { config } = await loadNsdbConfig(currentWorkingDirectory, parsedArguments.get('config', ''))
69
+ const typesFilePath = path.resolve(currentWorkingDirectory, getOption(parsedArguments, config, 'types', 'paths.types'))
70
+ const outputFilePath = path.resolve(currentWorkingDirectory, getOption(parsedArguments, config, 'out', 'paths.enums'))
71
+ const databaseImportPath = getOption(parsedArguments, config, 'db-import-path', 'imports.databaseTypes')
72
+
73
+ if (!exists(typesFilePath)) {
74
+ console.error(`❌ Missing types file: ${typesFilePath}`)
75
+ process.exit(1)
76
+ }
77
+
78
+ const project = createTsProject()
79
+ const sourceFile = addSourceFile(project, typesFilePath)
80
+ const databaseAlias = loadDatabaseAlias(sourceFile)
81
+ if (!databaseAlias) {
82
+ console.error(`❌ Type alias "Database" not found in ${typesFilePath}`)
83
+ process.exit(1)
84
+ }
85
+ const enumsType = getPublicEnumsType(databaseAlias)
86
+ if (!enumsType) {
87
+ console.warn('⚠️ Database["public"]["Enums"] not found — writing an empty file.')
88
+ ensureDir(path.dirname(outputFilePath))
89
+ writeText(outputFilePath, `${GENERATED_FILE_MARKER}\nexport {}\n`)
90
+ console.log(`✅ enums: ${path.relative(currentWorkingDirectory, outputFilePath)}`)
91
+ return
92
+ }
93
+
94
+ const descriptors = extractEnumDescriptors(sourceFile, enumsType)
95
+ const fileContent = buildContent(databaseImportPath, descriptors)
96
+ writeText(outputFilePath, fileContent)
97
+ console.log(`✅ enums: ${path.relative(currentWorkingDirectory, outputFilePath)} — ${descriptors.length} enum(s)`)
98
+ }
99
+
100
+ if (import.meta.url === `file://${process.argv[1]}`) {
101
+ main().catch((error) => {
102
+ console.error('❌ Unexpected error while generating enums.')
103
+ console.error(error)
104
+ process.exit(1)
105
+ })
106
+ }
@@ -0,0 +1,165 @@
1
+ #!/usr/bin/env node
2
+ import path from 'node:path'
3
+ import postgres from 'postgres'
4
+ import { parseArgs } from '../helpers/args.js'
5
+ import { getOption, loadNsdbConfig } from '../helpers/config.js'
6
+ import { writeText } from '../helpers/io.js'
7
+
8
+ function sameColumns(left, right) {
9
+ return left.length === right.length && left.every((column, index) => column === right[index])
10
+ }
11
+
12
+ export async function introspectDatabase({ dbUrl, schemaName = 'public' }) {
13
+ const sql = postgres(dbUrl, { max: 1, prepare: false })
14
+ try {
15
+ const columns = await sql`
16
+ select
17
+ cls.relname as table_name,
18
+ att.attname as column_name,
19
+ att.attnum as ordinal_position,
20
+ pg_catalog.format_type(att.atttypid, att.atttypmod) as data_type,
21
+ not att.attnotnull as nullable,
22
+ pg_get_expr(def.adbin, def.adrelid) as default_expression,
23
+ att.attidentity as identity_kind,
24
+ att.attgenerated as generated_kind,
25
+ typ.typtype = 'e' as is_enum,
26
+ typ.typname as database_type
27
+ from pg_catalog.pg_attribute att
28
+ join pg_catalog.pg_class cls on cls.oid = att.attrelid
29
+ join pg_catalog.pg_namespace ns on ns.oid = cls.relnamespace
30
+ join pg_catalog.pg_type typ on typ.oid = att.atttypid
31
+ left join pg_catalog.pg_attrdef def
32
+ on def.adrelid = att.attrelid and def.adnum = att.attnum
33
+ where ns.nspname = ${schemaName}
34
+ and cls.relkind in ('r', 'p')
35
+ and att.attnum > 0
36
+ and not att.attisdropped
37
+ order by cls.relname, att.attnum
38
+ `
39
+
40
+ const constraints = await sql`
41
+ select
42
+ con.conname as constraint_name,
43
+ con.contype as constraint_type,
44
+ local_table.relname as table_name,
45
+ foreign_table.relname as referenced_table,
46
+ array(
47
+ select local_att.attname
48
+ from unnest(con.conkey) with ordinality as key(attnum, position)
49
+ join pg_catalog.pg_attribute local_att
50
+ on local_att.attrelid = con.conrelid and local_att.attnum = key.attnum
51
+ order by key.position
52
+ ) as columns,
53
+ case when con.confkey is null then array[]::text[] else array(
54
+ select foreign_att.attname
55
+ from unnest(con.confkey) with ordinality as key(attnum, position)
56
+ join pg_catalog.pg_attribute foreign_att
57
+ on foreign_att.attrelid = con.confrelid and foreign_att.attnum = key.attnum
58
+ order by key.position
59
+ ) end as referenced_columns
60
+ from pg_catalog.pg_constraint con
61
+ join pg_catalog.pg_class local_table on local_table.oid = con.conrelid
62
+ join pg_catalog.pg_namespace ns on ns.oid = local_table.relnamespace
63
+ left join pg_catalog.pg_class foreign_table on foreign_table.oid = con.confrelid
64
+ where ns.nspname = ${schemaName} and con.contype in ('p', 'u', 'f')
65
+ order by local_table.relname, con.conname
66
+ `
67
+
68
+ const tables = {}
69
+ for (const column of columns) {
70
+ const table = tables[column.table_name] ??= {
71
+ primaryKey: [],
72
+ uniqueConstraints: [],
73
+ columns: {},
74
+ relationships: [],
75
+ }
76
+ const identity = column.identity_kind === 'a'
77
+ ? 'always'
78
+ : column.identity_kind === 'd' ? 'byDefault' : false
79
+ const generated = column.generated_kind === 's'
80
+ ? 'stored'
81
+ : column.generated_kind === 'v' ? 'virtual' : false
82
+ table.columns[column.column_name] = {
83
+ position: Number(column.ordinal_position),
84
+ dataType: column.data_type,
85
+ databaseType: column.database_type,
86
+ nullable: Boolean(column.nullable),
87
+ hasDefault: column.default_expression != null || Boolean(identity),
88
+ defaultExpression: column.default_expression,
89
+ identity,
90
+ generated,
91
+ enum: Boolean(column.is_enum),
92
+ primaryKey: false,
93
+ unique: false,
94
+ insertable: !generated && identity !== 'always',
95
+ updatable: !generated && identity !== 'always',
96
+ }
97
+ }
98
+
99
+ for (const constraint of constraints) {
100
+ const table = tables[constraint.table_name]
101
+ if (!table) continue
102
+ const constraintColumns = [...constraint.columns]
103
+ if (constraint.constraint_type === 'p') {
104
+ table.primaryKey = constraintColumns
105
+ for (const column of constraintColumns) {
106
+ table.columns[column].primaryKey = true
107
+ table.columns[column].updatable = false
108
+ }
109
+ }
110
+ if (constraint.constraint_type === 'p' || constraint.constraint_type === 'u') {
111
+ table.uniqueConstraints.push({ name: constraint.constraint_name, columns: constraintColumns })
112
+ if (constraintColumns.length === 1) table.columns[constraintColumns[0]].unique = true
113
+ }
114
+ }
115
+
116
+ for (const constraint of constraints.filter(item => item.constraint_type === 'f')) {
117
+ const table = tables[constraint.table_name]
118
+ if (!table) continue
119
+ const relationColumns = [...constraint.columns]
120
+ table.relationships.push({
121
+ foreignKeyName: constraint.constraint_name,
122
+ columns: relationColumns,
123
+ referencedRelation: constraint.referenced_table,
124
+ referencedColumns: [...constraint.referenced_columns],
125
+ isOneToOne: table.uniqueConstraints.some(unique => sameColumns(unique.columns, relationColumns)),
126
+ })
127
+ }
128
+
129
+ return { version: 1, schema: schemaName, tables }
130
+ } finally {
131
+ await sql.end()
132
+ }
133
+ }
134
+
135
+ export async function main() {
136
+ const parsedArguments = parseArgs()
137
+ const currentWorkingDirectory = process.cwd()
138
+ const { config } = await loadNsdbConfig(currentWorkingDirectory, parsedArguments.get('config', ''))
139
+ const dbUrl = getOption(parsedArguments, config, 'db-url', 'supabase.dbUrl', process.env.SUPABASE_DB_URL || '')
140
+ const schemaName = getOption(parsedArguments, config, 'schema', 'supabase.schema', 'public')
141
+ const outputPath = path.resolve(
142
+ currentWorkingDirectory,
143
+ getOption(parsedArguments, config, 'out', 'paths.metadata', 'nsdb/database.metadata.json'),
144
+ )
145
+
146
+ if (!dbUrl) {
147
+ console.warn(
148
+ '⚠️ Metadata introspection skipped: configure supabase.dbUrl or SUPABASE_DB_URL. ' +
149
+ 'Generation will fall back to Supabase TypeScript types and cannot reliably infer SQL defaults, identity/generated columns, composite constraints, or inverse relations.'
150
+ )
151
+ return
152
+ }
153
+
154
+ const metadata = await introspectDatabase({ dbUrl, schemaName })
155
+ writeText(outputPath, `${JSON.stringify(metadata, null, 2)}\n`)
156
+ console.log(`✅ metadata: ${path.relative(currentWorkingDirectory, outputPath)}`)
157
+ }
158
+
159
+ if (import.meta.url === `file://${process.argv[1]}`) {
160
+ main().catch((error) => {
161
+ console.error('❌ Failed to introspect PostgreSQL metadata.')
162
+ console.error(error)
163
+ process.exit(1)
164
+ })
165
+ }
@@ -0,0 +1,164 @@
1
+ #!/usr/bin/env node
2
+ import path from 'path'
3
+ import { parseArgs } from '../helpers/args.js'
4
+ import { getOption, loadNsdbConfig } from '../helpers/config.js'
5
+ import { exists, readText, writeText, ensureDir } from '../helpers/io.js'
6
+ import {
7
+ createTsProject,
8
+ addSourceFile,
9
+ loadDatabaseAlias,
10
+ getPublicTablesType
11
+ } from '../helpers/ts.js'
12
+ import { toPascal, modelHookName, storeName, schemaName } from '../helpers/names.js'
13
+ import { markGenerated, removeStaleGeneratedFiles } from '../helpers/generated.js'
14
+ import { selectTableProperties } from '../helpers/tables.js'
15
+ import { getColumnPolicies } from '../helpers/tables.js'
16
+ import { getTableMetadata, loadDatabaseMetadata } from '../helpers/metadata.js'
17
+ import { buildRelationCatalog } from '../helpers/relations.js'
18
+
19
+ function asKeyUnion(keys) {
20
+ return keys.length ? keys.map(key => `'${key}'`).join(' | ') : 'never'
21
+ }
22
+
23
+ function relationRowsType(relations, config, tableColumnsByName) {
24
+ if (!relations.length) return '{}'
25
+ const lines = relations.map(relation => {
26
+ const targetColumns = tableColumnsByName.get(relation.referencedTable) ?? []
27
+ const targetPolicies = getColumnPolicies(config.tables, relation.referencedTable, targetColumns)
28
+ const omitted = targetColumns.filter(column => targetPolicies[column].serverOnly || !targetPolicies[column].selectable)
29
+ const rowType = `Omit<Tables<'${relation.referencedTable}'>, ${asKeyUnion(omitted)}>`
30
+ const valueType = relation.kind === 'hasMany' || relation.kind === 'manyToMany'
31
+ ? `${rowType}[]`
32
+ : `${rowType}${relation.nullable ? ' | null' : ''}`
33
+ return `\t'${relation.alias}': ${valueType}`
34
+ })
35
+ return `{\n${lines.join('\n')}\n}`
36
+ }
37
+
38
+ function buildModelCode(tableProperty, templateContent, currentWorkingDirectory, config, databaseMetadata, locationNode, tableRelations, tableColumnsByName) {
39
+ const tableName = tableProperty.getName()
40
+ const pascalName = toPascal(tableName)
41
+ const rowTypeName = `${pascalName}Row`
42
+ const hookName = modelHookName(tableName)
43
+ const storeIdentifier = storeName(tableName)
44
+ const storeFileRelativePath = `stores/${storeIdentifier}.ts`
45
+ const storeAbsolutePath = path.resolve(currentWorkingDirectory, storeFileRelativePath)
46
+ const storeExists = exists(storeAbsolutePath)
47
+ const tableType = tableProperty.getTypeAtLocation(locationNode)
48
+ const rowType = tableType.getProperty('Row')?.getTypeAtLocation(locationNode)
49
+ const columnNames = rowType?.getProperties().map(property => property.getName()) ?? []
50
+ const policies = getColumnPolicies(config.tables, tableName, columnNames)
51
+ const tableMetadata = getTableMetadata(databaseMetadata, tableName)
52
+ const primaryKeyColumns = tableMetadata?.primaryKey ?? (columnNames.includes('id') ? ['id'] : [])
53
+ if (primaryKeyColumns.length !== 1) {
54
+ throw new Error(`[nsdb] ${tableName} requires exactly one primary key for generated CRUD; found ${primaryKeyColumns.length}.`)
55
+ }
56
+ const primaryKey = primaryKeyColumns[0]
57
+ const conventionalReadonly = ['id', 'created_at', 'updated_at', 'inserted_at']
58
+ const rowOmit = columnNames.filter(column => policies[column].serverOnly || !policies[column].selectable)
59
+ const insertOmit = columnNames.filter(column => {
60
+ if (policies[column].serverOnly || policies[column].editable === false) return true
61
+ if (tableMetadata) return !tableMetadata.columns[column]?.insertable
62
+ return conventionalReadonly.includes(column)
63
+ })
64
+ const updateOmit = columnNames.filter(column => {
65
+ if (policies[column].serverOnly || policies[column].editable === false) return true
66
+ if (tableMetadata) return !tableMetadata.columns[column]?.updatable
67
+ return conventionalReadonly.includes(column)
68
+ })
69
+
70
+ const code = templateContent
71
+ .replace(/__TABLE__/g, tableName)
72
+ .replace(/__PASCAL__/g, pascalName)
73
+ .replace(/__ROW__/g, rowTypeName)
74
+ .replace(/__HOOK__/g, hookName)
75
+ .replace(/__PRIMARY_KEY__/g, primaryKey)
76
+ .replace(/__ROW_OMIT__/g, asKeyUnion(rowOmit))
77
+ .replace(/__INSERT_OMIT__/g, asKeyUnion(insertOmit))
78
+ .replace(/__UPDATE_OMIT__/g, asKeyUnion(updateOmit))
79
+ .replace(/__RELATION_ROWS__/g, relationRowsType(tableRelations, config, tableColumnsByName))
80
+ .replace(/__STORE_IMPORT__/g, storeExists ? `import { ${storeIdentifier} } from '~~/stores/${storeIdentifier}'` : '')
81
+ .replace(/__STORE_CREATOR__/g, storeExists ? `(() => ${storeIdentifier}() as any)` : 'undefined')
82
+
83
+ return { code, hookName }
84
+ }
85
+
86
+ export async function main() {
87
+ const parsedArguments = parseArgs()
88
+ const currentWorkingDirectory = process.cwd()
89
+ const { config } = await loadNsdbConfig(currentWorkingDirectory, parsedArguments.get('config', ''))
90
+ const typesFilePath = path.resolve(currentWorkingDirectory, getOption(parsedArguments, config, 'types', 'paths.types'))
91
+ const outputDirectory = path.resolve(currentWorkingDirectory, getOption(parsedArguments, config, 'outDir', 'paths.models'))
92
+ const barrelFilePath = path.join(outputDirectory, 'index.ts')
93
+ const templateFilePath = path.resolve(
94
+ currentWorkingDirectory,
95
+ getOption(parsedArguments, config, 'template', 'templates.model')
96
+ )
97
+
98
+ if (!exists(typesFilePath)) {
99
+ console.error(`❌ Missing types file: ${typesFilePath}`)
100
+ process.exit(1)
101
+ }
102
+ if (!exists(templateFilePath)) {
103
+ console.error(`❌ Missing template: ${templateFilePath}`)
104
+ process.exit(1)
105
+ }
106
+ ensureDir(outputDirectory)
107
+
108
+ const project = createTsProject()
109
+ const sourceFile = addSourceFile(project, typesFilePath)
110
+ const databaseAlias = loadDatabaseAlias(sourceFile)
111
+ if (!databaseAlias) {
112
+ console.error('❌ Type alias "Database" not found')
113
+ process.exit(1)
114
+ }
115
+ const tablesType = getPublicTablesType(databaseAlias)
116
+ if (!tablesType) {
117
+ console.error('❌ Database["public"]["Tables"] not found')
118
+ process.exit(1)
119
+ }
120
+
121
+ const templateContent = readText(templateFilePath)
122
+ const databaseMetadata = loadDatabaseMetadata(currentWorkingDirectory, config)
123
+ const exportStatements = []
124
+ const generatedFileNames = []
125
+
126
+ const tableProperties = selectTableProperties(tablesType, config.tables)
127
+ const exposedTableNames = new Set(tableProperties.map(property => property.getName()))
128
+ const relationCatalog = buildRelationCatalog(databaseMetadata, exposedTableNames)
129
+ const tableColumnsByName = new Map(tableProperties.map(property => {
130
+ const tableType = property.getTypeAtLocation(databaseAlias)
131
+ const rowType = tableType.getProperty('Row')?.getTypeAtLocation(databaseAlias)
132
+ return [property.getName(), rowType?.getProperties().map(column => column.getName()) ?? []]
133
+ }))
134
+ for (const tableProperty of tableProperties) {
135
+ const tableName = tableProperty.getName()
136
+ const { code, hookName } = buildModelCode(
137
+ tableProperty,
138
+ templateContent,
139
+ currentWorkingDirectory,
140
+ config,
141
+ databaseMetadata,
142
+ databaseAlias,
143
+ relationCatalog[tableName] ?? [],
144
+ tableColumnsByName,
145
+ )
146
+ const modelFilePath = path.join(outputDirectory, `${tableName}.ts`)
147
+ writeText(modelFilePath, markGenerated(code))
148
+ console.log('✅ model:', path.relative(currentWorkingDirectory, modelFilePath))
149
+ exportStatements.push(`export * from './${tableName}' // ${hookName}`)
150
+ generatedFileNames.push(`${tableName}.ts`)
151
+ }
152
+
153
+ writeText(barrelFilePath, markGenerated(exportStatements.join('\n') + '\n'))
154
+ removeStaleGeneratedFiles(outputDirectory, ['index.ts', ...generatedFileNames])
155
+ console.log('✅ models barrel:', path.relative(currentWorkingDirectory, barrelFilePath))
156
+ }
157
+
158
+ if (import.meta.url === `file://${process.argv[1]}`) {
159
+ main().catch((error) => {
160
+ console.error('❌ Unexpected error while generating models.')
161
+ console.error(error)
162
+ process.exit(1)
163
+ })
164
+ }