@stonyx/orm 0.2.5-alpha.0 → 0.3.1

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 (166) hide show
  1. package/README.md +482 -15
  2. package/config/environment.js +63 -6
  3. package/dist/aggregates.d.ts +21 -0
  4. package/dist/aggregates.js +93 -0
  5. package/dist/attr.d.ts +2 -0
  6. package/dist/attr.js +22 -0
  7. package/dist/belongs-to.d.ts +11 -0
  8. package/dist/belongs-to.js +59 -0
  9. package/dist/cli.d.ts +22 -0
  10. package/dist/cli.js +148 -0
  11. package/dist/commands.d.ts +7 -0
  12. package/dist/commands.js +146 -0
  13. package/dist/db.d.ts +21 -0
  14. package/dist/db.js +180 -0
  15. package/dist/exports/db.d.ts +7 -0
  16. package/{src → dist}/exports/db.js +2 -4
  17. package/dist/has-many.d.ts +11 -0
  18. package/dist/has-many.js +58 -0
  19. package/dist/hooks.d.ts +75 -0
  20. package/dist/hooks.js +110 -0
  21. package/dist/index.d.ts +14 -0
  22. package/dist/index.js +34 -0
  23. package/dist/main.d.ts +46 -0
  24. package/dist/main.js +181 -0
  25. package/dist/manage-record.d.ts +13 -0
  26. package/dist/manage-record.js +123 -0
  27. package/dist/meta-request.d.ts +6 -0
  28. package/dist/meta-request.js +52 -0
  29. package/dist/migrate.d.ts +2 -0
  30. package/dist/migrate.js +57 -0
  31. package/dist/model-property.d.ts +9 -0
  32. package/dist/model-property.js +29 -0
  33. package/dist/model.d.ts +15 -0
  34. package/dist/model.js +18 -0
  35. package/dist/mysql/connection.d.ts +14 -0
  36. package/dist/mysql/connection.js +24 -0
  37. package/dist/mysql/migration-generator.d.ts +45 -0
  38. package/dist/mysql/migration-generator.js +254 -0
  39. package/dist/mysql/migration-runner.d.ts +12 -0
  40. package/dist/mysql/migration-runner.js +88 -0
  41. package/dist/mysql/mysql-db.d.ts +100 -0
  42. package/dist/mysql/mysql-db.js +425 -0
  43. package/dist/mysql/query-builder.d.ts +10 -0
  44. package/dist/mysql/query-builder.js +44 -0
  45. package/dist/mysql/schema-introspector.d.ts +19 -0
  46. package/dist/mysql/schema-introspector.js +257 -0
  47. package/dist/mysql/type-map.d.ts +21 -0
  48. package/dist/mysql/type-map.js +36 -0
  49. package/dist/orm-request.d.ts +38 -0
  50. package/dist/orm-request.js +475 -0
  51. package/dist/plural-registry.d.ts +4 -0
  52. package/dist/plural-registry.js +9 -0
  53. package/dist/postgres/connection.d.ts +15 -0
  54. package/dist/postgres/connection.js +32 -0
  55. package/dist/postgres/migration-generator.d.ts +45 -0
  56. package/dist/postgres/migration-generator.js +280 -0
  57. package/dist/postgres/migration-runner.d.ts +10 -0
  58. package/dist/postgres/migration-runner.js +87 -0
  59. package/dist/postgres/postgres-db.d.ts +119 -0
  60. package/dist/postgres/postgres-db.js +477 -0
  61. package/dist/postgres/query-builder.d.ts +27 -0
  62. package/dist/postgres/query-builder.js +98 -0
  63. package/dist/postgres/schema-introspector.d.ts +29 -0
  64. package/dist/postgres/schema-introspector.js +296 -0
  65. package/dist/postgres/type-map.d.ts +23 -0
  66. package/dist/postgres/type-map.js +56 -0
  67. package/dist/record.d.ts +75 -0
  68. package/dist/record.js +129 -0
  69. package/dist/relationships.d.ts +10 -0
  70. package/dist/relationships.js +41 -0
  71. package/dist/schema-helpers.d.ts +20 -0
  72. package/dist/schema-helpers.js +48 -0
  73. package/dist/serializer.d.ts +17 -0
  74. package/dist/serializer.js +136 -0
  75. package/dist/setup-rest-server.d.ts +1 -0
  76. package/dist/setup-rest-server.js +52 -0
  77. package/dist/standalone-db.d.ts +58 -0
  78. package/dist/standalone-db.js +142 -0
  79. package/dist/store.d.ts +62 -0
  80. package/dist/store.js +286 -0
  81. package/dist/timescale/query-builder.d.ts +43 -0
  82. package/dist/timescale/query-builder.js +115 -0
  83. package/dist/timescale/timescale-db.d.ts +45 -0
  84. package/dist/timescale/timescale-db.js +84 -0
  85. package/dist/transforms.d.ts +2 -0
  86. package/dist/transforms.js +17 -0
  87. package/dist/types/orm-types.d.ts +153 -0
  88. package/dist/types/orm-types.js +1 -0
  89. package/dist/utils.d.ts +7 -0
  90. package/dist/utils.js +17 -0
  91. package/dist/view-resolver.d.ts +8 -0
  92. package/dist/view-resolver.js +171 -0
  93. package/dist/view.d.ts +11 -0
  94. package/dist/view.js +18 -0
  95. package/package.json +64 -11
  96. package/src/aggregates.ts +109 -0
  97. package/src/{attr.js → attr.ts} +2 -2
  98. package/src/belongs-to.ts +90 -0
  99. package/src/cli.ts +183 -0
  100. package/src/commands.ts +179 -0
  101. package/src/db.ts +232 -0
  102. package/src/exports/db.ts +7 -0
  103. package/src/has-many.ts +92 -0
  104. package/src/hooks.ts +151 -0
  105. package/src/{index.js → index.ts} +12 -2
  106. package/src/main.ts +229 -0
  107. package/src/manage-record.ts +161 -0
  108. package/src/{meta-request.js → meta-request.ts} +17 -14
  109. package/src/migrate.ts +72 -0
  110. package/src/model-property.ts +35 -0
  111. package/src/model.ts +21 -0
  112. package/src/mysql/connection.ts +43 -0
  113. package/src/mysql/migration-generator.ts +337 -0
  114. package/src/mysql/migration-runner.ts +121 -0
  115. package/src/mysql/mysql-db.ts +543 -0
  116. package/src/mysql/query-builder.ts +69 -0
  117. package/src/mysql/schema-introspector.ts +310 -0
  118. package/src/mysql/type-map.ts +42 -0
  119. package/src/orm-request.ts +582 -0
  120. package/src/plural-registry.ts +12 -0
  121. package/src/postgres/connection.ts +48 -0
  122. package/src/postgres/migration-generator.ts +370 -0
  123. package/src/postgres/migration-runner.ts +115 -0
  124. package/src/postgres/postgres-db.ts +616 -0
  125. package/src/postgres/query-builder.ts +148 -0
  126. package/src/postgres/schema-introspector.ts +360 -0
  127. package/src/postgres/type-map.ts +61 -0
  128. package/src/record.ts +186 -0
  129. package/src/relationships.ts +54 -0
  130. package/src/schema-helpers.ts +59 -0
  131. package/src/serializer.ts +161 -0
  132. package/src/setup-rest-server.ts +62 -0
  133. package/src/standalone-db.ts +185 -0
  134. package/src/store.ts +373 -0
  135. package/src/timescale/query-builder.ts +174 -0
  136. package/src/timescale/timescale-db.ts +119 -0
  137. package/src/transforms.ts +20 -0
  138. package/src/types/mysql2.d.ts +49 -0
  139. package/src/types/orm-types.ts +158 -0
  140. package/src/types/pg.d.ts +32 -0
  141. package/src/types/stonyx-cron.d.ts +5 -0
  142. package/src/types/stonyx-events.d.ts +4 -0
  143. package/src/types/stonyx-rest-server.d.ts +16 -0
  144. package/src/types/stonyx-utils.d.ts +33 -0
  145. package/src/types/stonyx.d.ts +21 -0
  146. package/src/utils.ts +22 -0
  147. package/src/view-resolver.ts +211 -0
  148. package/src/view.ts +22 -0
  149. package/.claude/project-structure.md +0 -578
  150. package/.github/workflows/ci.yml +0 -36
  151. package/.github/workflows/publish.yml +0 -143
  152. package/src/belongs-to.js +0 -63
  153. package/src/db.js +0 -80
  154. package/src/has-many.js +0 -61
  155. package/src/main.js +0 -119
  156. package/src/manage-record.js +0 -103
  157. package/src/model-property.js +0 -29
  158. package/src/model.js +0 -9
  159. package/src/orm-request.js +0 -249
  160. package/src/record.js +0 -100
  161. package/src/relationships.js +0 -43
  162. package/src/serializer.js +0 -138
  163. package/src/setup-rest-server.js +0 -57
  164. package/src/store.js +0 -211
  165. package/src/transforms.js +0 -20
  166. package/stonyx-bootstrap.cjs +0 -30
