webspresso 0.0.6 → 0.0.8

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.
package/bin/webspresso.js CHANGED
@@ -18,25 +18,113 @@ program
18
18
 
19
19
  // New project command
20
20
  program
21
- .command('new <project-name>')
21
+ .command('new [project-name]')
22
22
  .description('Create a new Webspresso project')
23
23
  .option('-t, --template <template>', 'Template to use (minimal, full)', 'minimal')
24
24
  .option('--no-tailwind', 'Skip Tailwind CSS setup')
25
25
  .option('-i, --install', 'Auto install dependencies and build CSS')
26
- .action(async (projectName, options) => {
26
+ .action(async (projectNameArg, options) => {
27
27
  const useTailwind = options.tailwind !== false;
28
28
  const autoInstall = options.install === true;
29
- const projectPath = path.resolve(projectName);
30
29
 
31
- if (fs.existsSync(projectPath)) {
32
- console.error(`❌ Directory ${projectName} already exists!`);
33
- process.exit(1);
30
+ let projectName;
31
+ let projectPath;
32
+ let useCurrentDir = false;
33
+
34
+ if (!projectNameArg) {
35
+ // No project name provided - ask if they want to use current directory
36
+ const currentDirName = path.basename(process.cwd());
37
+ const currentDirFiles = fs.readdirSync(process.cwd());
38
+ const hasExistingFiles = currentDirFiles.some(f => !f.startsWith('.'));
39
+
40
+ const { useCurrent } = await inquirer.prompt([
41
+ {
42
+ type: 'confirm',
43
+ name: 'useCurrent',
44
+ message: `Install in current directory (${currentDirName})?`,
45
+ default: true
46
+ }
47
+ ]);
48
+
49
+ if (useCurrent) {
50
+ useCurrentDir = true;
51
+ projectPath = process.cwd();
52
+
53
+ // Check for existing Webspresso files
54
+ if (fs.existsSync(path.join(projectPath, 'server.js')) ||
55
+ fs.existsSync(path.join(projectPath, 'pages'))) {
56
+ console.error('❌ Current directory already contains a Webspresso project!');
57
+ process.exit(1);
58
+ }
59
+
60
+ // Warn if there are existing files
61
+ if (hasExistingFiles) {
62
+ const { continueAnyway } = await inquirer.prompt([
63
+ {
64
+ type: 'confirm',
65
+ name: 'continueAnyway',
66
+ message: '⚠️ Current directory is not empty. Continue anyway?',
67
+ default: false
68
+ }
69
+ ]);
70
+
71
+ if (!continueAnyway) {
72
+ console.log('Cancelled.');
73
+ process.exit(0);
74
+ }
75
+ }
76
+
77
+ // Ask for project name (for package.json)
78
+ const { name } = await inquirer.prompt([
79
+ {
80
+ type: 'input',
81
+ name: 'name',
82
+ message: 'Project name:',
83
+ default: currentDirName,
84
+ validate: (input) => {
85
+ if (!input.trim()) return 'Project name is required';
86
+ if (!/^[a-z0-9-_]+$/i.test(input)) return 'Project name can only contain letters, numbers, hyphens, and underscores';
87
+ return true;
88
+ }
89
+ }
90
+ ]);
91
+
92
+ projectName = name;
93
+ } else {
94
+ // Ask for directory name
95
+ const { dirName } = await inquirer.prompt([
96
+ {
97
+ type: 'input',
98
+ name: 'dirName',
99
+ message: 'Project directory name:',
100
+ validate: (input) => {
101
+ if (!input.trim()) return 'Directory name is required';
102
+ if (!/^[a-z0-9-_]+$/i.test(input)) return 'Directory name can only contain letters, numbers, hyphens, and underscores';
103
+ if (fs.existsSync(path.resolve(input))) return `Directory ${input} already exists!`;
104
+ return true;
105
+ }
106
+ }
107
+ ]);
108
+
109
+ projectName = dirName;
110
+ projectPath = path.resolve(dirName);
111
+ }
112
+ } else {
113
+ projectName = projectNameArg;
114
+ projectPath = path.resolve(projectNameArg);
115
+
116
+ if (fs.existsSync(projectPath)) {
117
+ console.error(`❌ Directory ${projectName} already exists!`);
118
+ process.exit(1);
119
+ }
34
120
  }
35
121
 
36
122
  console.log(`\n🚀 Creating new Webspresso project: ${projectName}\n`);
37
123
 
38
- // Create directory structure
39
- fs.mkdirSync(projectPath, { recursive: true });
124
+ // Create directory structure (skip root if using current dir)
125
+ if (!useCurrentDir) {
126
+ fs.mkdirSync(projectPath, { recursive: true });
127
+ }
40
128
  fs.mkdirSync(path.join(projectPath, 'pages'), { recursive: true });
41
129
  fs.mkdirSync(path.join(projectPath, 'pages', 'locales'), { recursive: true });
42
130
  fs.mkdirSync(path.join(projectPath, 'views'), { recursive: true });
@@ -361,7 +449,9 @@ module.exports = {
361
449
 
362
450
  console.log('\n✅ Project ready!\n');
363
451
  console.log('Start developing:');
364
- console.log(` cd ${projectName}`);
452
+ if (!useCurrentDir) {
453
+ console.log(` cd ${projectName}`);
454
+ }
365
455
  console.log(' npm run dev\n');
366
456
  } catch (err) {
367
457
  console.error('❌ Installation failed:', err.message);
@@ -370,7 +460,9 @@ module.exports = {
370
460
  } else {
371
461
  console.log('\n✅ Project created successfully!\n');
372
462
  console.log('Next steps:');
373
- console.log(` cd ${projectName}`);
463
+ if (!useCurrentDir) {
464
+ console.log(` cd ${projectName}`);
465
+ }
374
466
  console.log(' npm install');
375
467
  if (useTailwind) {
376
468
  console.log(' npm run build:css');
@@ -785,6 +877,261 @@ module.exports = {
785
877
  }
786
878
  });
787
879
 
880
+ // ============================================================================
881
+ // Database Commands
882
+ // ============================================================================
883
+
884
+ /**
885
+ * Load database configuration
886
+ * @param {string} [configPath] - Custom config path
887
+ * @returns {Object} Database config
888
+ */
889
+ function loadDbConfig(configPath) {
890
+ const defaultPaths = ['webspresso.db.js', 'knexfile.js'];
891
+ const paths = configPath ? [configPath, ...defaultPaths] : defaultPaths;
892
+
893
+ for (const p of paths) {
894
+ const fullPath = path.resolve(process.cwd(), p);
895
+ if (fs.existsSync(fullPath)) {
896
+ return { config: require(fullPath), path: fullPath };
897
+ }
898
+ }
899
+
900
+ console.error('❌ Database config not found. Create webspresso.db.js or knexfile.js');
901
+ process.exit(1);
902
+ }
903
+
904
+ /**
905
+ * Create database instance from config
906
+ * @param {Object} config - Database config
907
+ * @param {string} [env] - Environment name
908
+ * @returns {Promise<Object>} Database instance
909
+ */
910
+ async function createDbInstance(config, env) {
911
+ const environment = env || process.env.NODE_ENV || 'development';
912
+ const dbConfig = config[environment] || config;
913
+
914
+ // Dynamic import knex
915
+ let knex;
916
+ try {
917
+ knex = require('knex');
918
+ } catch {
919
+ console.error('❌ Knex not installed. Run: npm install knex');
920
+ process.exit(1);
921
+ }
922
+
923
+ return knex(dbConfig);
924
+ }
925
+
926
+ // db:migrate command
927
+ program
928
+ .command('db:migrate')
929
+ .description('Run pending database migrations')
930
+ .option('-e, --env <environment>', 'Environment (development, production)', 'development')
931
+ .option('-c, --config <path>', 'Path to database config file')
932
+ .action(async (options) => {
933
+ const { config, path: configPath } = loadDbConfig(options.config);
934
+ console.log(`\n📦 Using config: ${configPath}`);
935
+ console.log(` Environment: ${options.env}\n`);
936
+
937
+ const knex = await createDbInstance(config, options.env);
938
+
939
+ try {
940
+ const migrationConfig = config.migrations || {};
941
+ const [batch, migrations] = await knex.migrate.latest(migrationConfig);
942
+
943
+ if (migrations.length === 0) {
944
+ console.log('✅ Already up to date.\n');
945
+ } else {
946
+ console.log(`Running migrations (batch ${batch}):`);
947
+ for (const m of migrations) {
948
+ console.log(` → ${m}`);
949
+ }
950
+ console.log(`\n✅ Done. ${migrations.length} migration(s) completed.\n`);
951
+ }
952
+ } catch (err) {
953
+ console.error('❌ Migration failed:', err.message);
954
+ process.exit(1);
955
+ } finally {
956
+ await knex.destroy();
957
+ }
958
+ });
959
+
960
+ // db:rollback command
961
+ program
962
+ .command('db:rollback')
963
+ .description('Rollback the last batch of migrations')
964
+ .option('-e, --env <environment>', 'Environment (development, production)', 'development')
965
+ .option('-c, --config <path>', 'Path to database config file')
966
+ .option('-a, --all', 'Rollback all migrations')
967
+ .action(async (options) => {
968
+ const { config, path: configPath } = loadDbConfig(options.config);
969
+ console.log(`\n📦 Using config: ${configPath}`);
970
+ console.log(` Environment: ${options.env}\n`);
971
+
972
+ const knex = await createDbInstance(config, options.env);
973
+
974
+ try {
975
+ const migrationConfig = {
976
+ ...(config.migrations || {}),
977
+ ...(options.all ? { all: true } : {}),
978
+ };
979
+
980
+ const [batch, migrations] = await knex.migrate.rollback(migrationConfig);
981
+
982
+ if (migrations.length === 0) {
983
+ console.log('✅ Nothing to rollback.\n');
984
+ } else {
985
+ console.log(`Rolling back${options.all ? ' all' : ''} migrations:`);
986
+ for (const m of migrations) {
987
+ console.log(` ← ${m}`);
988
+ }
989
+ console.log(`\n✅ Done. ${migrations.length} migration(s) rolled back.\n`);
990
+ }
991
+ } catch (err) {
992
+ console.error('❌ Rollback failed:', err.message);
993
+ process.exit(1);
994
+ } finally {
995
+ await knex.destroy();
996
+ }
997
+ });
998
+
999
+ // db:status command
1000
+ program
1001
+ .command('db:status')
1002
+ .description('Show migration status')
1003
+ .option('-e, --env <environment>', 'Environment (development, production)', 'development')
1004
+ .option('-c, --config <path>', 'Path to database config file')
1005
+ .action(async (options) => {
1006
+ const { config, path: configPath } = loadDbConfig(options.config);
1007
+ console.log(`\n📦 Using config: ${configPath}`);
1008
+ console.log(` Environment: ${options.env}\n`);
1009
+
1010
+ const knex = await createDbInstance(config, options.env);
1011
+
1012
+ try {
1013
+ const migrationConfig = config.migrations || {};
1014
+ const [completed, pending] = await knex.migrate.list(migrationConfig);
1015
+
1016
+ console.log('Migration Status');
1017
+ console.log('================\n');
1018
+
1019
+ // Sort all migrations by name
1020
+ const all = [
1021
+ ...completed.map(m => ({ name: m.name || m, completed: true })),
1022
+ ...pending.map(m => ({ name: m.name || m, completed: false })),
1023
+ ].sort((a, b) => a.name.localeCompare(b.name));
1024
+
1025
+ if (all.length === 0) {
1026
+ console.log(' No migrations found.\n');
1027
+ } else {
1028
+ for (const m of all) {
1029
+ const status = m.completed ? '✓' : '○';
1030
+ const suffix = m.completed ? '' : ' (pending)';
1031
+ console.log(` ${status} ${m.name}${suffix}`);
1032
+ }
1033
+ console.log(`\n Total: ${all.length} (${completed.length} completed, ${pending.length} pending)\n`);
1034
+ }
1035
+ } catch (err) {
1036
+ console.error('❌ Failed to get status:', err.message);
1037
+ process.exit(1);
1038
+ } finally {
1039
+ await knex.destroy();
1040
+ }
1041
+ });
1042
+
1043
+ // db:make command
1044
+ program
1045
+ .command('db:make <name>')
1046
+ .description('Create a new migration file')
1047
+ .option('-c, --config <path>', 'Path to database config file')
1048
+ .option('-m, --model <model>', 'Generate migration from model (requires models directory)')
1049
+ .action(async (name, options) => {
1050
+ const { config, path: configPath } = loadDbConfig(options.config);
1051
+ console.log(`\n📦 Using config: ${configPath}\n`);
1052
+
1053
+ const migrationDir = config.migrations?.directory || './migrations';
1054
+
1055
+ // Ensure migrations directory exists
1056
+ if (!fs.existsSync(migrationDir)) {
1057
+ fs.mkdirSync(migrationDir, { recursive: true });
1058
+ console.log(`Created directory: ${migrationDir}`);
1059
+ }
1060
+
1061
+ // Generate filename with timestamp
1062
+ const now = new Date();
1063
+ const timestamp = [
1064
+ now.getFullYear(),
1065
+ String(now.getMonth() + 1).padStart(2, '0'),
1066
+ String(now.getDate()).padStart(2, '0'),
1067
+ '_',
1068
+ String(now.getHours()).padStart(2, '0'),
1069
+ String(now.getMinutes()).padStart(2, '0'),
1070
+ String(now.getSeconds()).padStart(2, '0'),
1071
+ ].join('');
1072
+
1073
+ const filename = `${timestamp}_${name}.js`;
1074
+ const filepath = path.join(migrationDir, filename);
1075
+
1076
+ let content;
1077
+
1078
+ if (options.model) {
1079
+ // Try to load model and generate migration from schema
1080
+ const modelsDir = config.models || './models';
1081
+ const modelPath = path.resolve(process.cwd(), modelsDir, `${options.model}.js`);
1082
+
1083
+ if (fs.existsSync(modelPath)) {
1084
+ try {
1085
+ const model = require(modelPath);
1086
+ const { scaffoldMigration } = require('../core/orm/migrations/scaffold');
1087
+ content = scaffoldMigration(model);
1088
+ console.log(`Generated migration from model: ${options.model}`);
1089
+ } catch (err) {
1090
+ console.warn(`⚠️ Could not generate from model: ${err.message}`);
1091
+ console.log(' Creating empty migration instead.\n');
1092
+ content = getDefaultMigrationContent(name);
1093
+ }
1094
+ } else {
1095
+ console.warn(`⚠️ Model not found: ${modelPath}`);
1096
+ console.log(' Creating empty migration instead.\n');
1097
+ content = getDefaultMigrationContent(name);
1098
+ }
1099
+ } else {
1100
+ content = getDefaultMigrationContent(name);
1101
+ }
1102
+
1103
+ fs.writeFileSync(filepath, content);
1104
+ console.log(`✅ Created: ${filepath}\n`);
1105
+ });
1106
+
1107
+ /**
1108
+ * Get default migration content
1109
+ * @param {string} name - Migration name
1110
+ * @returns {string}
1111
+ */
1112
+ function getDefaultMigrationContent(name) {
1113
+ // Parse table name from migration name (e.g., create_users_table -> users)
1114
+ const match = name.match(/^create_(\w+)_table$/);
1115
+ const tableName = match ? match[1] : 'table_name';
1116
+
1117
+ return `/**
1118
+ * Migration: ${name}
1119
+ */
1120
+
1121
+ exports.up = function(knex) {
1122
+ return knex.schema.createTable('${tableName}', (table) => {
1123
+ table.bigIncrements('id').primary();
1124
+ // Add your columns here
1125
+ table.timestamps(true, true);
1126
+ });
1127
+ };
1128
+
1129
+ exports.down = function(knex) {
1130
+ return knex.schema.dropTableIfExists('${tableName}');
1131
+ };
1132
+ `;
1133
+ }
1134
+
788
1135
  // Parse arguments
789
1136
  program.parse();
790
1137
 
@@ -46,3 +46,4 @@ module.exports = {
46
46
  applySchema
47
47
  };
48
48
 
49
+
@@ -66,3 +66,4 @@ module.exports = {
66
66
  clearAllSchemas
67
67
  };
68
68
 
69
+
@@ -0,0 +1,232 @@
1
+ /**
2
+ * Webspresso ORM - Eager Loader
3
+ * Batch loading algorithm for relations
4
+ * @module core/orm/eager-loader
5
+ */
6
+
7
+ const { resolveRelationModel, getRelationKeys } = require('./model');
8
+ const { applyScopes, createScopeContext } = require('./scopes');
9
+
10
+ /**
11
+ * Load relations for a set of records using batch queries
12
+ * @param {Object[]} records - Records to load relations for
13
+ * @param {string[]} relationNames - Names of relations to load
14
+ * @param {import('./types').ModelDefinition} model - Model definition
15
+ * @param {import('knex').Knex|import('knex').Knex.Transaction} knex - Knex instance
16
+ * @param {import('./types').ScopeContext} [scopeContext] - Scope context
17
+ * @returns {Promise<Object[]>} Records with relations attached
18
+ */
19
+ async function loadRelations(records, relationNames, model, knex, scopeContext) {
20
+ if (!records.length || !relationNames.length) {
21
+ return records;
22
+ }
23
+
24
+ const context = scopeContext || createScopeContext();
25
+
26
+ // Process each relation
27
+ for (const relationName of relationNames) {
28
+ const relation = model.relations[relationName];
29
+ if (!relation) {
30
+ console.warn(`Relation "${relationName}" not found on model "${model.name}"`);
31
+ continue;
32
+ }
33
+
34
+ const { localKey, foreignKey, relatedModel } = getRelationKeys(model, relationName);
35
+
36
+ switch (relation.type) {
37
+ case 'belongsTo':
38
+ await loadBelongsTo(records, relationName, localKey, foreignKey, relatedModel, knex, context);
39
+ break;
40
+ case 'hasMany':
41
+ await loadHasMany(records, relationName, localKey, foreignKey, relatedModel, knex, context);
42
+ break;
43
+ case 'hasOne':
44
+ await loadHasOne(records, relationName, localKey, foreignKey, relatedModel, knex, context);
45
+ break;
46
+ }
47
+ }
48
+
49
+ return records;
50
+ }
51
+
52
+ /**
53
+ * Load belongsTo relation
54
+ * Parent record has foreign key pointing to related record's primary key
55
+ * Example: User belongsTo Company (user.company_id -> company.id)
56
+ *
57
+ * @param {Object[]} records - Parent records
58
+ * @param {string} relationName - Relation name
59
+ * @param {string} localKey - Local primary key (typically 'id')
60
+ * @param {string} foreignKey - Foreign key on parent (e.g., 'company_id')
61
+ * @param {import('./types').ModelDefinition} relatedModel - Related model
62
+ * @param {import('knex').Knex} knex - Knex instance
63
+ * @param {import('./types').ScopeContext} scopeContext - Scope context
64
+ */
65
+ async function loadBelongsTo(records, relationName, localKey, foreignKey, relatedModel, knex, scopeContext) {
66
+ // Collect unique foreign key values
67
+ const foreignKeyValues = [...new Set(
68
+ records
69
+ .map(r => r[foreignKey])
70
+ .filter(v => v !== null && v !== undefined)
71
+ )];
72
+
73
+ if (foreignKeyValues.length === 0) {
74
+ // No foreign keys, set all relations to null
75
+ for (const record of records) {
76
+ record[relationName] = null;
77
+ }
78
+ return;
79
+ }
80
+
81
+ // Build query for related records
82
+ let qb = knex(relatedModel.table).whereIn(relatedModel.primaryKey, foreignKeyValues);
83
+
84
+ // Apply scopes to related model
85
+ qb = applyScopes(qb, scopeContext, relatedModel);
86
+
87
+ const relatedRecords = await qb;
88
+
89
+ // Index related records by primary key
90
+ const relatedMap = new Map();
91
+ for (const related of relatedRecords) {
92
+ relatedMap.set(related[relatedModel.primaryKey], related);
93
+ }
94
+
95
+ // Attach related records to parent records
96
+ for (const record of records) {
97
+ const fkValue = record[foreignKey];
98
+ record[relationName] = fkValue !== null && fkValue !== undefined
99
+ ? relatedMap.get(fkValue) || null
100
+ : null;
101
+ }
102
+ }
103
+
104
+ /**
105
+ * Load hasMany relation
106
+ * Related records have foreign key pointing to parent record's primary key
107
+ * Example: User hasMany Posts (post.user_id -> user.id)
108
+ *
109
+ * @param {Object[]} records - Parent records
110
+ * @param {string} relationName - Relation name
111
+ * @param {string} localKey - Local primary key (typically 'id')
112
+ * @param {string} foreignKey - Foreign key on related records (e.g., 'user_id')
113
+ * @param {import('./types').ModelDefinition} relatedModel - Related model
114
+ * @param {import('knex').Knex} knex - Knex instance
115
+ * @param {import('./types').ScopeContext} scopeContext - Scope context
116
+ */
117
+ async function loadHasMany(records, relationName, localKey, foreignKey, relatedModel, knex, scopeContext) {
118
+ // Collect unique primary key values from parent records
119
+ const primaryKeyValues = [...new Set(
120
+ records
121
+ .map(r => r[localKey])
122
+ .filter(v => v !== null && v !== undefined)
123
+ )];
124
+
125
+ if (primaryKeyValues.length === 0) {
126
+ // No primary keys, set all relations to empty arrays
127
+ for (const record of records) {
128
+ record[relationName] = [];
129
+ }
130
+ return;
131
+ }
132
+
133
+ // Build query for related records
134
+ let qb = knex(relatedModel.table).whereIn(foreignKey, primaryKeyValues);
135
+
136
+ // Apply scopes to related model
137
+ qb = applyScopes(qb, scopeContext, relatedModel);
138
+
139
+ const relatedRecords = await qb;
140
+
141
+ // Group related records by foreign key
142
+ const relatedGroups = new Map();
143
+ for (const related of relatedRecords) {
144
+ const fkValue = related[foreignKey];
145
+ if (!relatedGroups.has(fkValue)) {
146
+ relatedGroups.set(fkValue, []);
147
+ }
148
+ relatedGroups.get(fkValue).push(related);
149
+ }
150
+
151
+ // Attach related records to parent records
152
+ for (const record of records) {
153
+ const pkValue = record[localKey];
154
+ record[relationName] = relatedGroups.get(pkValue) || [];
155
+ }
156
+ }
157
+
158
+ /**
159
+ * Load hasOne relation
160
+ * Related record has foreign key pointing to parent record's primary key
161
+ * Returns single related record (or null)
162
+ *
163
+ * @param {Object[]} records - Parent records
164
+ * @param {string} relationName - Relation name
165
+ * @param {string} localKey - Local primary key (typically 'id')
166
+ * @param {string} foreignKey - Foreign key on related record
167
+ * @param {import('./types').ModelDefinition} relatedModel - Related model
168
+ * @param {import('knex').Knex} knex - Knex instance
169
+ * @param {import('./types').ScopeContext} scopeContext - Scope context
170
+ */
171
+ async function loadHasOne(records, relationName, localKey, foreignKey, relatedModel, knex, scopeContext) {
172
+ // Collect unique primary key values from parent records
173
+ const primaryKeyValues = [...new Set(
174
+ records
175
+ .map(r => r[localKey])
176
+ .filter(v => v !== null && v !== undefined)
177
+ )];
178
+
179
+ if (primaryKeyValues.length === 0) {
180
+ // No primary keys, set all relations to null
181
+ for (const record of records) {
182
+ record[relationName] = null;
183
+ }
184
+ return;
185
+ }
186
+
187
+ // Build query for related records
188
+ let qb = knex(relatedModel.table).whereIn(foreignKey, primaryKeyValues);
189
+
190
+ // Apply scopes to related model
191
+ qb = applyScopes(qb, scopeContext, relatedModel);
192
+
193
+ const relatedRecords = await qb;
194
+
195
+ // Index related records by foreign key (first occurrence wins for hasOne)
196
+ const relatedMap = new Map();
197
+ for (const related of relatedRecords) {
198
+ const fkValue = related[foreignKey];
199
+ if (!relatedMap.has(fkValue)) {
200
+ relatedMap.set(fkValue, related);
201
+ }
202
+ }
203
+
204
+ // Attach related records to parent records
205
+ for (const record of records) {
206
+ const pkValue = record[localKey];
207
+ record[relationName] = relatedMap.get(pkValue) || null;
208
+ }
209
+ }
210
+
211
+ /**
212
+ * Load a single relation for a single record
213
+ * @param {Object} record - Record to load relation for
214
+ * @param {string} relationName - Name of relation to load
215
+ * @param {import('./types').ModelDefinition} model - Model definition
216
+ * @param {import('knex').Knex} knex - Knex instance
217
+ * @param {import('./types').ScopeContext} [scopeContext] - Scope context
218
+ * @returns {Promise<Object>} Record with relation attached
219
+ */
220
+ async function loadRelation(record, relationName, model, knex, scopeContext) {
221
+ const result = await loadRelations([record], [relationName], model, knex, scopeContext);
222
+ return result[0];
223
+ }
224
+
225
+ module.exports = {
226
+ loadRelations,
227
+ loadRelation,
228
+ loadBelongsTo,
229
+ loadHasMany,
230
+ loadHasOne,
231
+ };
232
+