noormme 1.0.0 → 1.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (137) hide show
  1. package/LICENSE +89 -21
  2. package/README.md +195 -578
  3. package/dist/cjs/cli/commands/analyze.js +4 -1
  4. package/dist/cjs/cli/commands/generate.js +48 -8
  5. package/dist/cjs/cli/commands/init.js +54 -14
  6. package/dist/cjs/cli/commands/inspect.js +10 -3
  7. package/dist/cjs/cli/commands/migrate.js +38 -2
  8. package/dist/cjs/cli/commands/optimize.js +4 -1
  9. package/dist/cjs/cli/commands/status.js +41 -5
  10. package/dist/cjs/cli/commands/watch.js +4 -1
  11. package/dist/cjs/cli/index.js +4 -1
  12. package/dist/cjs/dialect/database-introspector.js +16 -21
  13. package/dist/cjs/dialect/sqlite/sqlite-introspector.js +13 -24
  14. package/dist/cjs/dynamic/dynamic.d.ts +20 -0
  15. package/dist/cjs/dynamic/dynamic.js +25 -0
  16. package/dist/cjs/edge-runtime/edge-config.d.ts +125 -0
  17. package/dist/cjs/edge-runtime/edge-config.js +323 -0
  18. package/dist/cjs/errors/NoormError.d.ts +27 -4
  19. package/dist/cjs/errors/NoormError.js +142 -21
  20. package/dist/cjs/logging/logger.d.ts +7 -2
  21. package/dist/cjs/logging/logger.js +21 -7
  22. package/dist/cjs/noormme.d.ts +12 -8
  23. package/dist/cjs/noormme.js +133 -61
  24. package/dist/cjs/operation-node/column-node.js +4 -0
  25. package/dist/cjs/operation-node/identifier-node.js +4 -0
  26. package/dist/cjs/operation-node/table-node.js +7 -0
  27. package/dist/cjs/parser/reference-parser.js +5 -0
  28. package/dist/cjs/parser/table-parser.js +2 -0
  29. package/dist/cjs/performance/index.d.ts +44 -0
  30. package/dist/cjs/performance/index.js +64 -0
  31. package/dist/cjs/performance/query-optimizer.d.ts +134 -0
  32. package/dist/cjs/performance/query-optimizer.js +391 -0
  33. package/dist/cjs/performance/services/cache-service.d.ts +177 -0
  34. package/dist/cjs/performance/services/cache-service.js +415 -0
  35. package/dist/cjs/performance/services/connection-factory.d.ts +198 -0
  36. package/dist/cjs/performance/services/connection-factory.js +498 -0
  37. package/dist/cjs/performance/services/metrics-collector.d.ts +162 -0
  38. package/dist/cjs/performance/services/metrics-collector.js +406 -0
  39. package/dist/cjs/performance/utils/query-parser.d.ts +123 -0
  40. package/dist/cjs/performance/utils/query-parser.js +295 -0
  41. package/dist/cjs/raw-builder/sql.d.ts +73 -26
  42. package/dist/cjs/raw-builder/sql.js +9 -0
  43. package/dist/cjs/repository/repository-factory.d.ts +10 -42
  44. package/dist/cjs/repository/repository-factory.js +276 -394
  45. package/dist/cjs/schema/core/coordinators/schema-discovery.coordinator.js +6 -4
  46. package/dist/cjs/schema/core/factories/discovery-factory.js +5 -5
  47. package/dist/cjs/schema/core/utils/name-generator.js +34 -2
  48. package/dist/cjs/schema/core/utils/type-mapper.d.ts +19 -14
  49. package/dist/cjs/schema/core/utils/type-mapper.js +4 -7
  50. package/dist/cjs/schema/dialects/sqlite/discovery/sqlite-constraint-discovery.js +3 -2
  51. package/dist/cjs/schema/dialects/sqlite/sqlite-discovery.coordinator.d.ts +2 -0
  52. package/dist/cjs/schema/dialects/sqlite/sqlite-discovery.coordinator.js +19 -5
  53. package/dist/cjs/schema/test/dialect-capabilities.test.js +6 -0
  54. package/dist/cjs/schema/test/error-handling.test.js +52 -33
  55. package/dist/cjs/schema/test/integration.test.js +51 -5
  56. package/dist/cjs/schema/test/sqlite-discovery-coordinator.test.js +8 -4
  57. package/dist/cjs/sqlite-migration/index.js +35 -2
  58. package/dist/cjs/testing/test-utils.d.ts +5 -0
  59. package/dist/cjs/testing/test-utils.js +66 -6
  60. package/dist/cjs/types/index.d.ts +78 -13
  61. package/dist/cjs/types/index.js +23 -0
  62. package/dist/cjs/types/type-generator.d.ts +8 -0
  63. package/dist/cjs/types/type-generator.js +86 -17
  64. package/dist/cjs/util/safe-sql-helpers.d.ts +124 -0
  65. package/dist/cjs/util/safe-sql-helpers.js +221 -0
  66. package/dist/cjs/util/security-validator.d.ts +44 -0
  67. package/dist/cjs/util/security-validator.js +256 -0
  68. package/dist/cjs/util/security.d.ts +60 -0
  69. package/dist/cjs/util/security.js +137 -0
  70. package/dist/cjs/watch/schema-watcher.js +26 -7
  71. package/dist/esm/cli/commands/generate.js +10 -6
  72. package/dist/esm/cli/commands/init.js +15 -11
  73. package/dist/esm/cli/commands/inspect.js +6 -2
  74. package/dist/esm/cli/commands/status.js +3 -3
  75. package/dist/esm/dialect/database-introspector.js +16 -21
  76. package/dist/esm/dialect/sqlite/sqlite-introspector.js +13 -24
  77. package/dist/esm/dynamic/dynamic.d.ts +20 -0
  78. package/dist/esm/dynamic/dynamic.js +25 -0
  79. package/dist/esm/edge-runtime/edge-config.d.ts +125 -0
  80. package/dist/esm/edge-runtime/edge-config.js +281 -0
  81. package/dist/esm/errors/NoormError.d.ts +27 -4
  82. package/dist/esm/errors/NoormError.js +134 -18
  83. package/dist/esm/logging/logger.d.ts +7 -2
  84. package/dist/esm/logging/logger.js +21 -7
  85. package/dist/esm/noormme.d.ts +12 -8
  86. package/dist/esm/noormme.js +130 -61
  87. package/dist/esm/operation-node/column-node.js +4 -0
  88. package/dist/esm/operation-node/identifier-node.js +4 -0
  89. package/dist/esm/operation-node/table-node.js +7 -0
  90. package/dist/esm/parser/reference-parser.js +5 -0
  91. package/dist/esm/parser/table-parser.js +2 -0
  92. package/dist/esm/performance/index.d.ts +44 -0
  93. package/dist/esm/performance/index.js +48 -0
  94. package/dist/esm/performance/query-optimizer.d.ts +134 -0
  95. package/dist/esm/performance/query-optimizer.js +387 -0
  96. package/dist/esm/performance/services/cache-service.d.ts +177 -0
  97. package/dist/esm/performance/services/cache-service.js +410 -0
  98. package/dist/esm/performance/services/connection-factory.d.ts +198 -0
  99. package/dist/esm/performance/services/connection-factory.js +493 -0
  100. package/dist/esm/performance/services/metrics-collector.d.ts +162 -0
  101. package/dist/esm/performance/services/metrics-collector.js +402 -0
  102. package/dist/esm/performance/utils/query-parser.d.ts +123 -0
  103. package/dist/esm/performance/utils/query-parser.js +292 -0
  104. package/dist/esm/raw-builder/sql.d.ts +73 -26
  105. package/dist/esm/raw-builder/sql.js +9 -0
  106. package/dist/esm/repository/repository-factory.d.ts +10 -42
  107. package/dist/esm/repository/repository-factory.js +277 -395
  108. package/dist/esm/schema/core/coordinators/schema-discovery.coordinator.js +6 -4
  109. package/dist/esm/schema/core/factories/discovery-factory.js +5 -5
  110. package/dist/esm/schema/core/utils/name-generator.js +34 -2
  111. package/dist/esm/schema/core/utils/type-mapper.d.ts +19 -14
  112. package/dist/esm/schema/core/utils/type-mapper.js +4 -7
  113. package/dist/esm/schema/dialects/sqlite/discovery/sqlite-constraint-discovery.js +3 -2
  114. package/dist/esm/schema/dialects/sqlite/sqlite-discovery.coordinator.d.ts +2 -0
  115. package/dist/esm/schema/dialects/sqlite/sqlite-discovery.coordinator.js +19 -5
  116. package/dist/esm/schema/test/dialect-capabilities.test.js +6 -0
  117. package/dist/esm/schema/test/error-handling.test.js +52 -33
  118. package/dist/esm/schema/test/integration.test.js +18 -5
  119. package/dist/esm/schema/test/sqlite-discovery-coordinator.test.js +8 -4
  120. package/dist/esm/testing/test-utils.d.ts +5 -0
  121. package/dist/esm/testing/test-utils.js +66 -6
  122. package/dist/esm/types/index.d.ts +78 -13
  123. package/dist/esm/types/index.js +22 -1
  124. package/dist/esm/types/type-generator.d.ts +8 -0
  125. package/dist/esm/types/type-generator.js +86 -17
  126. package/dist/esm/util/safe-sql-helpers.d.ts +124 -0
  127. package/dist/esm/util/safe-sql-helpers.js +209 -0
  128. package/dist/esm/util/security-validator.d.ts +44 -0
  129. package/dist/esm/util/security-validator.js +246 -0
  130. package/dist/esm/util/security.d.ts +60 -0
  131. package/dist/esm/util/security.js +114 -0
  132. package/dist/esm/watch/schema-watcher.js +26 -7
  133. package/package.json +1 -1
  134. package/dist/cjs/performance/query-analyzer.d.ts +0 -89
  135. package/dist/cjs/performance/query-analyzer.js +0 -263
  136. package/dist/esm/performance/query-analyzer.d.ts +0 -89
  137. package/dist/esm/performance/query-analyzer.js +0 -260
