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