@stonyx/orm 0.2.1-alpha.3 → 0.2.1-alpha.31

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,93 @@
1
+ export class AggregateProperty {
2
+ constructor(aggregateType, relationship, field) {
3
+ this.aggregateType = aggregateType;
4
+ this.relationship = relationship;
5
+ this.field = field;
6
+ this.mysqlFunction = aggregateType.toUpperCase();
7
+ this.resultType = aggregateType === 'avg' ? 'float' : 'number';
8
+ }
9
+
10
+ compute(relatedRecords) {
11
+ if (!relatedRecords || !Array.isArray(relatedRecords) || relatedRecords.length === 0) {
12
+ if (this.aggregateType === 'min' || this.aggregateType === 'max') return null;
13
+ return 0;
14
+ }
15
+
16
+ switch (this.aggregateType) {
17
+ case 'count':
18
+ return relatedRecords.length;
19
+
20
+ case 'sum':
21
+ return relatedRecords.reduce((acc, record) => {
22
+ const val = parseFloat(record?.__data?.[this.field] ?? record?.[this.field]);
23
+ return acc + (isNaN(val) ? 0 : val);
24
+ }, 0);
25
+
26
+ case 'avg': {
27
+ let sum = 0;
28
+ let count = 0;
29
+ for (const record of relatedRecords) {
30
+ const val = parseFloat(record?.__data?.[this.field] ?? record?.[this.field]);
31
+ if (!isNaN(val)) {
32
+ sum += val;
33
+ count++;
34
+ }
35
+ }
36
+ return count === 0 ? 0 : sum / count;
37
+ }
38
+
39
+ case 'min': {
40
+ let min = null;
41
+ for (const record of relatedRecords) {
42
+ const val = parseFloat(record?.__data?.[this.field] ?? record?.[this.field]);
43
+ if (!isNaN(val) && (min === null || val < min)) min = val;
44
+ }
45
+ return min;
46
+ }
47
+
48
+ case 'max': {
49
+ let max = null;
50
+ for (const record of relatedRecords) {
51
+ const val = parseFloat(record?.__data?.[this.field] ?? record?.[this.field]);
52
+ if (!isNaN(val) && (max === null || val > max)) max = val;
53
+ }
54
+ return max;
55
+ }
56
+
57
+ default:
58
+ return null;
59
+ }
60
+ }
61
+ }
62
+
63
+ export function count(relationship) {
64
+ return new AggregateProperty('count', relationship);
65
+ }
66
+
67
+ export function avg(relationshipOrField, field) {
68
+ if (field !== undefined) {
69
+ return new AggregateProperty('avg', relationshipOrField, field);
70
+ }
71
+ return new AggregateProperty('avg', undefined, relationshipOrField);
72
+ }
73
+
74
+ export function sum(relationshipOrField, field) {
75
+ if (field !== undefined) {
76
+ return new AggregateProperty('sum', relationshipOrField, field);
77
+ }
78
+ return new AggregateProperty('sum', undefined, relationshipOrField);
79
+ }
80
+
81
+ export function min(relationshipOrField, field) {
82
+ if (field !== undefined) {
83
+ return new AggregateProperty('min', relationshipOrField, field);
84
+ }
85
+ return new AggregateProperty('min', undefined, relationshipOrField);
86
+ }
87
+
88
+ export function max(relationshipOrField, field) {
89
+ if (field !== undefined) {
90
+ return new AggregateProperty('max', relationshipOrField, field);
91
+ }
92
+ return new AggregateProperty('max', undefined, relationshipOrField);
93
+ }
package/src/belongs-to.js CHANGED
@@ -11,7 +11,7 @@ export default function belongsTo(modelName) {
11
11
  const pendingHasManyQueue = relationships.get('pending');
12
12
  const pendingBelongsToQueue = relationships.get('pendingBelongsTo');
13
13
 
14
- return (sourceRecord, rawData, options) => {
14
+ const fn = (sourceRecord, rawData, options) => {
15
15
  if (!rawData) return null;
16
16
 
17
17
  const { __name: sourceModelName } = sourceRecord.__model;
@@ -60,4 +60,7 @@ export default function belongsTo(modelName) {
60
60
 
61
61
  return output;
62
62
  }
63
+
64
+ Object.defineProperty(fn, '__relatedModelName', { value: modelName });
65
+ return fn;
63
66
  }
package/src/cli.js ADDED
@@ -0,0 +1,177 @@
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
+ const USAGE = `Usage: stonyx-orm <command> [options]
28
+
29
+ Commands:
30
+ create <collection> <json-data> Create a record
31
+ list <collection> List all records
32
+ get <collection> <id> Get a record by ID
33
+ delete <collection> <id> Delete a record by ID
34
+
35
+ Options:
36
+ --config <path> Path to JSON config file
37
+ --help Show this help message
38
+
39
+ Environment variables:
40
+ DB_MODE 'file' or 'directory' (default: 'directory')
41
+ DB_PATH Path to db.json (default: 'db.json')
42
+ DB_DIRECTORY Directory name for collection files (default: 'db')`;
43
+
44
+ async function loadConfig(args) {
45
+ const config = {};
46
+
47
+ // Check for --config flag
48
+ const configIndex = args.indexOf('--config');
49
+
50
+ if (configIndex !== -1 && args[configIndex + 1]) {
51
+ const configPath = args[configIndex + 1];
52
+
53
+ try {
54
+ const content = await fs.readFile(configPath, 'utf-8');
55
+ Object.assign(config, JSON.parse(content));
56
+ } catch (err) {
57
+ console.error(`Error reading config file '${configPath}': ${err.message}`);
58
+ process.exit(1);
59
+ }
60
+
61
+ // Remove --config and its value from args
62
+ args.splice(configIndex, 2);
63
+ }
64
+
65
+ // Environment variables override config file, config file overrides defaults
66
+ return {
67
+ mode: process.env.DB_MODE || config.mode || 'directory',
68
+ dbPath: process.env.DB_PATH || config.dbPath || 'db.json',
69
+ directory: process.env.DB_DIRECTORY || config.directory || 'db',
70
+ };
71
+ }
72
+
73
+ function parseArgs(argv) {
74
+ // Strip node binary and script path
75
+ const args = argv.slice(2);
76
+
77
+ if (args.includes('--help') || args.includes('-h') || args.length === 0) {
78
+ console.log(USAGE);
79
+ process.exit(0);
80
+ }
81
+
82
+ return args;
83
+ }
84
+
85
+ async function run() {
86
+ const args = parseArgs(process.argv);
87
+ const config = await loadConfig(args);
88
+ const db = new StandaloneDB(config);
89
+
90
+ const [command, collection, ...rest] = args;
91
+
92
+ if (!command) {
93
+ console.error('Error: No command specified.\n');
94
+ console.log(USAGE);
95
+ process.exit(1);
96
+ }
97
+
98
+ if (!collection && command !== '--help') {
99
+ console.error(`Error: No collection specified for '${command}' command.\n`);
100
+ console.log(USAGE);
101
+ process.exit(1);
102
+ }
103
+
104
+ try {
105
+ switch (command) {
106
+ case 'list': {
107
+ const records = await db.list(collection);
108
+ console.log(JSON.stringify(records, null, 2));
109
+ break;
110
+ }
111
+
112
+ case 'get': {
113
+ const id = rest[0];
114
+
115
+ if (!id) {
116
+ console.error("Error: 'get' command requires an <id> argument.");
117
+ process.exit(1);
118
+ }
119
+
120
+ const record = await db.get(collection, id);
121
+
122
+ if (!record) {
123
+ console.error(`Record with id '${id}' not found in '${collection}'.`);
124
+ process.exit(1);
125
+ }
126
+
127
+ console.log(JSON.stringify(record, null, 2));
128
+ break;
129
+ }
130
+
131
+ case 'create': {
132
+ const jsonStr = rest.join(' ');
133
+
134
+ if (!jsonStr) {
135
+ console.error("Error: 'create' command requires <json-data> argument.");
136
+ process.exit(1);
137
+ }
138
+
139
+ let data;
140
+
141
+ try {
142
+ data = JSON.parse(jsonStr);
143
+ } catch {
144
+ console.error(`Error: Invalid JSON data: ${jsonStr}`);
145
+ process.exit(1);
146
+ }
147
+
148
+ const created = await db.create(collection, data);
149
+ console.log(JSON.stringify(created, null, 2));
150
+ break;
151
+ }
152
+
153
+ case 'delete': {
154
+ const deleteId = rest[0];
155
+
156
+ if (!deleteId) {
157
+ console.error("Error: 'delete' command requires an <id> argument.");
158
+ process.exit(1);
159
+ }
160
+
161
+ const removed = await db.delete(collection, deleteId);
162
+ console.log(JSON.stringify(removed, null, 2));
163
+ break;
164
+ }
165
+
166
+ default:
167
+ console.error(`Error: Unknown command '${command}'.\n`);
168
+ console.log(USAGE);
169
+ process.exit(1);
170
+ }
171
+ } catch (err) {
172
+ console.error(`Error: ${err.message}`);
173
+ process.exit(1);
174
+ }
175
+ }
176
+
177
+ run();
@@ -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,106 @@ 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
+ // Use createFile for new files, updateFile for existing ones
151
+ await Promise.all(collectionKeys.map(async key => {
152
+ const filePath = path.join(dirPath, `${key}.json`);
153
+ const exists = await fileExists(filePath);
154
+ const data = jsonData[key] || [];
155
+
156
+ if (exists) await updateFile(filePath, data, { json: true });
157
+ else await createFile(filePath, data, { json: true });
158
+ }));
159
+
160
+ // Write empty-array skeleton to db.json
161
+ const skeleton = {};
162
+ for (const key of collectionKeys) skeleton[key] = [];
163
+
164
+ const dbFilePath = `${config.rootPath}/${file}`;
165
+ const dbFileExists = await fileExists(dbFilePath);
166
+
167
+ if (dbFileExists) await updateFile(dbFilePath, skeleton, { json: true });
168
+ else await createFile(dbFilePath, skeleton, { json: true });
169
+
170
+ log.db(`DB has been successfully saved to ${config.orm.db.directory}/ directory`);
171
+ return;
172
+ }
173
+
68
174
  await updateFile(`${config.rootPath}/${file}`, jsonData, { json: true });
69
175
 
70
176
  log.db(`DB has been successfully saved to ${file}`);
71
177
  }