@@ -1,446 +1,328 @@
1
1
  /// <reference types="./repository-factory.d.ts" />
2
- import { NoormError, RelationshipNotFoundError } from '../errors/NoormError.js';
3
- import { wrapKyselyError } from '../utils/errorHelpers.js';
2
+ import { RelationshipNotFoundError, ColumnNotFoundError } from '../errors/NoormError.js';
4
3
  /**
5
- * Repository factory that creates dynamic repository classes
4
+ * Simple repository factory for creating table repositories
6
5
  */
7
6
  export class RepositoryFactory {
8
7
  db;
9
- config;
10
- constructor(db, config = {}) {
8
+ performanceConfig;
9
+ constructor(db, performanceConfig) {
11
10
  this.db = db;
12
- this.config = config;
11
+ this.performanceConfig = performanceConfig;
12
+ }
13
+ /**
14
+ * Transform boolean columns from SQLite integers (0/1) to JavaScript booleans
15
+ */
16
+ transformBooleans(data, table) {
17
+ // Find all boolean columns
18
+ const booleanColumns = table.columns
19
+ .filter(col => col.type === 'boolean')
20
+ .map(col => col.name);
21
+ if (booleanColumns.length === 0) {
22
+ return data;
23
+ }
24
+ const transformRecord = (record) => {
25
+ if (!record || typeof record !== 'object') {
26
+ return record;
27
+ }
28
+ const transformed = { ...record };
29
+ for (const col of booleanColumns) {
30
+ if (col in transformed) {
31
+ // Convert 0/1 to false/true
32
+ transformed[col] = Boolean(transformed[col]);
33
+ }
34
+ }
35
+ return transformed;
36
+ };
37
+ if (Array.isArray(data)) {
38
+ return data.map(transformRecord);
39
+ }
40
+ return transformRecord(data);
13
41
  }
14
42
  /**
15
43
  * Create a repository for the specified table
16
44
  */
17
45
  createRepository(table, relationships) {
46
+ // Determine the primary key column name
47
+ const primaryKey = table.columns.find(c => c.isPrimaryKey)?.name || 'id';
18
48
  const repository = {
19
- // Basic CRUD operations
20
- findById: async (id) => {
21
- try {
22
- const result = await this.db
49
+ // Django-style objects wrapper
50
+ objects: {
51
+ all: async () => {
52
+ return await this.db
23
53
  .selectFrom(table.name)
24
54
  .selectAll()
25
- .where(this.getPrimaryKeyCondition(table, id))
26
- .executeTakeFirst();
27
- return result;
28
- }
29
- catch (error) {
30
- throw wrapKyselyError(error, {
31
- table: table.name,
32
- operation: 'findById',
33
- availableColumns: table.columns.map(c => c.name)
34
- });
35
- }
36
- },
37
- findAll: async () => {
38
- try {
39
- const results = await this.db
55
+ .execute();
56
+ },
57
+ get: async (id) => {
58
+ return await this.db
40
59
  .selectFrom(table.name)
41
60
  .selectAll()
42
- .execute();
43
- return results;
44
- }
45
- catch (error) {
46
- throw wrapKyselyError(error, {
47
- table: table.name,
48
- operation: 'findAll',
49
- availableColumns: table.columns.map(c => c.name)
50
- });
51
- }
52
- },
53
- create: async (data) => {
54
- try {
55
- const result = await this.db
61
+ .where(primaryKey, '=', id)
62
+ .executeTakeFirst();
63
+ },
64
+ create: async (data) => {
65
+ return await this.db
56
66
  .insertInto(table.name)
57
67
  .values(data)
58
68
  .returningAll()
59
- .executeTakeFirstOrThrow();
60
- return result;
69
+ .executeTakeFirst();
70
+ },
71
+ update: async (id, data) => {
72
+ return await this.db
73
+ .updateTable(table.name)
74
+ .set(data)
75
+ .where(primaryKey, '=', id)
76
+ .returningAll()
77
+ .executeTakeFirst();
78
+ },
79
+ delete: async (id) => {
80
+ return await this.db
81
+ .deleteFrom(table.name)
82
+ .where(primaryKey, '=', id)
83
+ .executeTakeFirst();
61
84
  }
62
- catch (error) {
63
- throw wrapKyselyError(error, {
64
- table: table.name,
65
- operation: 'create',
66
- availableColumns: table.columns.map(c => c.name)
67
- });
85
+ },
86
+ // Direct CRUD methods
87
+ findById: async (id) => {
88
+ const result = await this.db
89
+ .selectFrom(table.name)
90
+ .selectAll()
91
+ .where(primaryKey, '=', id)
92
+ .executeTakeFirst();
93
+ return (result ? this.transformBooleans(result, table) : null);
94
+ },
95
+ findAll: async () => {
96
+ const results = await this.db
97
+ .selectFrom(table.name)
98
+ .selectAll()
99
+ .execute();
100
+ return this.transformBooleans(results, table);
101
+ },
102
+ create: async (data) => {
103
+ const result = await this.db
104
+ .insertInto(table.name)
105
+ .values(data)
106
+ .returningAll()
107
+ .executeTakeFirst();
108
+ if (!result) {
109
+ throw new Error(`Failed to create entity in table ${table.name}`);
68
110
  }
111
+ return result;
69
112
  },
70
113
  update: async (entity) => {
71
- try {
72
- const primaryKeyValue = this.getPrimaryKeyValue(table, entity);
73
- const updateData = this.getUpdateData(table, entity);
74
- const result = await this.db
75
- .updateTable(table.name)
76
- .set(updateData)
77
- .where(this.getPrimaryKeyCondition(table, primaryKeyValue))
78
- .returningAll()
79
- .executeTakeFirstOrThrow();
80
- return result;
114
+ const id = entity[primaryKey];
115
+ if (id === undefined) {
116
+ throw new Error(`Cannot update entity: missing primary key '${primaryKey}'. ` +
117
+ `Please ensure the entity has a '${primaryKey}' property.`);
81
118
  }
82
- catch (error) {
83
- throw wrapKyselyError(error, {
84
- table: table.name,
85
- operation: 'update',
86
- availableColumns: table.columns.map(c => c.name)
87
- });
119
+ const result = await this.db
120
+ .updateTable(table.name)
121
+ .set(entity)
122
+ .where(primaryKey, '=', id)
123
+ .returningAll()
124
+ .executeTakeFirst();
125
+ if (!result) {
126
+ throw new Error(`Failed to update entity with ${primaryKey}=${id} in table ${table.name}. ` +
127
+ `Entity may not exist.`);
88
128
  }
129
+ return result;
89
130
  },
90
131
  delete: async (id) => {
91
- try {
92
- const result = await this.db
93
- .deleteFrom(table.name)
94
- .where(this.getPrimaryKeyCondition(table, id))
95
- .execute();
96
- return result.length > 0;
97
- }
98
- catch (error) {
99
- throw wrapKyselyError(error, {
100
- table: table.name,
101
- operation: 'delete',
102
- availableColumns: table.columns.map(c => c.name)
103
- });
104
- }
132
+ const result = await this.db
133
+ .deleteFrom(table.name)
134
+ .where(primaryKey, '=', id)
135
+ .execute();
136
+ return result.length > 0 && Number(result[0].numDeletedRows) > 0;
105
137
  },
106
- // Relationship operations
107
- findWithRelations: async (id, relations) => {
108
- const entity = await repository.findById(id);
109
- if (!entity)
110
- return null;
111
- // Validate relationships exist
112
- for (const relationName of relations) {
113
- const relationship = relationships.find(r => r.name === relationName);
114
- if (!relationship) {
115
- throw new RelationshipNotFoundError(relationName, table.name, relationships.map(r => r.name));
116
- }
117
- }
118
- await this.loadRelationships([entity], relations, relationships);
119
- return entity;
138
+ // Utility methods
139
+ count: async () => {
140
+ const result = await this.db
141
+ .selectFrom(table.name)
142
+ .select((eb) => eb.fn.countAll().as('count'))
143
+ .executeTakeFirst();
144
+ return Number(result?.count || 0);
120
145
  },
121
- loadRelationships: async (entities, relations) => {
122
- // Validate relationships exist
123
- for (const relationName of relations) {
124
- const relationship = relationships.find(r => r.name === relationName);
125
- if (!relationship) {
126
- throw new RelationshipNotFoundError(relationName, table.name, relationships.map(r => r.name));
127
- }
128
- }
129
- await this.loadRelationships(entities, relations, relationships);
146
+ exists: async (id) => {
147
+ const result = await this.db
148
+ .selectFrom(table.name)
149
+ .select(primaryKey)
150
+ .where(primaryKey, '=', id)
151
+ .executeTakeFirst();
152
+ return result !== undefined;
130
153
  },
131
- // Pagination helper
154
+ // Pagination
132
155
  paginate: async (options) => {
133
- try {
134
- const { page, limit, where, orderBy } = options;
135
- const offset = (page - 1) * limit;
136
- // Build base query
137
- let query = this.db.selectFrom(table.name);
138
- // Apply where conditions
139
- if (where) {
140
- Object.entries(where).forEach(([key, value]) => {
141
- if (value !== undefined) {
142
- query = query.where(key, '=', value);
143
- }
144
- });
145
- }
146
- // Get total count
147
- const countResult = await query
148
- .select(({ fn }) => [fn.count('*').as('count')])
149
- .executeTakeFirst();
150
- const total = Number(countResult?.count ?? 0);
151
- // Get paginated data
152
- let dataQuery = query.selectAll().limit(limit).offset(offset);
153
- if (orderBy) {
154
- dataQuery = dataQuery.orderBy(orderBy.column, orderBy.direction);
156
+ let query = this.db.selectFrom(table.name).selectAll();
157
+ // Apply where conditions
158
+ if (options.where) {
159
+ for (const [key, value] of Object.entries(options.where)) {
160
+ if (value !== undefined) {
161
+ query = query.where(key, '=', value);
162
+ }
155
163
  }
156
- const data = await dataQuery.execute();
157
- return {
158
- data: data,
159
- pagination: {
160
- page,
161
- limit,
162
- total,
163
- totalPages: Math.ceil(total / limit),
164
- hasNext: offset + limit < total,
165
- hasPrev: page > 1,
166
- },
167
- };
168
- }
169
- catch (error) {
170
- throw wrapKyselyError(error, {
171
- table: table.name,
172
- operation: 'paginate',
173
- availableColumns: table.columns.map(c => c.name)
174
- });
175
164
  }
176
- },
177
- // Count relationships
178
- withCount: async (id, relationshipNames) => {
179
- try {
180
- const entity = await repository.findById(id);
181
- if (!entity) {
182
- throw new NoormError(`${table.name} not found with id: ${id}`, {
183
- table: table.name,
184
- operation: 'withCount'
185
- });
186
- }
187
- const counts = {};
188
- for (const relationName of relationshipNames) {
189
- const relationship = relationships.find(r => r.name === relationName);
190
- if (!relationship) {
191
- throw new RelationshipNotFoundError(relationName, table.name, relationships.map(r => r.name));
165
+ // Count total with same where conditions
166
+ let countQuery = this.db
167
+ .selectFrom(table.name)
168
+ .select((eb) => eb.fn.countAll().as('count'));
169
+ if (options.where) {
170
+ for (const [key, value] of Object.entries(options.where)) {
171
+ if (value !== undefined) {
172
+ countQuery = countQuery.where(key, '=', value);
192
173
  }
193
- const countResult = await this.db
194
- .selectFrom(relationship.toTable)
195
- .where(relationship.toColumn, '=', entity[relationship.fromColumn])
196
- .select(({ fn }) => [fn.count('*').as('count')])
197
- .executeTakeFirst();
198
- counts[`${relationName}Count`] = Number(countResult?.count ?? 0);
199
174
  }
200
- return { ...entity, ...counts };
201
175
  }
202
- catch (error) {
203
- if (error instanceof NoormError) {
204
- throw error;
205
- }
206
- throw wrapKyselyError(error, {
207
- table: table.name,
208
- operation: 'withCount',
209
- availableColumns: table.columns.map(c => c.name)
210
- });
176
+ const countResult = await countQuery.executeTakeFirst();
177
+ const total = Number(countResult?.count || 0);
178
+ // Apply order by
179
+ if (options.orderBy) {
180
+ query = query.orderBy(options.orderBy.column, options.orderBy.direction);
211
181
  }
212
- },
213
- // Count and exists methods (implemented in addCustomFinders)
214
- count: async () => 0, // Will be overridden in addCustomFinders
215
- exists: async (id) => false // Will be overridden in addCustomFinders
216
- };
217
- // Add custom finder methods based on table structure
218
- this.addCustomFinders(repository, table);
219
- return repository;
220
- }
221
- /**
222
- * Add custom finder methods to repository
223
- */
224
- addCustomFinders(repository, table) {
225
- // Add findBy methods for unique columns
226
- for (const column of table.columns) {
227
- if (column.name !== table.primaryKey?.[0]) {
228
- const methodName = `findBy${this.toPascalCase(column.name)}`;
229
- repository[methodName] = async (value) => {
230
- const result = await this.db
231
- .selectFrom(table.name)
232
- .selectAll()
233
- .where(column.name, '=', value)
234
- .executeTakeFirst();
235
- return result;
182
+ // Apply pagination
183
+ const offset = (options.page - 1) * options.limit;
184
+ query = query.limit(options.limit).offset(offset);
185
+ const data = await query.execute();
186
+ const totalPages = Math.ceil(total / options.limit);
187
+ const transformedData = this.transformBooleans(data, table);
188
+ return {
189
+ data: transformedData,
190
+ pagination: {
191
+ page: options.page,
192
+ limit: options.limit,
193
+ total,
194
+ totalPages,
195
+ hasNext: options.page < totalPages,
196
+ hasPrev: options.page > 1
197
+ }
236
198
  };
237
- }
238
- }
239
- // Add findManyBy methods for non-unique columns
240
- for (const column of table.columns) {
241
- const methodName = `findManyBy${this.toPascalCase(column.name)}`;
242
- repository[methodName] = async (value) => {
243
- const results = await this.db
199
+ },
200
+ // Relationship methods
201
+ findWithRelations: async (id, relations) => {
202
+ // Basic implementation - fetches entity without loading relations
203
+ // Full relationship loading would require additional implementation
204
+ const result = await this.db
244
205
  .selectFrom(table.name)
245
206
  .selectAll()
246
- .where(column.name, '=', value)
247
- .execute();
248
- return results;
249
- };
250
- }
251
- // Add count methods
252
- repository.count = async () => {
253
- const result = await this.db
254
- .selectFrom(table.name)
255
- .select(this.db.fn.count('*').as('count'))
256
- .executeTakeFirst();
257
- return Number(result?.count || 0);
258
- };
259
- // Add exists method
260
- repository.exists = async (id) => {
261
- const result = await this.db
262
- .selectFrom(table.name)
263
- .select(this.db.fn.count('*').as('count'))
264
- .where(this.getPrimaryKeyCondition(table, id))
265
- .executeTakeFirst();
266
- return Number(result?.count || 0) > 0;
267
- };
268
- }
269
- /**
270
- * Load relationships for entities
271
- */
272
- async loadRelationships(entities, relations, allRelationships) {
273
- if (!this.config.enableBatchLoading) {
274
- // Load relationships one by one
275
- for (const entity of entities) {
276
- await this.loadEntityRelationships(entity, relations, allRelationships);
277
- }
278
- return;
279
- }
280
- // Batch load relationships for performance
281
- const batchSize = this.config.maxBatchSize || 100;
282
- for (let i = 0; i < entities.length; i += batchSize) {
283
- const batch = entities.slice(i, i + batchSize);
284
- await this.batchLoadRelationships(batch, relations, allRelationships);
285
- }
286
- }
287
- /**
288
- * Load relationships for a single entity
289
- */
290
- async loadEntityRelationships(entity, relations, allRelationships) {
291
- for (const relationName of relations) {
292
- const relationship = allRelationships.find(r => r.name === relationName);
293
- if (!relationship)
294
- continue;
295
- await this.loadSingleRelationship(entity, relationship);
296
- }
297
- }
298
- /**
299
- * Batch load relationships for multiple entities
300
- */
301
- async batchLoadRelationships(entities, relations, allRelationships) {
302
- for (const relationName of relations) {
303
- const relationship = allRelationships.find(r => r.name === relationName);
304
- if (!relationship)
305
- continue;
306
- await this.batchLoadSingleRelationship(entities, relationship);
307
- }
308
- }
309
- /**
310
- * Load a single relationship for an entity
311
- */
312
- async loadSingleRelationship(entity, relationship) {
313
- const entityValue = entity[relationship.fromColumn];
314
- if (!entityValue)
315
- return;
316
- let relatedData;
317
- switch (relationship.type) {
318
- case 'many-to-one':
319
- relatedData = await this.db
320
- .selectFrom(relationship.toTable)
321
- .selectAll()
322
- .where(relationship.toColumn, '=', entityValue)
207
+ .where(primaryKey, '=', id)
323
208
  .executeTakeFirst();
324
- break;
325
- case 'one-to-many':
326
- relatedData = await this.db
327
- .selectFrom(relationship.toTable)
328
- .selectAll()
329
- .where(relationship.fromColumn, '=', entityValue)
330
- .execute();
331
- break;
332
- case 'many-to-many':
333
- // TODO: Implement many-to-many relationships
334
- relatedData = [];
335
- break;
336
- }
337
- entity[relationship.name] = relatedData;
338
- }
339
- /**
340
- * Batch load a single relationship for multiple entities
341
- */
342
- async batchLoadSingleRelationship(entities, relationship) {
343
- const entityValues = entities
344
- .map(e => e[relationship.fromColumn])
345
- .filter(v => v !== undefined && v !== null);
346
- if (entityValues.length === 0)
347
- return;
348
- let relatedData;
349
- switch (relationship.type) {
350
- case 'many-to-one':
351
- relatedData = await this.db
352
- .selectFrom(relationship.toTable)
353
- .selectAll()
354
- .where(relationship.toColumn, 'in', entityValues)
355
- .execute();
356
- break;
357
- case 'one-to-many':
358
- relatedData = await this.db
359
- .selectFrom(relationship.toTable)
209
+ return result ?? null;
210
+ },
211
+ loadRelationships: async (entities, relations) => {
212
+ // Stub implementation for relationship loading
213
+ // Full implementation would load related entities based on relationship definitions
214
+ // For now, this is a no-op to satisfy the interface
215
+ },
216
+ withCount: async (id, relationshipNames) => {
217
+ // Fetch the base entity
218
+ const rawEntity = await this.db
219
+ .selectFrom(table.name)
360
220
  .selectAll()
361
- .where(relationship.fromColumn, 'in', entityValues)
362
- .execute();
363
- break;
364
- case 'many-to-many':
365
- // TODO: Implement many-to-many relationships
366
- relatedData = [];
367
- break;
368
- default:
369
- relatedData = [];
370
- }
371
- // Group related data by foreign key value
372
- const groupedData = new Map();
373
- for (const item of relatedData) {
374
- const key = item[relationship.toColumn];
375
- if (!groupedData.has(key)) {
376
- groupedData.set(key, []);
377
- }
378
- groupedData.get(key).push(item);
379
- }
380
- // Assign related data to entities
381
- for (const entity of entities) {
382
- const entityValue = entity[relationship.fromColumn];
383
- if (entityValue) {
384
- let entityRelatedData;
385
- if (relationship.type === 'many-to-one') {
386
- entityRelatedData = groupedData.get(entityValue)?.[0];
221
+ .where(primaryKey, '=', id)
222
+ .executeTakeFirst();
223
+ if (!rawEntity) {
224
+ throw new Error(`Entity with ${primaryKey}=${id} not found in table ${table.name}`);
387
225
  }
388
- else {
389
- entityRelatedData = groupedData.get(entityValue) || [];
226
+ // Transform booleans in the entity
227
+ const entity = this.transformBooleans(rawEntity, table);
228
+ // Filter relationships to only those from this table
229
+ const tableRelationships = relationships.filter(r => r.fromTable === table.name);
230
+ const availableRelationshipNames = tableRelationships.map(r => r.name);
231
+ // Validate all relationships exist before executing queries
232
+ for (const relationshipName of relationshipNames) {
233
+ const relationship = tableRelationships.find(r => r.name === relationshipName);
234
+ if (!relationship) {
235
+ throw new RelationshipNotFoundError(relationshipName, table.name, availableRelationshipNames);
236
+ }
390
237
  }
391
- entity[relationship.name] = entityRelatedData;
392
- }
393
- }
394
- }
395
- /**
396
- * Get primary key condition for queries
397
- */
398
- getPrimaryKeyCondition(table, id) {
399
- if (table.primaryKey && table.primaryKey.length === 1) {
400
- return { [table.primaryKey[0]]: id };
401
- }
402
- // Handle composite primary keys
403
- if (table.primaryKey && table.primaryKey.length > 1) {
404
- const conditions = {};
405
- for (let i = 0; i < table.primaryKey.length; i++) {
406
- conditions[table.primaryKey[i]] = id[i];
238
+ // Create counts object for each relationship
239
+ const counts = {};
240
+ for (const relationshipName of relationshipNames) {
241
+ // Find the relationship definition for this table
242
+ const relationship = tableRelationships.find(r => r.name === relationshipName);
243
+ if (relationship) {
244
+ // Count related entities based on relationship type
245
+ // For one-to-many: count records in toTable where toColumn matches our fromColumn value
246
+ // Example: For user.posts, count posts where posts.user_id = user.id
247
+ const entityKeyValue = entity[relationship.fromColumn];
248
+ // Only count if the foreign key value is not NULL
249
+ if (entityKeyValue != null) {
250
+ // Build query to count related records
251
+ let countQuery = this.db
252
+ .selectFrom(relationship.toTable)
253
+ .select((eb) => eb.fn.countAll().as('count'))
254
+ .where(relationship.toColumn, '=', entityKeyValue);
255
+ // Find the related table's foreign keys to exclude orphaned records
256
+ // Get all relationships FROM the target table to find its foreign keys
257
+ const targetTableInfo = relationships
258
+ .filter(r => r.fromTable === relationship.toTable)
259
+ .map(r => r.fromColumn);
260
+ // Exclude records where any foreign key is NULL (orphaned records)
261
+ for (const fkColumn of targetTableInfo) {
262
+ countQuery = countQuery.where(fkColumn, 'is not', null);
263
+ }
264
+ const relatedCount = await countQuery.executeTakeFirst();
265
+ // Use camelCase format for count property (e.g., "posts" -> "postsCount")
266
+ counts[`${relationshipName}Count`] = Number(relatedCount?.count || 0);
267
+ }
268
+ else {
269
+ // If the foreign key is NULL, count is 0
270
+ counts[`${relationshipName}Count`] = 0;
271
+ }
272
+ }
273
+ }
274
+ return { ...entity, ...counts };
407
275
  }
408
- return conditions;
409
- }
410
- throw new Error(`No primary key defined for table ${table.name}`);
411
- }
412
- /**
413
- * Get primary key value from entity
414
- */
415
- getPrimaryKeyValue(table, entity) {
416
- if (table.primaryKey && table.primaryKey.length === 1) {
417
- return entity[table.primaryKey[0]];
418
- }
419
- // Handle composite primary keys
420
- if (table.primaryKey && table.primaryKey.length > 1) {
421
- return table.primaryKey.map(key => entity[key]);
422
- }
423
- throw new Error(`No primary key defined for table ${table.name}`);
276
+ };
277
+ // Wrap repository in Proxy to handle dynamic method calls like findByXxx
278
+ return this.wrapWithDynamicMethods(repository, table);
424
279
  }
425
280
  /**
426
- * Get update data (exclude primary key)
281
+ * Wrap repository with Proxy to handle dynamic method calls
427
282
  */
428
- getUpdateData(table, entity) {
429
- const updateData = {};
430
- for (const column of table.columns) {
431
- if (!table.primaryKey?.includes(column.name)) {
432
- updateData[column.name] = entity[column.name];
283
+ wrapWithDynamicMethods(repository, table) {
284
+ const availableColumns = table.columns.map(c => c.name);
285
+ const db = this.db;
286
+ const transformBooleans = this.transformBooleans.bind(this);
287
+ return new Proxy(repository, {
288
+ get(target, prop, receiver) {
289
+ // Check if property exists on target
290
+ if (prop in target) {
291
+ return Reflect.get(target, prop, receiver);
292
+ }
293
+ // Handle dynamic findByXxx methods
294
+ if (typeof prop === 'string' && prop.startsWith('findBy')) {
295
+ return async (value) => {
296
+ // Extract column name from method name
297
+ // findByEmail -> Email -> email
298
+ // findByInvalidColumn -> InvalidColumn -> invalid_column
299
+ const columnNameRaw = prop.substring(6); // Remove 'findBy'
300
+ // Convert from PascalCase to snake_case
301
+ // Handle both camelCase and PascalCase
302
+ let columnName = columnNameRaw
303
+ // Insert underscore before uppercase letters that follow lowercase
304
+ .replace(/([a-z])([A-Z])/g, '$1_$2')
305
+ // Insert underscore before uppercase letters that follow numbers
306
+ .replace(/([0-9])([A-Z])/g, '$1_$2')
307
+ // Convert to lowercase
308
+ .toLowerCase();
309
+ // Check if column exists (case-insensitive)
310
+ const columnExists = availableColumns.some(col => col.toLowerCase() === columnName.toLowerCase());
311
+ if (!columnExists) {
312
+ throw new ColumnNotFoundError(columnName, table.name, availableColumns);
313
+ }
314
+ // Execute query with the valid column
315
+ const actualColumn = availableColumns.find(col => col.toLowerCase() === columnName.toLowerCase());
316
+ const result = await db
317
+ .selectFrom(table.name)
318
+ .selectAll()
319
+ .where(actualColumn, '=', value)
320
+ .executeTakeFirst();
321
+ return transformBooleans(result || null, table);
322
+ };
323
+ }
324
+ return undefined;
433
325
  }
434
- }
435
- return updateData;
436
- }
437
- /**
438
- * Convert string to PascalCase
439
- */
440
- toPascalCase(str) {
441
- return str
442
- .replace(/(?:^\w|[A-Z]|\b\w)/g, (word) => word.toUpperCase())
443
- .replace(/\s+/g, '')
444
- .replace(/[_-]/g, '');
326
+ });
445
327
  }
446
328
  }