@stonyx/orm 0.2.1-beta.2 → 0.2.1-beta.21

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.
@@ -0,0 +1,170 @@
1
+ import { fileToDirectory, directoryToFile } from './migrate.js';
2
+
3
+ export default {
4
+ 'db:migrate-to-directory': {
5
+ description: 'Migrate DB from single file to directory mode',
6
+ bootstrap: true,
7
+ run: async () => {
8
+ await fileToDirectory();
9
+ console.log('DB migration to directory mode complete.');
10
+ }
11
+ },
12
+ 'db:migrate-to-file': {
13
+ description: 'Migrate DB from directory mode to single file',
14
+ bootstrap: true,
15
+ run: async () => {
16
+ await directoryToFile();
17
+ console.log('DB migration to file mode complete.');
18
+ }
19
+ },
20
+ 'db:generate-migration': {
21
+ description: 'Generate a MySQL migration from current model schemas',
22
+ bootstrap: true,
23
+ run: async (args) => {
24
+ const description = args.join(' ') || 'migration';
25
+ const { generateMigration } = await import('./mysql/migration-generator.js');
26
+ const result = await generateMigration(description);
27
+
28
+ if (result) {
29
+ console.log(`Migration created: ${result.filename}`);
30
+ } else {
31
+ console.log('No schema changes detected. No migration generated.');
32
+ }
33
+ }
34
+ },
35
+ 'db:migrate': {
36
+ description: 'Apply pending MySQL migrations',
37
+ bootstrap: true,
38
+ run: async () => {
39
+ const config = (await import('stonyx/config')).default;
40
+ const mysqlConfig = config.orm.mysql;
41
+
42
+ if (!mysqlConfig) {
43
+ console.error('MySQL is not configured. Set MYSQL_HOST to enable MySQL mode.');
44
+ process.exit(1);
45
+ }
46
+
47
+ const { getPool, closePool } = await import('./mysql/connection.js');
48
+ const { ensureMigrationsTable, getAppliedMigrations, getMigrationFiles, applyMigration, parseMigrationFile } = await import('./mysql/migration-runner.js');
49
+ const { readFile } = await import('@stonyx/utils/file');
50
+ const path = await import('path');
51
+
52
+ const pool = await getPool(mysqlConfig);
53
+ const migrationsPath = path.resolve(config.rootPath, mysqlConfig.migrationsDir);
54
+
55
+ try {
56
+ await ensureMigrationsTable(pool, mysqlConfig.migrationsTable);
57
+
58
+ const applied = await getAppliedMigrations(pool, mysqlConfig.migrationsTable);
59
+ const files = await getMigrationFiles(migrationsPath);
60
+ const pending = files.filter(f => !applied.includes(f));
61
+
62
+ if (pending.length === 0) {
63
+ console.log('No pending migrations.');
64
+ return;
65
+ }
66
+
67
+ console.log(`Applying ${pending.length} migration(s)...`);
68
+
69
+ for (const filename of pending) {
70
+ const content = await readFile(path.join(migrationsPath, filename));
71
+ const { up } = parseMigrationFile(content);
72
+
73
+ await applyMigration(pool, filename, up, mysqlConfig.migrationsTable);
74
+ console.log(` Applied: ${filename}`);
75
+ }
76
+
77
+ console.log('All migrations applied.');
78
+ } finally {
79
+ await closePool();
80
+ }
81
+ }
82
+ },
83
+ 'db:migrate:rollback': {
84
+ description: 'Rollback the most recent MySQL migration',
85
+ bootstrap: true,
86
+ run: async () => {
87
+ const config = (await import('stonyx/config')).default;
88
+ const mysqlConfig = config.orm.mysql;
89
+
90
+ if (!mysqlConfig) {
91
+ console.error('MySQL is not configured. Set MYSQL_HOST to enable MySQL mode.');
92
+ process.exit(1);
93
+ }
94
+
95
+ const { getPool, closePool } = await import('./mysql/connection.js');
96
+ const { ensureMigrationsTable, getAppliedMigrations, rollbackMigration, parseMigrationFile } = await import('./mysql/migration-runner.js');
97
+ const { readFile } = await import('@stonyx/utils/file');
98
+ const path = await import('path');
99
+
100
+ const pool = await getPool(mysqlConfig);
101
+ const migrationsPath = path.resolve(config.rootPath, mysqlConfig.migrationsDir);
102
+
103
+ try {
104
+ await ensureMigrationsTable(pool, mysqlConfig.migrationsTable);
105
+
106
+ const applied = await getAppliedMigrations(pool, mysqlConfig.migrationsTable);
107
+
108
+ if (applied.length === 0) {
109
+ console.log('No migrations to rollback.');
110
+ return;
111
+ }
112
+
113
+ const lastFilename = applied[applied.length - 1];
114
+ const content = await readFile(path.join(migrationsPath, lastFilename));
115
+ const { down } = parseMigrationFile(content);
116
+
117
+ if (!down) {
118
+ console.error(`No DOWN section found in ${lastFilename}. Cannot rollback.`);
119
+ process.exit(1);
120
+ }
121
+
122
+ await rollbackMigration(pool, lastFilename, down, mysqlConfig.migrationsTable);
123
+ console.log(`Rolled back: ${lastFilename}`);
124
+ } finally {
125
+ await closePool();
126
+ }
127
+ }
128
+ },
129
+ 'db:migrate:status': {
130
+ description: 'Show status of MySQL migrations',
131
+ bootstrap: true,
132
+ run: async () => {
133
+ const config = (await import('stonyx/config')).default;
134
+ const mysqlConfig = config.orm.mysql;
135
+
136
+ if (!mysqlConfig) {
137
+ console.error('MySQL is not configured. Set MYSQL_HOST to enable MySQL mode.');
138
+ process.exit(1);
139
+ }
140
+
141
+ const { getPool, closePool } = await import('./mysql/connection.js');
142
+ const { ensureMigrationsTable, getAppliedMigrations, getMigrationFiles } = await import('./mysql/migration-runner.js');
143
+ const path = await import('path');
144
+
145
+ const pool = await getPool(mysqlConfig);
146
+ const migrationsPath = path.resolve(config.rootPath, mysqlConfig.migrationsDir);
147
+
148
+ try {
149
+ await ensureMigrationsTable(pool, mysqlConfig.migrationsTable);
150
+
151
+ const applied = new Set(await getAppliedMigrations(pool, mysqlConfig.migrationsTable));
152
+ const files = await getMigrationFiles(migrationsPath);
153
+
154
+ if (files.length === 0) {
155
+ console.log('No migration files found.');
156
+ return;
157
+ }
158
+
159
+ console.log('Migration status:');
160
+
161
+ for (const filename of files) {
162
+ const status = applied.has(filename) ? 'applied' : 'pending';
163
+ console.log(` [${status}] ${filename}`);
164
+ }
165
+ } finally {
166
+ await closePool();
167
+ }
168
+ }
169
+ },
170
+ };
package/src/db.js CHANGED
@@ -18,14 +18,15 @@ import Cron from '@stonyx/cron';
18
18
  import config from 'stonyx/config';
