@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,443 @@
1
+ #!/usr/bin/env node
2
+ import path from 'path'
3
+ import { exists, readText, writeText, ensureDir } 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
+ getPublicTablesType
11
+ } from '../helpers/ts.js'
12
+ import { toPascal } from '../helpers/names.js'
13
+ import { markGenerated, removeStaleGeneratedFiles } from '../helpers/generated.js'
14
+ import { getColumnPolicies, selectTableProperties } from '../helpers/tables.js'
15
+ import { getTableMetadata, loadDatabaseMetadata } from '../helpers/metadata.js'
16
+ import { buildRelationCatalog } from '../helpers/relations.js'
17
+
18
+ /**
19
+ * Ancien helper (conservé au cas où tu en aurais besoin plus tard).
20
+ * Il n'est plus utilisé pour générer le schema UI, mais je le laisse
21
+ * pour ne pas "perdre" la fonctionnalité.
22
+ */
23
+ function guessFieldKindFromTypeText(typeText) {
24
+ const normalizedText = String(typeText || '').toLowerCase()
25
+
26
+ if (normalizedText.includes('uuid')) return 'uuid'
27
+ if (normalizedText.includes('timestamp') || normalizedText.includes('date')) return 'timestamp'
28
+ if (normalizedText.includes('bool')) return 'boolean'
29
+ if (
30
+ normalizedText.includes('int') ||
31
+ normalizedText.includes('number') ||
32
+ normalizedText.includes('float') ||
33
+ normalizedText.includes('numeric')
34
+ ) {
35
+ return 'number'
36
+ }
37
+ if (normalizedText.includes('json')) return 'json'
38
+ if (
39
+ normalizedText.includes('string') ||
40
+ normalizedText.includes('text') ||
41
+ normalizedText.includes('char') ||
42
+ normalizedText.includes('varchar')
43
+ ) {
44
+ return 'string'
45
+ }
46
+ return 'unknown'
47
+ }
48
+
49
+ /**
50
+ * Nouveau helper : mappe le type TS (textuel) vers ton FieldType UI
51
+ * ('text' | 'number' | 'checkbox' | 'datetime' | 'textarea' | ...).
52
+ * Les enums seront forcés plus bas à 'select'.
53
+ */
54
+ function guessFieldTypeForUi(typeText, columnMetadata = null) {
55
+ const databaseType = String(columnMetadata?.dataType ?? '').toLowerCase()
56
+ if (databaseType.endsWith('[]')) return 'array'
57
+ if (databaseType === 'json' || databaseType === 'jsonb') return 'json'
58
+ if (databaseType === 'date') return 'date'
59
+ if (databaseType.includes('timestamp')) return 'datetime'
60
+ if (databaseType.includes('bool')) return 'checkbox'
61
+ if (/^(smallint|integer|bigint|numeric|decimal|real|double precision)/.test(databaseType)) return 'number'
62
+
63
+ const normalized = String(typeText || '').toLowerCase()
64
+
65
+ if (normalized.includes('bool')) return 'checkbox'
66
+ if (
67
+ normalized.includes('int') ||
68
+ normalized.includes('number') ||
69
+ normalized.includes('float') ||
70
+ normalized.includes('numeric')
71
+ ) {
72
+ return 'number'
73
+ }
74
+ if (normalized.includes('timestamp') || normalized.includes('date')) {
75
+ return 'datetime'
76
+ }
77
+ if (normalized.includes('json')) return 'json'
78
+ if (normalized.includes('[]')) return 'array'
79
+ // Pour le reste, on part sur du 'text' par défaut
80
+ return 'text'
81
+ }
82
+
83
+ function extractEnumNameFromTypeText(typeText) {
84
+ const enumMatch = String(typeText || '')
85
+ .replace(/'/g, '"')
86
+ .match(/Database\["public"\]\["Enums"\]\["([^"]+)"\]/)
87
+
88
+ return enumMatch?.[1] ?? null
89
+ }
90
+
91
+ function extractLiteralOptions(typeText) {
92
+ return [...String(typeText || '').matchAll(/["']([^"']+)["']/g)]
93
+ .map(match => match[1])
94
+ .filter((value, index, values) => values.indexOf(value) === index)
95
+ }
96
+
97
+ function humanizeIdentifier(identifier) {
98
+ const words = String(identifier).replace(/_/g, ' ').trim()
99
+ return words ? `${words[0].toUpperCase()}${words.slice(1)}` : identifier
100
+ }
101
+
102
+ function isFieldRequired(insertType, fieldName) {
103
+ const insertPropertySymbol = insertType.getProperty(fieldName)
104
+ if (!insertPropertySymbol) return false
105
+
106
+ const declaration = insertPropertySymbol.getDeclarations()?.[0]
107
+ const isOptional = declaration?.hasQuestionToken?.() ?? false
108
+ return !isOptional
109
+ }
110
+
111
+ // Récupère le texte brut du type tel qu'il est écrit dans `Insert`
112
+ // (ex: `Database["public"]["Enums"]["PROVIDERS"] | null`)
113
+ function getInsertPropertyTypeNodeText(insertType, fieldName) {
114
+ const insertPropertySymbol = insertType.getProperty(fieldName)
115
+ if (!insertPropertySymbol) return null
116
+
117
+ const declaration = insertPropertySymbol.getDeclarations()?.[0]
118
+ if (!declaration) return null
119
+
120
+ const typeNode = declaration.getTypeNode()
121
+ if (!typeNode) return null
122
+
123
+ return typeNode.getText()
124
+ }
125
+
126
+ /**
127
+ * Lit la propriété `Relationships` d'une table Supabase
128
+ * et renvoie une liste d'objets JS :
129
+ * {
130
+ * foreignKeyName,
131
+ * columns: string[],
132
+ * isOneToOne: boolean,
133
+ * referencedRelation: string,
134
+ * referencedColumns: string[]
135
+ * }
136
+ *
137
+ * ⚠️ IMPORTANT : on lit les TYPES (getTypeAtLocation + getText),
138
+ * pas des initializers (il n'y en a pas dans les déclarations de type).
139
+ */
140
+ function getRelationshipsForTable(tableType, locationNode) {
141
+ const relProperty = tableType.getProperty('Relationships')
142
+ if (!relProperty) return []
143
+
144
+ const relType = relProperty.getTypeAtLocation(locationNode)
145
+ const tupleElements = relType.isTuple() ? relType.getTupleElements() : []
146
+
147
+ return tupleElements.map((elType) => {
148
+ const t = elType.getApparentType()
149
+
150
+ function getLiteralFromType(propName) {
151
+ const prop = t.getProperty(propName)
152
+ if (!prop) return undefined
153
+ const type = prop.getTypeAtLocation(locationNode)
154
+ const text = type.getText() // ex: '"songs_profile_id_fkey"' ou 'false'
155
+ return text.replace(/['"`]/g, '')
156
+ }
157
+
158
+ function getArrayFromType(propName) {
159
+ const prop = t.getProperty(propName)
160
+ if (!prop) return []
161
+ const type = prop.getTypeAtLocation(locationNode)
162
+ const text = type.getText() // ex: '["playlist_id"]' ou '["id"]'
163
+
164
+ const matches = []
165
+ const regex = /"([^"]+)"/g
166
+ let m
167
+ while ((m = regex.exec(text)) !== null) {
168
+ matches.push(m[1])
169
+ }
170
+ return matches
171
+ }
172
+
173
+ const foreignKeyName = getLiteralFromType('foreignKeyName')
174
+ const columns = getArrayFromType('columns')
175
+ const referencedRelation = getLiteralFromType('referencedRelation')
176
+ const referencedColumns = getArrayFromType('referencedColumns')
177
+ const isOneToOneText = getLiteralFromType('isOneToOne')
178
+ const isOneToOne = isOneToOneText === 'true'
179
+
180
+ return {
181
+ foreignKeyName,
182
+ columns,
183
+ isOneToOne,
184
+ referencedRelation,
185
+ referencedColumns,
186
+ }
187
+ })
188
+ }
189
+
190
+ function buildSchemaForTable(tableProperty, locationNode, exposedTableNames, config, databaseMetadata, tableRelations) {
191
+ const tableName = tableProperty.getName()
192
+ const pascalTableName = toPascal(tableName)
193
+ const rowTypeAliasName = `${pascalTableName}Row`
194
+
195
+ const tableType = tableProperty.getTypeAtLocation(locationNode)
196
+ const rowProperty = tableType.getProperty('Row')
197
+ const insertProperty = tableType.getProperty('Insert')
198
+ const updateProperty = tableType.getProperty('Update')
199
+
200
+ if (!rowProperty || !insertProperty || !updateProperty) return null
201
+
202
+ const rowType = rowProperty.getTypeAtLocation(locationNode)
203
+ const insertType = insertProperty.getTypeAtLocation(locationNode)
204
+ const updateType = updateProperty.getTypeAtLocation(locationNode)
205
+
206
+ // 🔥 On lit les relations pour cette table
207
+ const tableMetadata = getTableMetadata(databaseMetadata, tableName)
208
+ const relationships = (tableMetadata?.relationships ?? getRelationshipsForTable(tableType, locationNode))
209
+ .filter(relation => exposedTableNames.has(relation.referencedRelation))
210
+ console.log(`🔍 Table "${tableName}" - found ${relationships.length} relationships.`)
211
+
212
+ // Map: nom de colonne locale -> descriptor de relation
213
+ const relationByColumn = new Map()
214
+ for (const rel of relationships) {
215
+ if (rel.columns.length !== 1) continue
216
+ for (const col of rel.columns) {
217
+ relationByColumn.set(col, rel)
218
+ }
219
+ }
220
+
221
+ const fieldLines = []
222
+ const rowFields = rowType.getProperties()
223
+ const columnPolicies = getColumnPolicies(config.tables, tableName, rowFields.map(field => field.getName()))
224
+
225
+ for (const rowField of rowFields) {
226
+ const fieldName = rowField.getName()
227
+ const columnMetadata = tableMetadata?.columns?.[fieldName] ?? null
228
+ const columnPolicy = columnPolicies[fieldName]
229
+ if (columnPolicy.serverOnly) continue
230
+ const fieldType = rowField.getTypeAtLocation(locationNode)
231
+ const rowFieldTypeText = fieldType.getText()
232
+ const fallbackNullable = /(^|\s)null(\s|$)/.test(rowFieldTypeText.replace(/\|/g, ' '))
233
+
234
+ const insertPropertySymbol = insertType.getProperty(fieldName)
235
+ const insertFieldTypeText = insertPropertySymbol
236
+ ?.getTypeAtLocation(locationNode)
237
+ .getText()
238
+
239
+ const fieldIsRequired = columnMetadata
240
+ ? Boolean(columnMetadata.insertable && !columnMetadata.nullable && !columnMetadata.hasDefault)
241
+ : isFieldRequired(insertType, fieldName)
242
+
243
+ // Texte du type tel qu'il est écrit dans Insert
244
+ // => ex: Database["public"]["Enums"]["PROVIDERS"] | null
245
+ const insertRawTypeReferenceText = getInsertPropertyTypeNodeText(
246
+ insertType,
247
+ fieldName
248
+ )
249
+
250
+ const enumName = extractEnumNameFromTypeText(insertRawTypeReferenceText)
251
+ const literalOptions = enumName ? [] : extractLiteralOptions(insertRawTypeReferenceText)
252
+
253
+ // Type UI par défaut (FieldType)
254
+ let uiFieldType = guessFieldTypeForUi(
255
+ insertFieldTypeText || rowFieldTypeText,
256
+ columnMetadata,
257
+ )
258
+
259
+ // Si on a trouvé un enum sur Insert, on force le type à 'select'
260
+ let optionsAttachment = ''
261
+ if (enumName) {
262
+ uiFieldType = 'select'
263
+ optionsAttachment =
264
+ `, options: Enums.${toPascal(enumName)}Values.map(v => ({ label: String(v), value: v }))`
265
+ }
266
+ else if (literalOptions.length > 1) {
267
+ uiFieldType = 'select'
268
+ optionsAttachment = `, options: ${JSON.stringify(literalOptions)}.map(v => ({ label: String(v), value: v }))`
269
+ }
270
+
271
+ const isPrimaryKey = columnMetadata
272
+ ? Boolean(columnMetadata.primaryKey)
273
+ : fieldName === 'id'
274
+ const policyAllowsEditing = columnPolicy.editable !== false
275
+ const isConventionallyReadonly = isPrimaryKey || ['created_at', 'updated_at', 'inserted_at'].includes(fieldName)
276
+ const isInsertable = policyAllowsEditing && (columnMetadata
277
+ ? Boolean(columnMetadata.insertable)
278
+ : Boolean(insertType.getProperty(fieldName)) && !isConventionallyReadonly)
279
+ const isUpdatable = policyAllowsEditing && (columnMetadata
280
+ ? Boolean(columnMetadata.updatable)
281
+ : Boolean(updateType.getProperty(fieldName)) && !isConventionallyReadonly)
282
+ const isEditable = isInsertable || isUpdatable
283
+ const isReadOnlyField = !isEditable
284
+ const fallbackHasDefault = !columnMetadata && isInsertable && !fieldIsRequired && !fallbackNullable
285
+
286
+ // 🔗 Relation éventuelle pour ce champ
287
+ const relationDescriptor = relationByColumn.get(fieldName)
288
+ let relationAttachment = ''
289
+
290
+ if (relationDescriptor) {
291
+ // kind simple: si isOneToOne => 'hasOne', sinon 'belongsTo'
292
+ const kind = relationDescriptor.isOneToOne ? 'hasOne' : 'belongsTo'
293
+ const catalogRelation = tableRelations.find(
294
+ relation => relation.foreignKeyName === relationDescriptor.foreignKeyName && relation.direction === 'forward',
295
+ )
296
+
297
+ uiFieldType = 'relation' // on force le type UI pour les FK
298
+
299
+ relationAttachment =
300
+ `, relation: {` +
301
+ ` alias: '${catalogRelation?.alias ?? relationDescriptor.referencedRelation}',` +
302
+ ` kind: '${kind}',` +
303
+ ` direction: '${catalogRelation?.direction ?? 'forward'}',` +
304
+ ` referencedTable: '${relationDescriptor.referencedRelation}',` +
305
+ ` embedResource: '${catalogRelation?.embedResource ?? relationDescriptor.referencedRelation}',` +
306
+ ` localColumns: [${relationDescriptor.columns.map((c) => `'${c}'`).join(', ')}],` +
307
+ ` referencedColumns: [${relationDescriptor.referencedColumns
308
+ .map((c) => `'${c}'`)
309
+ .join(', ')}],` +
310
+ ` foreignKeyName: '${relationDescriptor.foreignKeyName}',` +
311
+ ` nullable: ${catalogRelation?.nullable ?? false},` +
312
+ ` composite: ${catalogRelation?.composite ?? false}` +
313
+ ` }`
314
+ }
315
+
316
+ fieldLines.push(
317
+ `\t${fieldName}: {` +
318
+ ` label: '${humanizeIdentifier(fieldName)}',` +
319
+ ` type: '${uiFieldType}',` +
320
+ ` required: ${fieldIsRequired}` +
321
+ `, selectable: ${columnPolicy.selectable}` +
322
+ `, editable: ${isEditable}` +
323
+ `, insertable: ${isInsertable}` +
324
+ `, updatable: ${isUpdatable}` +
325
+ `${columnPolicy.hidden ? ', hidden: true' : ''}` +
326
+ `${isReadOnlyField ? ', readonly: true' : ''}` +
327
+ `${isPrimaryKey ? ', primaryKey: true' : ''}` +
328
+ `${columnMetadata ? `, nullable: ${columnMetadata.nullable}, hasDefault: ${columnMetadata.hasDefault}, databaseType: ${JSON.stringify(columnMetadata.dataType)}, defaultExpression: ${JSON.stringify(columnMetadata.defaultExpression)}` : `, nullable: ${fallbackNullable}, hasDefault: ${fallbackHasDefault}`}` +
329
+ `${optionsAttachment}` +
330
+ `${relationAttachment}` +
331
+ ` },`
332
+ )
333
+ }
334
+
335
+ return { tableName, pascalTableName, rowTypeAliasName, fieldLines, relations: tableRelations }
336
+ }
337
+
338
+ function renderTemplate(templateContent, descriptor) {
339
+ return templateContent
340
+ .replace(/__TABLE__/g, descriptor.tableName)
341
+ .replace(/__PASCAL__/g, descriptor.pascalTableName)
342
+ .replace(/__ROW__/g, descriptor.rowTypeAliasName)
343
+ .replace('__RELATIONS__', JSON.stringify(descriptor.relations, null, 2))
344
+ .replace('// __FIELDS__', descriptor.fieldLines.join('\n'))
345
+ }
346
+
347
+ export async function main() {
348
+ const parsedArguments = parseArgs()
349
+ const currentWorkingDirectory = process.cwd()
350
+ const { config } = await loadNsdbConfig(currentWorkingDirectory, parsedArguments.get('config', ''))
351
+
352
+ const typesFilePath = path.resolve(
353
+ currentWorkingDirectory,
354
+ getOption(parsedArguments, config, 'types', 'paths.types')
355
+ )
356
+
357
+ const outputDirectory = path.resolve(
358
+ currentWorkingDirectory,
359
+ getOption(parsedArguments, config, 'outDir', 'paths.schemas')
360
+ )
361
+
362
+ const barrelFilePath = path.join(outputDirectory, 'index.ts')
363
+
364
+ const templateFilePath = path.resolve(
365
+ currentWorkingDirectory,
366
+ getOption(parsedArguments, config, 'template', 'templates.schema')
367
+ )
368
+
369
+ if (!exists(typesFilePath)) {
370
+ console.error(`❌ Missing types file: ${typesFilePath}`)
371
+ process.exit(1)
372
+ }
373
+
374
+ if (!exists(templateFilePath)) {
375
+ console.error(`❌ Missing template: ${templateFilePath}`)
376
+ process.exit(1)
377
+ }
378
+
379
+ ensureDir(outputDirectory)
380
+
381
+ const project = createTsProject()
382
+ const sourceFile = addSourceFile(project, typesFilePath)
383
+ const databaseAlias = loadDatabaseAlias(sourceFile)
384
+
385
+ if (!databaseAlias) {
386
+ console.error('❌ Type alias "Database" not found')
387
+ process.exit(1)
388
+ }
389
+
390
+ const tablesType = getPublicTablesType(databaseAlias)
391
+
392
+ if (!tablesType) {
393
+ console.error('❌ Database["public"]["Tables"] not found')
394
+ process.exit(1)
395
+ }
396
+
397
+ const templateContent = readText(templateFilePath)
398
+ const databaseMetadata = loadDatabaseMetadata(currentWorkingDirectory, config)
399
+ const exportStatements = []
400
+ const generatedFileNames = []
401
+
402
+ const tableProperties = selectTableProperties(tablesType, config.tables)
403
+ const exposedTableNames = new Set(tableProperties.map(tableProperty => tableProperty.getName()))
404
+ const relationCatalog = buildRelationCatalog(databaseMetadata, exposedTableNames)
405
+ for (const tableProperty of tableProperties) {
406
+ const tableName = tableProperty.getName()
407
+ const schemaDescriptor = buildSchemaForTable(
408
+ tableProperty,
409
+ databaseAlias,
410
+ exposedTableNames,
411
+ config,
412
+ databaseMetadata,
413
+ relationCatalog[tableName] ?? [],
414
+ )
415
+ if (!schemaDescriptor || !schemaDescriptor.fieldLines.length) continue
416
+
417
+ const fileContent = renderTemplate(templateContent, schemaDescriptor)
418
+ const schemaFilePath = path.join(
419
+ outputDirectory,
420
+ `${schemaDescriptor.tableName}.ts`
421
+ )
422
+
423
+ writeText(schemaFilePath, markGenerated(fileContent))
424
+ console.log('✅ schema:', path.relative(currentWorkingDirectory, schemaFilePath))
425
+
426
+ exportStatements.push(
427
+ `export * from './${schemaDescriptor.tableName}' // ${schemaDescriptor.pascalTableName}Schema`
428
+ )
429
+ generatedFileNames.push(`${schemaDescriptor.tableName}.ts`)
430
+ }
431
+
432
+ writeText(barrelFilePath, markGenerated(exportStatements.join('\n') + '\n'))
433
+ removeStaleGeneratedFiles(outputDirectory, ['index.ts', ...generatedFileNames])
434
+ console.log('✅ schemas barrel:', path.relative(currentWorkingDirectory, barrelFilePath))
435
+ }
436
+
437
+ if (import.meta.url === `file://${process.argv[1]}`) {
438
+ main().catch((error) => {
439
+ console.error('❌ Unexpected error while generating schemas.')
440
+ console.error(error)
441
+ process.exit(1)
442
+ })
443
+ }
@@ -0,0 +1,90 @@
1
+ #!/usr/bin/env node
2
+ import path from 'path'
3
+ import { parseArgs } from '../helpers/args.js'
4
+ import { getBoolOption, getOption, loadNsdbConfig } from '../helpers/config.js'
5
+ import { exists, listFiles, writeText, ensureDir } from '../helpers/io.js'
6
+ import { toPascal, storeName } from '../helpers/names.js'
7
+ import { markGenerated, removeStaleGeneratedFiles } from '../helpers/generated.js'
8
+ import { isNsdbGeneratedFile } from '../helpers/generated.js'
9
+ import { getTableMetadata, loadDatabaseMetadata } from '../helpers/metadata.js'
10
+
11
+ function loadTableNames(modelsDirectoryPath) {
12
+ if (!exists(modelsDirectoryPath)) {
13
+ console.error(`❌ Missing models directory: ${modelsDirectoryPath}`)
14
+ process.exit(1)
15
+ }
16
+
17
+ const entries = listFiles(modelsDirectoryPath)
18
+ return entries
19
+ .filter((entry) => entry.endsWith('.ts') && entry !== 'index.ts')
20
+ .filter((entry) => isNsdbGeneratedFile(path.join(modelsDirectoryPath, entry)))
21
+ .map((entry) => entry.replace(/\.ts$/, ''))
22
+ .sort()
23
+ }
24
+
25
+ function renderStoreFile({ tableName, storeIdentifier, rowTypeName, typesImportPath, primaryKey }) {
26
+ return `import { createDbStore } from '@lucashw68/nsdb/createDbStore'
27
+ import type { Tables } from '${typesImportPath}'
28
+
29
+ type ${rowTypeName} = Tables<'${tableName}'>
30
+
31
+ export const ${storeIdentifier} = createDbStore<${rowTypeName}>('${tableName}', {
32
+ key: '${primaryKey}',
33
+ orderBy: '${primaryKey}',
34
+ defaultSort: 'desc',
35
+ })
36
+ `
37
+ }
38
+
39
+ export async function main() {
40
+ const parsedArguments = parseArgs()
41
+ const currentWorkingDirectory = process.cwd()
42
+ const { config } = await loadNsdbConfig(currentWorkingDirectory, parsedArguments.get('config', ''))
43
+ const modelsDirectoryPath = path.resolve(currentWorkingDirectory, getOption(parsedArguments, config, 'models-dir', 'paths.models'))
44
+ const storesDirectoryPath = path.resolve(currentWorkingDirectory, getOption(parsedArguments, config, 'stores-dir', 'paths.stores'))
45
+ const typesImportPath = getOption(parsedArguments, config, 'types-import-path', 'imports.databaseTypes')
46
+ const shouldOverwriteExisting = getBoolOption(parsedArguments, config, 'force', 'generators.force', false)
47
+ const databaseMetadata = loadDatabaseMetadata(currentWorkingDirectory, config)
48
+
49
+ const tableNames = loadTableNames(modelsDirectoryPath)
50
+ if (!tableNames.length) {
51
+ console.warn('⚠️ No model files found. Run generate:models first.')
52
+ return
53
+ }
54
+
55
+ ensureDir(storesDirectoryPath)
56
+
57
+ for (const tableName of tableNames) {
58
+ const pascalName = toPascal(tableName)
59
+ const rowTypeName = `${pascalName}Row`
60
+ const storeIdentifier = storeName(tableName)
61
+ const primaryKeyColumns = getTableMetadata(databaseMetadata, tableName)?.primaryKey ?? ['id']
62
+ if (primaryKeyColumns.length !== 1) {
63
+ throw new Error(`[nsdb] ${tableName} requires exactly one primary key for a generated store.`)
64
+ }
65
+ const primaryKey = primaryKeyColumns[0]
66
+ const storeFilePath = path.join(storesDirectoryPath, `${storeIdentifier}.ts`)
67
+
68
+ if (exists(storeFilePath) && !shouldOverwriteExisting) {
69
+ console.log(`⚠️ ${path.relative(currentWorkingDirectory, storeFilePath)} already exists, skipping.`)
70
+ continue
71
+ }
72
+
73
+ const fileContent = renderStoreFile({ tableName, storeIdentifier, rowTypeName, typesImportPath, primaryKey })
74
+ writeText(storeFilePath, markGenerated(fileContent))
75
+ console.log(`✅ store: ${path.relative(currentWorkingDirectory, storeFilePath)}`)
76
+ }
77
+
78
+ removeStaleGeneratedFiles(
79
+ storesDirectoryPath,
80
+ tableNames.map(tableName => `${storeName(tableName)}.ts`),
81
+ )
82
+ }
83
+
84
+ if (import.meta.url === `file://${process.argv[1]}`) {
85
+ main().catch((error) => {
86
+ console.error('❌ Unexpected error while generating stores.')
87
+ console.error(error)
88
+ process.exit(1)
89
+ })
90
+ }