@@ -0,0 +1,90 @@
1
+ import { createRecord, store } from '@stonyx/orm';
2
+ import { getRelationships, getHasManyRegistry, getPendingRegistry, getPendingBelongsToRegistry } from './relationships.js';
3
+ import type { SourceRecord } from './types/orm-types.js';
4
+
5
+ function getOrSet<K, V>(map: Map<K, V>, key: K, defaultValue: V): V {
6
+ if (!map.has(key)) map.set(key, defaultValue);
7
+ return map.get(key) as V;
8
+ }
9
+
10
+ interface BelongsToOptions {
11
+ _relationshipKey?: string;
12
+ [key: string]: unknown;
13
+ }
14
+
15
+ interface PendingBelongsToEntry {
16
+ sourceRecord: SourceRecord;
17
+ sourceModelName: string;
18
+ relationshipKey: string | undefined;
19
+ relationshipId: unknown;
20
+ }
21
+
22
+ type RelationshipHandler = ((sourceRecord: SourceRecord, rawData: unknown, options: BelongsToOptions) => unknown) & {
23
+ __relatedModelName: string;
24
+ __relationshipType: 'belongsTo';
25
+ };
26
+
27
+ export default function belongsTo(modelName: string): RelationshipHandler {
28
+ const hasManyRelationships = getHasManyRegistry();
29
+ const pendingHasManyQueue = getPendingRegistry();
30
+ const pendingBelongsToQueue = getPendingBelongsToRegistry();
31
+
32
+ const fn = (sourceRecord: SourceRecord, rawData: unknown, options: BelongsToOptions): unknown => {
33
+ if (!rawData) return null;
34
+
35
+ const { __name: sourceModelName } = sourceRecord.__model;
36
+ const relationshipId = sourceRecord.id;
37
+ const relationshipKey = options._relationshipKey;
38
+ const relationship = getRelationships('belongsTo', sourceModelName, modelName, relationshipId as string) as Map<unknown, unknown>;
39
+ const modelStore = store.get(modelName);
40
+
41
+ // Try to get existing record
42
+ let output: unknown;
43
+
44
+ if (typeof rawData === 'object') {
45
+ output = createRecord(modelName, rawData as Record<string, unknown>, options);
46
+ } else if (modelStore) {
47
+ output = modelStore.get(rawData as number | string);
48
+ }
49
+
50
+ // If not found and is a string ID, register as pending
51
+ if (!output && typeof rawData !== 'object') {
52
+ const targetId = rawData;
53
+
54
+ // Register pending belongsTo
55
+ const modelPendingMap = getOrSet(pendingBelongsToQueue, modelName, new Map());
56
+ const targetPendingArray = getOrSet(modelPendingMap, targetId, []);
57
+
58
+ targetPendingArray.push({
59
+ sourceRecord,
60
+ sourceModelName,
61
+ relationshipKey,
62
+ relationshipId
63
+ });
64
+
65
+ relationship.set(relationshipId, null);
66
+ return null;
67
+ }
68
+
69
+ relationship.set(relationshipId, output || {});
70
+
71
+ // Populate hasMany side if the relationship is defined
72
+ const outputRecord = typeof output === 'object' && output !== null && 'id' in output ? output as SourceRecord : undefined;
73
+ const otherSide = outputRecord ? hasManyRelationships.get(modelName)?.get(sourceModelName)?.get(outputRecord.id) as unknown[] | undefined : undefined;
74
+
75
+ if (otherSide) {
76
+ otherSide.push(sourceRecord);
77
+
78
+ // Remove pending queue if it was just fulfilled
79
+ const pendingModelRelationships = pendingHasManyQueue.get(sourceModelName);
80
+
81
+ if (pendingModelRelationships) pendingModelRelationships.delete(relationshipId);
82
+ }
83
+
84
+ return output;
85
+ };
86
+
87
+ Object.defineProperty(fn, '__relatedModelName', { value: modelName });
88
+ Object.defineProperty(fn, '__relationshipType', { value: 'belongsTo' as const });
89
+ return fn as RelationshipHandler;
90
+ }
package/src/cli.ts ADDED
@@ -0,0 +1,183 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * Standalone CLI for ORM database operations.
5
+ *
6
+ * Performs CRUD operations on the JSON database without requiring
7
+ * the full Stonyx bootstrap. Supports both file and directory modes.
8
+ *
9
+ * Usage:
10
+ * stonyx-orm create <collection> <json-data>
11
+ * stonyx-orm list <collection>
12
+ * stonyx-orm get <collection> <id>
13
+ * stonyx-orm delete <collection> <id>
14
+ *
15
+ * Configuration (environment variables):
16
+ * DB_MODE -- 'file' or 'directory' (default: 'directory')
17
+ * DB_PATH -- Path to db.json (default: 'db.json')
18
+ * DB_DIRECTORY -- Directory name for collection files (default: 'db')
19
+ *
20
+ * Configuration (CLI flag):
21
+ * --config <path> -- Path to a JSON config file with { mode, dbPath, directory }
22
+ */
23
+
24
+ import StandaloneDB from './standalone-db.js';
25
+ import fs from 'fs/promises';
26
+
27
+ interface CLIConfig {
28
+ mode: 'file' | 'directory';
29
+ dbPath: string;
30
+ directory: string;
31
+ }
32
+
33
+ const USAGE = `Usage: stonyx-orm <command> [options]
34
+
35
+ Commands:
36
+ create <collection> <json-data> Create a record
37
+ list <collection> List all records
38
+ get <collection> <id> Get a record by ID
39
+ delete <collection> <id> Delete a record by ID
40
+
41
+ Options:
42
+ --config <path> Path to JSON config file
43
+ --help Show this help message
44
+
45
+ Environment variables:
46
+ DB_MODE 'file' or 'directory' (default: 'directory')
47
+ DB_PATH Path to db.json (default: 'db.json')
48
+ DB_DIRECTORY Directory name for collection files (default: 'db')`;
49
+
50
+ async function loadConfig(args: string[]): Promise<CLIConfig> {
51
+ const config: Record<string, string> = {};
52
+
53
+ // Check for --config flag
54
+ const configIndex = args.indexOf('--config');
55
+
56
+ if (configIndex !== -1 && args[configIndex + 1]) {
57
+ const configPath = args[configIndex + 1];
58
+
59
+ try {
60
+ const content = await fs.readFile(configPath, 'utf-8');
61
+ Object.assign(config, JSON.parse(content));
62
+ } catch (err) {
63
+ console.error(`Error reading config file '${configPath}': ${err instanceof Error ? err.message : String(err)}`);
64
+ process.exit(1);
65
+ }
66
+
67
+ // Remove --config and its value from args
68
+ args.splice(configIndex, 2);
69
+ }
70
+
71
+ // Environment variables override config file, config file overrides defaults
72
+ return {
73
+ mode: (process.env.DB_MODE || config.mode || 'directory') as 'file' | 'directory',
74
+ dbPath: process.env.DB_PATH || config.dbPath || 'db.json',
75
+ directory: process.env.DB_DIRECTORY || config.directory || 'db',
76
+ };
77
+ }
78
+
79
+ function parseArgs(argv: string[]): string[] {
80
+ // Strip node binary and script path
81
+ const args = argv.slice(2);
82
+
83
+ if (args.includes('--help') || args.includes('-h') || args.length === 0) {
84
+ console.log(USAGE);
85
+ process.exit(0);
86
+ }
87
+
88
+ return args;
89
+ }
90
+
91
+ async function run(): Promise<void> {
92
+ const args = parseArgs(process.argv);
93
+ const config = await loadConfig(args);
94
+ const db = new StandaloneDB(config);
95
+
96
+ const [command, collection, ...rest] = args;
97
+
98
+ if (!command) {
99
+ console.error('Error: No command specified.\n');
100
+ console.log(USAGE);
101
+ process.exit(1);
102
+ }
103
+
104
+ if (!collection && command !== '--help') {
105
+ console.error(`Error: No collection specified for '${command}' command.\n`);
106
+ console.log(USAGE);
107
+ process.exit(1);
108
+ }
109
+
110
+ try {
111
+ switch (command) {
112
+ case 'list': {
113
+ const records = await db.list(collection);
114
+ console.log(JSON.stringify(records, null, 2));
115
+ break;
116
+ }
117
+
118
+ case 'get': {
119
+ const id = rest[0];
120
+
121
+ if (!id) {
122
+ console.error("Error: 'get' command requires an <id> argument.");
123
+ process.exit(1);
124
+ }
125
+
126
+ const record = await db.get(collection, id);
127
+
128
+ if (!record) {
129
+ console.error(`Record with id '${id}' not found in '${collection}'.`);
130
+ process.exit(1);
131
+ }
132
+
133
+ console.log(JSON.stringify(record, null, 2));
134
+ break;
135
+ }
136
+
137
+ case 'create': {
138
+ const jsonStr = rest.join(' ');
139
+
140
+ if (!jsonStr) {
141
+ console.error("Error: 'create' command requires <json-data> argument.");
142
+ process.exit(1);
143
+ }
144
+
145
+ let data;
146
+
147
+ try {
148
+ data = JSON.parse(jsonStr);
149
+ } catch {
150
+ console.error(`Error: Invalid JSON data: ${jsonStr}`);
151
+ process.exit(1);
152
+ }
153
+
154
+ const created = await db.create(collection, data);
155
+ console.log(JSON.stringify(created, null, 2));
156
+ break;
157
+ }
158
+
159
+ case 'delete': {
160
+ const deleteId = rest[0];
161
+
162
+ if (!deleteId) {
163
+ console.error("Error: 'delete' command requires an <id> argument.");
164
+ process.exit(1);
165
+ }
166
+
167
+ const removed = await db.delete(collection, deleteId);
168
+ console.log(JSON.stringify(removed, null, 2));
169
+ break;
170
+ }
171
+
172
+ default:
173
+ console.error(`Error: Unknown command '${command}'.\n`);
174
+ console.log(USAGE);
175
+ process.exit(1);
176
+ }
177
+ } catch (err) {
178
+ console.error(`Error: ${err instanceof Error ? err.message : String(err)}`);
179
+ process.exit(1);
180
+ }
181
+ }
182
+
183
+ run();
@@ -0,0 +1,179 @@
1
+ import { fileToDirectory, directoryToFile } from './migrate.js';
2
+ import type { MysqlConfig } from './mysql/connection.js';
3
+
4
+ interface Command {
5
+ description: string;
6
+ bootstrap: boolean;
7
+ run: (args?: string[]) => Promise<void>;
8
+ }
9
+
10
+ const commands: Record<string, Command> = {
11
+ 'db:migrate-to-directory': {
12
+ description: 'Migrate DB from single file to directory mode',
13
+ bootstrap: true,
14
+ run: async () => {
15
+ await fileToDirectory();
16
+ console.log('DB migration to directory mode complete.');
17
+ }
18
+ },
19
+ 'db:migrate-to-file': {
20
+ description: 'Migrate DB from directory mode to single file',
21
+ bootstrap: true,
22
+ run: async () => {
23
+ await directoryToFile();
24
+ console.log('DB migration to file mode complete.');
25
+ }
26
+ },
27
+ 'db:generate-migration': {
28
+ description: 'Generate a MySQL migration from current model schemas',
29
+ bootstrap: true,
30
+ run: async (args) => {
31
+ const description = args?.join(' ') || 'migration';
32
+ const { generateMigration } = await import('./mysql/migration-generator.js');
33
+ const result = await generateMigration(description);
34
+
35
+ if (result) {
36
+ console.log(`Migration created: ${result.filename}`);
37
+ } else {
38
+ console.log('No schema changes detected. No migration generated.');
39
+ }
40
+ }
41
+ },
42
+ 'db:migrate': {
43
+ description: 'Apply pending MySQL migrations',
44
+ bootstrap: true,
45
+ run: async () => {
46
+ const config = (await import('stonyx/config')).default;
47
+ const mysqlConfig = config.orm.mysql;
48
+
49
+ if (!mysqlConfig) {
50
+ console.error('MySQL is not configured. Set MYSQL_HOST to enable MySQL mode.');
51
+ process.exit(1);
52
+ }
53
+
54
+ const { getPool, closePool } = await import('./mysql/connection.js');
55
+ const { ensureMigrationsTable, getAppliedMigrations, getMigrationFiles, applyMigration, parseMigrationFile } = await import('./mysql/migration-runner.js');
56
+ const { readFile } = await import('@stonyx/utils/file');
57
+ const path = await import('path');
58
+
59
+ const pool = await getPool(mysqlConfig as MysqlConfig);
60
+ const migrationsPath = path.resolve(config.rootPath, mysqlConfig.migrationsDir as string);
61
+
62
+ try {
63
+ await ensureMigrationsTable(pool, mysqlConfig.migrationsTable as string);
64
+
65
+ const applied = await getAppliedMigrations(pool, mysqlConfig.migrationsTable as string);
66
+ const files = await getMigrationFiles(migrationsPath);
67
+ const pending = files.filter((f: string) => !applied.includes(f));
68
+
69
+ if (pending.length === 0) {
70
+ console.log('No pending migrations.');
71
+ return;
72
+ }
73
+
74
+ console.log(`Applying ${pending.length} migration(s)...`);
75
+
76
+ for (const filename of pending) {
77
+ const content = await readFile(path.join(migrationsPath, filename) as string) as string;
78
+ const { up } = parseMigrationFile(content);
79
+
80
+ await applyMigration(pool, filename, up, mysqlConfig.migrationsTable as string);
81
+ console.log(` Applied: ${filename}`);
82
+ }
83
+
84
+ console.log('All migrations applied.');
85
+ } finally {
86
+ await closePool();
87
+ }
88
+ }
89
+ },
90
+ 'db:migrate:rollback': {
91
+ description: 'Rollback the most recent MySQL migration',
92
+ bootstrap: true,
93
+ run: async () => {
94
+ const config = (await import('stonyx/config')).default;
95
+ const mysqlConfig = config.orm.mysql;
96
+
97
+ if (!mysqlConfig) {
98
+ console.error('MySQL is not configured. Set MYSQL_HOST to enable MySQL mode.');
99
+ process.exit(1);
100
+ }
101
+
102
+ const { getPool, closePool } = await import('./mysql/connection.js');
103
+ const { ensureMigrationsTable, getAppliedMigrations, rollbackMigration, parseMigrationFile } = await import('./mysql/migration-runner.js');
104
+ const { readFile } = await import('@stonyx/utils/file');
105
+ const path = await import('path');
106
+
107
+ const pool = await getPool(mysqlConfig as MysqlConfig);
108
+ const migrationsPath = path.resolve(config.rootPath, mysqlConfig.migrationsDir as string);
109
+
110
+ try {
111
+ await ensureMigrationsTable(pool, mysqlConfig.migrationsTable as string);
112
+
113
+ const applied = await getAppliedMigrations(pool, mysqlConfig.migrationsTable as string);
114
+
115
+ if (applied.length === 0) {
116
+ console.log('No migrations to rollback.');
117
+ return;
118
+ }
119
+
120
+ const lastFilename = applied[applied.length - 1];
121
+ const content = await readFile(path.join(migrationsPath, lastFilename) as string) as string;
122
+ const { down } = parseMigrationFile(content);
123
+
124
+ if (!down) {
125
+ console.error(`No DOWN section found in ${lastFilename}. Cannot rollback.`);
126
+ process.exit(1);
127
+ }
128
+
129
+ await rollbackMigration(pool, lastFilename, down, mysqlConfig.migrationsTable as string);
130
+ console.log(`Rolled back: ${lastFilename}`);
131
+ } finally {
132
+ await closePool();
133
+ }
134
+ }
135
+ },
136
+ 'db:migrate:status': {
137
+ description: 'Show status of MySQL migrations',
138
+ bootstrap: true,
139
+ run: async () => {
140
+ const config = (await import('stonyx/config')).default;
141
+ const mysqlConfig = config.orm.mysql;
142
+
143
+ if (!mysqlConfig) {
144
+ console.error('MySQL is not configured. Set MYSQL_HOST to enable MySQL mode.');
145
+ process.exit(1);
146
+ }
147
+
148
+ const { getPool, closePool } = await import('./mysql/connection.js');
149
+ const { ensureMigrationsTable, getAppliedMigrations, getMigrationFiles } = await import('./mysql/migration-runner.js');
150
+ const path = await import('path');
151
+
152
+ const pool = await getPool(mysqlConfig as MysqlConfig);
153
+ const migrationsPath = path.resolve(config.rootPath, mysqlConfig.migrationsDir as string);
154
+
155
+ try {
156
+ await ensureMigrationsTable(pool, mysqlConfig.migrationsTable as string);
157
+
158
+ const applied = new Set(await getAppliedMigrations(pool, mysqlConfig.migrationsTable as string));
159
+ const files = await getMigrationFiles(migrationsPath);
160
+
161
+ if (files.length === 0) {
162
+ console.log('No migration files found.');
163
+ return;
164
+ }
165
+
166
+ console.log('Migration status:');
167
+
168
+ for (const filename of files) {
169
+ const status = applied.has(filename) ? 'applied' : 'pending';
170
+ console.log(` [${status}] ${filename}`);
171
+ }
172
+ } finally {
173
+ await closePool();
174
+ }
175
+ }
176
+ },
177
+ };
178
+
179
+ export default commands;
package/src/db.ts ADDED
@@ -0,0 +1,232 @@
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
+ import Cron from '@stonyx/cron';
18
+ import config from 'stonyx/config';
19
+ import log from 'stonyx/log';
20
+ import Orm, { store } from '@stonyx/orm';
21
+ import { createRecord } from './manage-record.js';
22
+ import { createFile, createDirectory, updateFile, readFile, fileExists } from '@stonyx/utils/file';
23
+ import path from 'path';
24
+
25
+ export const dbKey = '__db';
26
+
27
+ interface DBRecord {
28
+ format(): Record<string, unknown>;
29
+ [key: string]: unknown;
30
+ }
31
+
32
+ function asDBRecord(value: unknown): DBRecord {
33
+ if (typeof value !== 'object' || value === null || typeof (value as DBRecord).format !== 'function') {
34
+ throw new Error('createRecord did not return a valid DBRecord');
35
+ }
36
+ return value as DBRecord;
37
+ }
38
+
39
+ export default class DB {
40
+ static instance: DB;
41
+ record!: DBRecord;
42
+
43
+ constructor() {
44
+ if (DB.instance) return DB.instance;
45
+
46
+ DB.instance = this;
47
+ }
48
+
49
+ async getSchema(): Promise<unknown> {
50
+ const { rootPath } = config;
51
+ const { file, schema } = config.orm.db;
52
+
53
+ if (!file) throw new Error('Configuration Error: ORM DB file path must be defined.');
54
+
55
+ return (await import(`${rootPath}/${schema}`)).default;
56
+ }
57
+
58
+ getCollectionKeys(): string[] {
59
+ const SchemaClass = Orm.instance.models[`${dbKey}Model`] as new () => Record<string, unknown>;
60
+ const instance = new SchemaClass();
61
+ const keys: string[] = [];
62
+
63
+ for (const key of Object.keys(instance)) {
64
+ if (key === '__name' || key === 'id') continue;
65
+ if (typeof instance[key] === 'function') keys.push(key);
66
+ }
67
+
68
+ return keys;
69
+ }
70
+
71
+ getDirPath(): string {
72
+ const { rootPath } = config;
73
+ const { file, directory } = config.orm.db;
74
+ const dbDir = path.dirname(path.resolve(`${rootPath}/${file}`));
75
+
76
+ return path.join(dbDir, directory);
77
+ }
78
+
79
+ async validateMode(): Promise<void> {
80
+ const { rootPath } = config;
81
+ const { file, mode } = config.orm.db;
82
+ const collectionKeys = this.getCollectionKeys();
83
+ const dirPath = this.getDirPath();
84
+
85
+ if (mode === 'directory') {
86
+ const dbFilePath = path.resolve(`${rootPath}/${file}`);
87
+ const exists = await fileExists(dbFilePath);
88
+
89
+ if (exists) {
90
+ const data = await readFile(dbFilePath, { json: true }) as Record<string, unknown[]>;
91
+ const hasData = collectionKeys.some(key => Array.isArray(data[key]) && data[key].length > 0);
92
+
93
+ if (hasData) {
94
+ 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`);
95
+ process.exit(1);
96
+ }
97
+ }
98
+ } else {
99
+ const dirExists = await fileExists(dirPath);
100
+
101
+ if (dirExists) {
102
+ const hasCollectionFiles = (await Promise.all(
103
+ collectionKeys.map(key => fileExists(path.join(dirPath, `${key}.json`)))
104
+ )).some(Boolean);
105
+
106
+ if (hasCollectionFiles) {
107
+ 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`);
108
+ process.exit(1);
109
+ }
110
+ }
111
+ }
112
+ }
113
+
114
+ async init(): Promise<void> {
115
+ const { autosave, saveInterval } = config.orm.db;
116
+
117
+ store.set(dbKey, new Map());
118
+ (Orm.instance as Orm).models[`${dbKey}Model`] = await this.getSchema();
119
+
120
+ await this.validateMode();
121
+ this.record = await this.getRecord();
122
+
123
+ if (autosave !== 'true') return;
124
+
125
+ new Cron().register('save', this.save.bind(this), saveInterval);
126
+ }
127
+
128
+ async create(): Promise<Record<string, unknown>> {
129
+ const { rootPath } = config;
130
+ const { file, mode } = config.orm.db;
131
+
132
+ if (mode === 'directory') {
133
+ const dirPath = this.getDirPath();
134
+ const collectionKeys = this.getCollectionKeys();
135
+
136
+ await createDirectory(dirPath);
137
+
138
+ await Promise.all(collectionKeys.map(key =>
139
+ createFile(path.join(dirPath, `${key}.json`), [], { json: true })
140
+ ));
141
+
142
+ // Write empty-array skeleton to db.json
143
+ const skeleton: Record<string, unknown[]> = {};
144
+ for (const key of collectionKeys) skeleton[key] = [];
145
+
146
+ await createFile(`${rootPath}/${file}`, skeleton, { json: true });
147
+
148
+ return skeleton;
149
+ }
150
+
151
+ createFile(`${rootPath}/${file}`, {}, { json: true });
152
+
153
+ return {};
154
+ }
155
+
156
+ async save(): Promise<void> {
157
+ const { file, mode } = config.orm.db;
158
+ const jsonData = this.record.format() as Record<string, unknown>;
159
+ delete jsonData.id; // Don't save id
160
+
161
+ if (mode === 'directory') {
162
+ const dirPath = this.getDirPath();
163
+ const collectionKeys = this.getCollectionKeys();
164
+
165
+ // Write each collection to its own file in parallel
166
+ // Use createFile for new files, updateFile for existing ones
167
+ await Promise.all(collectionKeys.map(async key => {
168
+ const filePath = path.join(dirPath, `${key}.json`);
169
+ const exists = await fileExists(filePath);
170
+ const data = (jsonData[key] || []) as Record<string, unknown> | unknown[];
171
+
172
+ if (exists) await updateFile(filePath, data, { json: true });
173
+ else await createFile(filePath, data, { json: true });
174
+ }));
175
+
176
+ // Write empty-array skeleton to db.json
177
+ const skeleton: Record<string, unknown[]> = {};
178
+ for (const key of collectionKeys) skeleton[key] = [];
179
+
180
+ const dbFilePath = `${config.rootPath}/${file}`;
181
+ const dbFileExists = await fileExists(dbFilePath);
182
+
183
+ if (dbFileExists) await updateFile(dbFilePath, skeleton, { json: true });
184
+ else await createFile(dbFilePath, skeleton, { json: true });
185
+
186
+ log.db?.(`DB has been successfully saved to ${config.orm.db.directory}/ directory`);
187
+ return;
188
+ }
189
+
190
+ await updateFile(`${config.rootPath}/${file}`, jsonData, { json: true });
191
+
192
+ log.db?.(`DB has been successfully saved to ${file}`);
193
+ }
194
+
195
+ async getRecord(): Promise<DBRecord> {
196
+ const { mode } = config.orm.db;
197
+
198
+ if (mode === 'directory') return this.getRecordFromDirectory();
199
+
200
+ return this.getRecordFromFile();
201
+ }
202
+
203
+ async getRecordFromFile(): Promise<DBRecord> {
204
+ const { file } = config.orm.db;
205
+
206
+ const data = await readFile(file, { json: true, missingFileCallback: this.create.bind(this) });
207
+
208
+ return asDBRecord(createRecord(dbKey, data as Record<string, unknown>, { isDbRecord: true, serialize: false, transform: false }));
209
+ }
210
+
211
+ async getRecordFromDirectory(): Promise<DBRecord> {
212
+ const dirPath = this.getDirPath();
213
+ const collectionKeys = this.getCollectionKeys();
214
+ const dirExists = await fileExists(dirPath);
215
+
216
+ if (!dirExists) {
217
+ const data = await this.create();
218
+ return asDBRecord(createRecord(dbKey, data, { isDbRecord: true, serialize: false, transform: false }));
219
+ }
220
+
221
+ const assembled: Record<string, unknown> = {};
222
+
223
+ await Promise.all(collectionKeys.map(async key => {
224
+ const filePath = path.join(dirPath, `${key}.json`);
225
+ const exists = await fileExists(filePath);
226
+
227
+ assembled[key] = exists ? await readFile(filePath, { json: true }) : [];
228
+ }));
229
+
230
+ return asDBRecord(createRecord(dbKey, assembled, { isDbRecord: true, serialize: false, transform: false }));
231
+ }
232
+ }
@@ -0,0 +1,7 @@
1
+ import Orm from '@stonyx/orm';
2
+
3
+ const db = Orm.db as { record: unknown; save(): Promise<void> };
4
+
5
+ export default db;
6
+ export const data = db.record;
7
+ export const saveDB = db.save.bind(db);