19
19
  import log from 'stonyx/log';
20
20
  import Orm, { createRecord, store } from '@stonyx/orm';
21
- import { createFile, updateFile, readFile } from '@stonyx/utils/file';
21
+ import { createFile, createDirectory, updateFile, readFile, fileExists } from '@stonyx/utils/file';
22
+ import path from 'path';
22
23
 
23
24
  export const dbKey = '__db';
24
25
 
25
26
  export default class DB {
26
27
  constructor() {
27
28
  if (DB.instance) return DB.instance;
28
-
29
+
29
30
  DB.instance = this;
30
31
  }
31
32
 
@@ -38,12 +39,69 @@ export default class DB {
38
39
  return (await import(`${rootPath}/${schema}`)).default;
39
40
  }
40
41
 
42
+ getCollectionKeys() {
43
+ const SchemaClass = Orm.instance.models[`${dbKey}Model`];
44
+ const instance = new SchemaClass();
45
+ const keys = [];
46
+
47
+ for (const key of Object.keys(instance)) {
48
+ if (key === '__name' || key === 'id') continue;
49
+ if (typeof instance[key] === 'function') keys.push(key);
50
+ }
51
+
52
+ return keys;
53
+ }
54
+
55
+ getDirPath() {
56
+ const { rootPath } = config;
57
+ const { file, directory } = config.orm.db;
58
+ const dbDir = path.dirname(path.resolve(`${rootPath}/${file}`));
59
+
60
+ return path.join(dbDir, directory);
61
+ }
62
+
63
+ async validateMode() {
64
+ const { rootPath } = config;
65
+ const { file, mode } = config.orm.db;
66
+ const collectionKeys = this.getCollectionKeys();
67
+ const dirPath = this.getDirPath();
68
+
69
+ if (mode === 'directory') {
70
+ const dbFilePath = path.resolve(`${rootPath}/${file}`);
71
+ const exists = await fileExists(dbFilePath);
72
+
73
+ if (exists) {
74
+ const data = await readFile(dbFilePath, { json: true });
75
+ const hasData = collectionKeys.some(key => Array.isArray(data[key]) && data[key].length > 0);
76
+
77
+ if (hasData) {
78
+ log.error(`DB mode mismatch: db.json contains data but mode is set to 'directory'. Run migration first:\n\n stonyx db:migrate-to-directory\n`);
79
+ process.exit(1);
80
+ }
81
+ }
82
+ } else {
83
+ const dirExists = await fileExists(dirPath);
84
+
85
+ if (dirExists) {
86
+ const hasCollectionFiles = (await Promise.all(
87
+ collectionKeys.map(key => fileExists(path.join(dirPath, `${key}.json`)))
88
+ )).some(Boolean);
89
+
90
+ if (hasCollectionFiles) {
91
+ log.error(`DB mode mismatch: directory '${config.orm.db.directory}/' contains collection files but mode is set to 'file'. Run migration first:\n\n stonyx db:migrate-to-file\n`);
92
+ process.exit(1);
93
+ }
94
+ }
95
+ }
96
+ }
97
+
41
98
  async init() {
42
99
  const { autosave, saveInterval } = config.orm.db;
43
-
100
+
44
101
  store.set(dbKey, new Map());
45
102
  Orm.instance.models[`${dbKey}Model`] = await this.getSchema();
46
103
 
104
+ await this.validateMode();
47
105
  this.record = await this.getRecord();
48
106
 
49
107
  if (autosave !== 'true') return;
@@ -53,28 +111,96 @@ export default class DB {
53
111
 
54
112
  async create() {
55
113
  const { rootPath } = config;
56
- const { file } = config.orm.db;
114
+ const { file, mode } = config.orm.db;
115
+
116
+ if (mode === 'directory') {
117
+ const dirPath = this.getDirPath();
118
+ const collectionKeys = this.getCollectionKeys();
119
+
120
+ await createDirectory(dirPath);
121
+
122
+ await Promise.all(collectionKeys.map(key =>
123
+ createFile(path.join(dirPath, `${key}.json`), [], { json: true })
124
+ ));
125
+
126
+ // Write empty-array skeleton to db.json
127
+ const skeleton = {};
128
+ for (const key of collectionKeys) skeleton[key] = [];
129
+
130
+ await createFile(`${rootPath}/${file}`, skeleton, { json: true });
131
+
132
+ return skeleton;
133
+ }
57
134
 
58
135
  createFile(`${rootPath}/${file}`, {}, { json: true });
59
136
 
60
137
  return {};
61
138
  }
62
-
139
+
63
140
  async save() {
64
- const { file } = config.orm.db;
141
+ const { file, mode } = config.orm.db;
65
142
  const jsonData = this.record.format();
66
143
  delete jsonData.id; // Don't save id
67
144
 
145
+ if (mode === 'directory') {
146
+ const dirPath = this.getDirPath();
147
+ const collectionKeys = this.getCollectionKeys();
148
+
149
+ // Write each collection to its own file in parallel
150
+ await Promise.all(collectionKeys.map(key =>
151
+ updateFile(path.join(dirPath, `${key}.json`), jsonData[key] || [], { json: true })
152
+ ));
153
+
154
+ // Write empty-array skeleton to db.json
155
+ const skeleton = {};
156
+ for (const key of collectionKeys) skeleton[key] = [];
157
+
158
+ await updateFile(`${config.rootPath}/${file}`, skeleton, { json: true });
159
+
160
+ log.db(`DB has been successfully saved to ${config.orm.db.directory}/ directory`);
161
+ return;
162
+ }
163
+
68
164
  await updateFile(`${config.rootPath}/${file}`, jsonData, { json: true });
69
165
 
70
166
  log.db(`DB has been successfully saved to ${file}`);
71
167
  }
72
168
 
73
169
  async getRecord() {
170
+ const { mode } = config.orm.db;
171
+
172
+ if (mode === 'directory') return this.getRecordFromDirectory();
173
+
174
+ return this.getRecordFromFile();
175
+ }
176
+
177
+ async getRecordFromFile() {
74
178
  const { file } = config.orm.db;
75
179
 
76
180
  const data = await readFile(file, { json: true, missingFileCallback: this.create.bind(this) });
77
181
 
78
182
  return createRecord(dbKey, data, { isDbRecord: true, serialize: false, transform: false });
79
183
  }
184
+
185
+ async getRecordFromDirectory() {
186
+ const dirPath = this.getDirPath();
187
+ const collectionKeys = this.getCollectionKeys();
188
+ const dirExists = await fileExists(dirPath);
189
+
190
+ if (!dirExists) {
191
+ const data = await this.create();
192
+ return createRecord(dbKey, data, { isDbRecord: true, serialize: false, transform: false });
193
+ }
194
+
195
+ const assembled = {};
196
+
197
+ await Promise.all(collectionKeys.map(async key => {
198
+ const filePath = path.join(dirPath, `${key}.json`);
199
+ const exists = await fileExists(filePath);
200
+
201
+ assembled[key] = exists ? await readFile(filePath, { json: true }) : [];
202
+ }));
203
+
204
+ return createRecord(dbKey, assembled, { isDbRecord: true, serialize: false, transform: false });
205
+ }
80
206
  }
package/src/hooks.js ADDED
@@ -0,0 +1,124 @@
1
+ /*
2
+ * Copyright 2025 Stone Costa
3
+ *
4
+ * Licensed under the Apache License, Version 2.0 (the 'License');
5
+ * you may not use this file except in compliance with the License.
6
+ * You may obtain a copy of the License at
7
+ *
8
+ * http://www.apache.org/licenses/LICENSE-2.0
9
+ *
10
+ * Unless required by applicable law or agreed to in writing, software
11
+ * distributed under the License is distributed on an "AS IS" BASIS,
12
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ * See the License for the specific language governing permissions and
14
+ * limitations under the License.
15
+ */
16
+
17
+ /**
18
+ * Middleware-based hooks registry for ORM operations.
19
+ * Unlike event-based hooks, middleware hooks run sequentially and can halt operations.
20
+ */
21
+
22
+ // Map of "operation:model" -> handler[]
23
+ const beforeHooks = new Map();
24
+ const afterHooks = new Map();
25
+
26
+ /**
27
+ * Register a before hook middleware that runs before the operation executes.
28
+ *
29
+ * @param {string} operation - Operation name: 'create', 'update', 'delete', 'get', or 'list'
30
+ * @param {string} model - Model name (e.g., 'user', 'animal')
31
+ * @param {Function} handler - Middleware function (context) => any
32
+ * - Return undefined to continue to next hook/handler
33
+ * - Return any value to halt operation (integer = HTTP status, object = response body)
34
+ * @returns {Function} Unsubscribe function
35
+ */
36
+ export function beforeHook(operation, model, handler) {
37
+ const key = `${operation}:${model}`;
38
+ if (!beforeHooks.has(key)) {
39
+ beforeHooks.set(key, []);
40
+ }
41
+ beforeHooks.get(key).push(handler);
42
+
43
+ // Return unsubscribe function
44
+ return () => {
45
+ const hooks = beforeHooks.get(key);
46
+ if (hooks) {
47
+ const index = hooks.indexOf(handler);
48
+ if (index > -1) hooks.splice(index, 1);
49
+ }
50
+ };
51
+ }
52
+
53
+ /**
54
+ * Register an after hook middleware that runs after the operation completes.
55
+ * After hooks cannot halt operations (they run after completion).
56
+ *
57
+ * @param {string} operation - Operation name
58
+ * @param {string} model - Model name
59
+ * @param {Function} handler - Middleware function (context) => void
60
+ * @returns {Function} Unsubscribe function
61
+ */
62
+ export function afterHook(operation, model, handler) {
63
+ const key = `${operation}:${model}`;
64
+ if (!afterHooks.has(key)) {
65
+ afterHooks.set(key, []);
66
+ }
67
+ afterHooks.get(key).push(handler);
68
+
69
+ // Return unsubscribe function
70
+ return () => {
71
+ const hooks = afterHooks.get(key);
72
+ if (hooks) {
73
+ const index = hooks.indexOf(handler);
74
+ if (index > -1) hooks.splice(index, 1);
75
+ }
76
+ };
77
+ }
78
+
79
+ /**
80
+ * Get all before hooks for an operation:model combination.
81
+ * @param {string} operation
82
+ * @param {string} model
83
+ * @returns {Function[]}
84
+ */
85
+ export function getBeforeHooks(operation, model) {
86
+ const key = `${operation}:${model}`;
87
+ return beforeHooks.get(key) || [];
88
+ }
89
+
90
+ /**
91
+ * Get all after hooks for an operation:model combination.
92
+ * @param {string} operation
93
+ * @param {string} model
94
+ * @returns {Function[]}
95
+ */
96
+ export function getAfterHooks(operation, model) {
97
+ const key = `${operation}:${model}`;
98
+ return afterHooks.get(key) || [];
99
+ }
100
+
101
+ /**
102
+ * Clear registered hooks for a specific operation:model.
103
+ *
104
+ * @param {string} operation - Operation name
105
+ * @param {string} model - Model name
106
+ * @param {string} [type] - 'before' or 'after' (if omitted, clears both)
107
+ */
108
+ export function clearHook(operation, model, type) {
109
+ const key = `${operation}:${model}`;
110
+ if (!type || type === 'before') {
111
+ beforeHooks.set(key, []);
112
+ }
113
+ if (!type || type === 'after') {
114
+ afterHooks.set(key, []);
115
+ }
116
+ }
117
+
118
+ /**
119
+ * Clear all hooks (useful for testing).
120
+ */
121
+ export function clearAllHooks() {
122
+ beforeHooks.clear();
123
+ afterHooks.clear();
124
+ }
package/src/index.js CHANGED
@@ -25,4 +25,11 @@ import { createRecord, updateRecord } from './manage-record.js';
25
25
  export { default } from './main.js';
26
26
  export { store, relationships } from './main.js';
27
27
  export { Model, Serializer }; // base classes
28
- export { attr, belongsTo, hasMany, createRecord, updateRecord }; // helpers
28
+ export { attr, belongsTo, hasMany, createRecord, updateRecord }; // helpers
29
+ export { beforeHook, afterHook, clearHook, clearAllHooks } from './hooks.js'; // middleware hooks
30
+
31
+ // Store API:
32
+ // store.get(model, id) — sync, memory-only
33
+ // store.find(model, id) — async, MySQL for memory:false models
34
+ // store.findAll(model) — async, all records
35
+ // store.query(model, conditions) — async, always hits MySQL
package/src/main.js CHANGED
@@ -19,10 +19,12 @@ import config from 'stonyx/config';
19
19
  import log from 'stonyx/log';
20
20
  import { forEachFileImport } from '@stonyx/utils/file';
21
21
  import { kebabCaseToPascalCase, pluralize } from '@stonyx/utils/string';
22
+ import { registerPluralName } from './plural-registry.js';
22
23
  import setupRestServer from './setup-rest-server.js';
23
24
  import baseTransforms from './transforms.js';
24
25
  import Store from './store.js';
25
26
  import Serializer from './serializer.js';
27
+ import { setup } from '@stonyx/events';
26
28
 
27
29
  const defaultOptions = {
28
30
  dbType: 'json'
@@ -65,7 +67,10 @@ export default class Orm {
65
67
  // Transforms keep their original name, everything else gets converted to PascalCase with the type suffix
66
68
  const alias = type === 'Transform' ? name : `${kebabCaseToPascalCase(name)}${type}`;
67
69
 
68
- if (type === 'Model') Orm.store.set(name, new Map());
70
+ if (type === 'Model') {
71
+ Orm.store.set(name, new Map());
72
+ registerPluralName(name, exported);
73
+ }
69
74
 
70
75
  return this[pluralize(lowerCaseType)][alias] = exported;
71
76
  }, { ignoreAccessFailure: true, rawName: true, recursive: true, recursiveNaming: true });
@@ -74,10 +79,30 @@ export default class Orm {
74
79
  // Wait for imports before db & rest server setup
75
80
  await Promise.all(promises);
76
81
 
77
- if (this.options.dbType !== 'none') {
82
+ // Setup event names for hooks after models are loaded
83
+ const eventNames = [];
84
+ const operations = ['list', 'get', 'create', 'update', 'delete'];
85
+ const timings = ['before', 'after'];
86
+
87
+ for (const modelName of Orm.store.data.keys()) {
88
+ for (const timing of timings) {
89
+ for (const operation of operations) {
90
+ eventNames.push(`${timing}:${operation}:${modelName}`);
91
+ }
92
+ }
93
+ }
94
+
95
+ setup(eventNames);
96
+
97
+ if (config.orm.mysql) {
98
+ const { default: MysqlDB } = await import('./mysql/mysql-db.js');
99
+ this.mysqlDb = new MysqlDB();
100
+ this.db = this.mysqlDb;
101
+ promises.push(this.mysqlDb.init());
102
+ } else if (this.options.dbType !== 'none') {
78
103
  const db = new DB();
79
104
  this.db = db;
80
-
105
+
81
106
  promises.push(db.init());
82
107
  }
83
108
 
@@ -85,10 +110,29 @@ export default class Orm {
85
110
  promises.push(setupRestServer(restServer.route, paths.access, restServer.metaRoute));
86
111
  }
87
112
 
113
+ // Wire up memory resolver so store.find() can check model memory flags
114
+ Orm.store._memoryResolver = (modelName) => {
115
+ const { modelClass } = this.getRecordClasses(modelName);
116
+ return modelClass?.memory !== false;
117
+ };
118
+
119
+ // Wire up MySQL reference for on-demand queries from store.find()/findAll()
120
+ if (this.mysqlDb) {
121
+ Orm.store._mysqlDb = this.mysqlDb;
122
+ }
123
+
88
124
  Orm.ready = await Promise.all(promises);
89
125
  Orm.initialized = true;
90
126
  }
91
127
 
128
+ async startup() {
129
+ if (this.mysqlDb) await this.mysqlDb.startup();
130
+ }
131
+
132
+ async shutdown() {
133
+ if (this.mysqlDb) await this.mysqlDb.shutdown();
134
+ }
135
+
92
136
  static get db() {
93
137
  if (!Orm.initialized) throw new Error('ORM has not been initialized yet');
94
138
 
@@ -90,14 +90,29 @@ export function updateRecord(record, rawData, userOptions={}) {
90
90
 
91
91
  /**
92
92
  * gets the next available id based on last record entry.
93
- *
94
- * Note/TODO: Records going into a db should get their id from the db instead
95
- * Atm, i think the best way to do that would be as an id override that happens after the
96
- * record is created
93
+ *
94
+ * In MySQL mode with numeric IDs, assigns a temporary pending ID.
95
+ * MySQL's AUTO_INCREMENT provides the real ID after INSERT.
97
96
  */
98
97
  function assignRecordId(modelName, rawData) {
99
98
  if (rawData.id) return;
100
99
 
100
+ // In MySQL mode with numeric IDs, defer to MySQL auto-increment
101
+ if (Orm.instance?.mysqlDb && !isStringIdModel(modelName)) {
102
+ rawData.id = `__pending_${Date.now()}_${Math.random()}`;
103
+ rawData.__pendingMysqlId = true;
104
+ return;
105
+ }
106
+
101
107
  const modelStore = Array.from(store.get(modelName).values());
102
108
  rawData.id = modelStore.length ? modelStore.at(-1).id + 1 : 1;
103
109
  }
110
+
111
+ function isStringIdModel(modelName) {
112
+ const { modelClass } = Orm.instance.getRecordClasses(modelName);
113
+ if (!modelClass) return false;
114
+
115
+ const model = new modelClass(modelName);
116
+
117
+ return model.id?.type === 'string';
118
+ }
package/src/migrate.js ADDED
@@ -0,0 +1,72 @@
1
+ import config from 'stonyx/config';
2
+ import Orm from '@stonyx/orm';
3
+ import { createFile, createDirectory, readFile, updateFile, deleteDirectory } from '@stonyx/utils/file';
4
+ import { dbKey } from './db.js';
5
+ import path from 'path';
6
+
7
+ function getCollectionKeys() {
8
+ const SchemaClass = Orm.instance.models[`${dbKey}Model`];
9
+ const instance = new SchemaClass();
10
+ const keys = [];
11
+
12
+ for (const key of Object.keys(instance)) {
13
+ if (key === '__name' || key === 'id') continue;
14
+ if (typeof instance[key] === 'function') keys.push(key);
15
+ }
16
+
17
+ return keys;
18
+ }
19
+
20
+ function getDirPath() {
21
+ const { rootPath } = config;
22
+ const { file, directory } = config.orm.db;
23
+ const dbDir = path.dirname(path.resolve(`${rootPath}/${file}`));
24
+
25
+ return path.join(dbDir, directory);
26
+ }
27
+
28
+ export async function fileToDirectory() {
29
+ const { rootPath } = config;
30
+ const { file } = config.orm.db;
31
+ const dbFilePath = path.resolve(`${rootPath}/${file}`);
32
+ const collectionKeys = getCollectionKeys();
33
+ const dirPath = getDirPath();
34
+
35
+ // Read full data from db.json
36
+ const data = await readFile(dbFilePath, { json: true });
37
+
38
+ // Create directory and write each collection
39
+ await createDirectory(dirPath);
40
+
41
+ await Promise.all(collectionKeys.map(key =>
42
+ createFile(path.join(dirPath, `${key}.json`), data[key] || [], { json: true })
43
+ ));
44
+
45
+ // Overwrite db.json with empty-array skeleton
46
+ const skeleton = {};
47
+ for (const key of collectionKeys) skeleton[key] = [];
48
+
49
+ await updateFile(dbFilePath, skeleton, { json: true });
50
+ }
51
+
52
+ export async function directoryToFile() {
53
+ const { rootPath } = config;
54
+ const { file } = config.orm.db;
55
+ const dbFilePath = path.resolve(`${rootPath}/${file}`);
56
+ const collectionKeys = getCollectionKeys();
57
+ const dirPath = getDirPath();
58
+
59
+ // Read each collection from the directory
60
+ const assembled = {};
61
+
62
+ await Promise.all(collectionKeys.map(async key => {
63
+ const filePath = path.join(dirPath, `${key}.json`);
64
+ assembled[key] = await readFile(filePath, { json: true });
65
+ }));
66
+
67
+ // Overwrite db.json with full assembled data
68
+ await updateFile(dbFilePath, assembled, { json: true });
69
+
70
+ // Remove the directory
71
+ await deleteDirectory(dirPath);
72
+ }