72
178
 
73
179
  async getRecord() {
180
+ const { mode } = config.orm.db;
181
+
182
+ if (mode === 'directory') return this.getRecordFromDirectory();
183
+
184
+ return this.getRecordFromFile();
185
+ }
186
+
187
+ async getRecordFromFile() {
74
188
  const { file } = config.orm.db;
75
189
 
76
190
  const data = await readFile(file, { json: true, missingFileCallback: this.create.bind(this) });
77
191
 
78
192
  return createRecord(dbKey, data, { isDbRecord: true, serialize: false, transform: false });
79
193
  }
194
+
195
+ async getRecordFromDirectory() {
196
+ const dirPath = this.getDirPath();
197
+ const collectionKeys = this.getCollectionKeys();
198
+ const dirExists = await fileExists(dirPath);
199
+
200
+ if (!dirExists) {
201
+ const data = await this.create();
202
+ return createRecord(dbKey, data, { isDbRecord: true, serialize: false, transform: false });
203
+ }
204
+
205
+ const assembled = {};
206
+
207
+ await Promise.all(collectionKeys.map(async key => {
208
+ const filePath = path.join(dirPath, `${key}.json`);
209
+ const exists = await fileExists(filePath);
210
+
211
+ assembled[key] = exists ? await readFile(filePath, { json: true }) : [];
212
+ }));
213
+
214
+ return createRecord(dbKey, assembled, { isDbRecord: true, serialize: false, transform: false });
215
+ }
80
216
  }
package/src/has-many.js CHANGED
@@ -16,7 +16,7 @@ export default function hasMany(modelName) {
16
16
  const globalRelationships = relationships.get('global');
17
17
  const pendingRelationships = relationships.get('pending');
18
18
 
19
- return (sourceRecord, rawData, options) => {
19
+ const fn = (sourceRecord, rawData, options) => {
20
20
  const { __name: sourceModelName } = sourceRecord.__model;
21
21
  const relationshipId = sourceRecord.id;
22
22
  const relationship = getRelationships('hasMany', sourceModelName, modelName, relationshipId);
@@ -58,4 +58,7 @@ export default function hasMany(modelName) {
58
58
 
59
59
  return output;
60
60
  }
61
+
62
+ Object.defineProperty(fn, '__relatedModelName', { value: modelName });
63
+ return fn;
61
64
  }