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.
- package/LICENSE +89 -21
- package/README.md +195 -578
- package/dist/cjs/cli/commands/analyze.js +4 -1
- package/dist/cjs/cli/commands/generate.js +48 -8
- package/dist/cjs/cli/commands/init.js +54 -14
- package/dist/cjs/cli/commands/inspect.js +10 -3
- package/dist/cjs/cli/commands/migrate.js +38 -2
- package/dist/cjs/cli/commands/optimize.js +4 -1
- package/dist/cjs/cli/commands/status.js +41 -5
- package/dist/cjs/cli/commands/watch.js +4 -1
- package/dist/cjs/cli/index.js +4 -1
- package/dist/cjs/dialect/database-introspector.js +16 -21
- package/dist/cjs/dialect/sqlite/sqlite-introspector.js +13 -24
- package/dist/cjs/dynamic/dynamic.d.ts +20 -0
- package/dist/cjs/dynamic/dynamic.js +25 -0
- package/dist/cjs/edge-runtime/edge-config.d.ts +125 -0
- package/dist/cjs/edge-runtime/edge-config.js +323 -0
- package/dist/cjs/errors/NoormError.d.ts +27 -4
- package/dist/cjs/errors/NoormError.js +142 -21
- package/dist/cjs/logging/logger.d.ts +7 -2
- package/dist/cjs/logging/logger.js +21 -7
- package/dist/cjs/noormme.d.ts +12 -8
- package/dist/cjs/noormme.js +133 -61
- package/dist/cjs/operation-node/column-node.js +4 -0
- package/dist/cjs/operation-node/identifier-node.js +4 -0
- package/dist/cjs/operation-node/table-node.js +7 -0
- package/dist/cjs/parser/reference-parser.js +5 -0
- package/dist/cjs/parser/table-parser.js +2 -0
- package/dist/cjs/performance/index.d.ts +44 -0
- package/dist/cjs/performance/index.js +64 -0
- package/dist/cjs/performance/query-optimizer.d.ts +134 -0
- package/dist/cjs/performance/query-optimizer.js +391 -0
- package/dist/cjs/performance/services/cache-service.d.ts +177 -0
- package/dist/cjs/performance/services/cache-service.js +415 -0
- package/dist/cjs/performance/services/connection-factory.d.ts +198 -0
- package/dist/cjs/performance/services/connection-factory.js +498 -0
- package/dist/cjs/performance/services/metrics-collector.d.ts +162 -0
- package/dist/cjs/performance/services/metrics-collector.js +406 -0
- package/dist/cjs/performance/utils/query-parser.d.ts +123 -0
- package/dist/cjs/performance/utils/query-parser.js +295 -0
- package/dist/cjs/raw-builder/sql.d.ts +73 -26
- package/dist/cjs/raw-builder/sql.js +9 -0
- package/dist/cjs/repository/repository-factory.d.ts +10 -42
- package/dist/cjs/repository/repository-factory.js +276 -394
- package/dist/cjs/schema/core/coordinators/schema-discovery.coordinator.js +6 -4
- package/dist/cjs/schema/core/factories/discovery-factory.js +5 -5
- package/dist/cjs/schema/core/utils/name-generator.js +34 -2
- package/dist/cjs/schema/core/utils/type-mapper.d.ts +19 -14
- package/dist/cjs/schema/core/utils/type-mapper.js +4 -7
- package/dist/cjs/schema/dialects/sqlite/discovery/sqlite-constraint-discovery.js +3 -2
- package/dist/cjs/schema/dialects/sqlite/sqlite-discovery.coordinator.d.ts +2 -0
- package/dist/cjs/schema/dialects/sqlite/sqlite-discovery.coordinator.js +19 -5
- package/dist/cjs/schema/test/dialect-capabilities.test.js +6 -0
- package/dist/cjs/schema/test/error-handling.test.js +52 -33
- package/dist/cjs/schema/test/integration.test.js +51 -5
- package/dist/cjs/schema/test/sqlite-discovery-coordinator.test.js +8 -4
- package/dist/cjs/sqlite-migration/index.js +35 -2
- package/dist/cjs/testing/test-utils.d.ts +5 -0
- package/dist/cjs/testing/test-utils.js +66 -6
- package/dist/cjs/types/index.d.ts +78 -13
- package/dist/cjs/types/index.js +23 -0
- package/dist/cjs/types/type-generator.d.ts +8 -0
- package/dist/cjs/types/type-generator.js +86 -17
- package/dist/cjs/util/safe-sql-helpers.d.ts +124 -0
- package/dist/cjs/util/safe-sql-helpers.js +221 -0
- package/dist/cjs/util/security-validator.d.ts +44 -0
- package/dist/cjs/util/security-validator.js +256 -0
- package/dist/cjs/util/security.d.ts +60 -0
- package/dist/cjs/util/security.js +137 -0
- package/dist/cjs/watch/schema-watcher.js +26 -7
- package/dist/esm/cli/commands/generate.js +10 -6
- package/dist/esm/cli/commands/init.js +15 -11
- package/dist/esm/cli/commands/inspect.js +6 -2
- package/dist/esm/cli/commands/status.js +3 -3
- package/dist/esm/dialect/database-introspector.js +16 -21
- package/dist/esm/dialect/sqlite/sqlite-introspector.js +13 -24
- package/dist/esm/dynamic/dynamic.d.ts +20 -0
- package/dist/esm/dynamic/dynamic.js +25 -0
- package/dist/esm/edge-runtime/edge-config.d.ts +125 -0
- package/dist/esm/edge-runtime/edge-config.js +281 -0
- package/dist/esm/errors/NoormError.d.ts +27 -4
- package/dist/esm/errors/NoormError.js +134 -18
- package/dist/esm/logging/logger.d.ts +7 -2
- package/dist/esm/logging/logger.js +21 -7
- package/dist/esm/noormme.d.ts +12 -8
- package/dist/esm/noormme.js +130 -61
- package/dist/esm/operation-node/column-node.js +4 -0
- package/dist/esm/operation-node/identifier-node.js +4 -0
- package/dist/esm/operation-node/table-node.js +7 -0
- package/dist/esm/parser/reference-parser.js +5 -0
- package/dist/esm/parser/table-parser.js +2 -0
- package/dist/esm/performance/index.d.ts +44 -0
- package/dist/esm/performance/index.js +48 -0
- package/dist/esm/performance/query-optimizer.d.ts +134 -0
- package/dist/esm/performance/query-optimizer.js +387 -0
- package/dist/esm/performance/services/cache-service.d.ts +177 -0
- package/dist/esm/performance/services/cache-service.js +410 -0
- package/dist/esm/performance/services/connection-factory.d.ts +198 -0
- package/dist/esm/performance/services/connection-factory.js +493 -0
- package/dist/esm/performance/services/metrics-collector.d.ts +162 -0
- package/dist/esm/performance/services/metrics-collector.js +402 -0
- package/dist/esm/performance/utils/query-parser.d.ts +123 -0
- package/dist/esm/performance/utils/query-parser.js +292 -0
- package/dist/esm/raw-builder/sql.d.ts +73 -26
- package/dist/esm/raw-builder/sql.js +9 -0
- package/dist/esm/repository/repository-factory.d.ts +10 -42
- package/dist/esm/repository/repository-factory.js +277 -395
- package/dist/esm/schema/core/coordinators/schema-discovery.coordinator.js +6 -4
- package/dist/esm/schema/core/factories/discovery-factory.js +5 -5
- package/dist/esm/schema/core/utils/name-generator.js +34 -2
- package/dist/esm/schema/core/utils/type-mapper.d.ts +19 -14
- package/dist/esm/schema/core/utils/type-mapper.js +4 -7
- package/dist/esm/schema/dialects/sqlite/discovery/sqlite-constraint-discovery.js +3 -2
- package/dist/esm/schema/dialects/sqlite/sqlite-discovery.coordinator.d.ts +2 -0
- package/dist/esm/schema/dialects/sqlite/sqlite-discovery.coordinator.js +19 -5
- package/dist/esm/schema/test/dialect-capabilities.test.js +6 -0
- package/dist/esm/schema/test/error-handling.test.js +52 -33
- package/dist/esm/schema/test/integration.test.js +18 -5
- package/dist/esm/schema/test/sqlite-discovery-coordinator.test.js +8 -4
- package/dist/esm/testing/test-utils.d.ts +5 -0
- package/dist/esm/testing/test-utils.js +66 -6
- package/dist/esm/types/index.d.ts +78 -13
- package/dist/esm/types/index.js +22 -1
- package/dist/esm/types/type-generator.d.ts +8 -0
- package/dist/esm/types/type-generator.js +86 -17
- package/dist/esm/util/safe-sql-helpers.d.ts +124 -0
- package/dist/esm/util/safe-sql-helpers.js +209 -0
- package/dist/esm/util/security-validator.d.ts +44 -0
- package/dist/esm/util/security-validator.js +246 -0
- package/dist/esm/util/security.d.ts +60 -0
- package/dist/esm/util/security.js +114 -0
- package/dist/esm/watch/schema-watcher.js +26 -7
- package/package.json +1 -1
- package/dist/cjs/performance/query-analyzer.d.ts +0 -89
- package/dist/cjs/performance/query-analyzer.js +0 -263
- package/dist/esm/performance/query-analyzer.d.ts +0 -89
- package/dist/esm/performance/query-analyzer.js +0 -260
|
@@ -1,446 +1,328 @@
|
|
|
1
1
|
/// <reference types="./repository-factory.d.ts" />
|
|
2
|
-
import {
|
|
3
|
-
import { wrapKyselyError } from '../utils/errorHelpers.js';
|
|
2
|
+
import { RelationshipNotFoundError, ColumnNotFoundError } from '../errors/NoormError.js';
|
|
4
3
|
/**
|
|
5
|
-
*
|
|
4
|
+
* Simple repository factory for creating table repositories
|
|
6
5
|
*/
|
|
7
6
|
export class RepositoryFactory {
|
|
8
7
|
db;
|
|
9
|
-
|
|
10
|
-
constructor(db,
|
|
8
|
+
performanceConfig;
|
|
9
|
+
constructor(db, performanceConfig) {
|
|
11
10
|
this.db = db;
|
|
12
|
-
this.
|
|
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
|
-
//
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
49
|
+
// Django-style objects wrapper
|
|
50
|
+
objects: {
|
|
51
|
+
all: async () => {
|
|
52
|
+
return await this.db
|
|
23
53
|
.selectFrom(table.name)
|
|
24
54
|
.selectAll()
|
|
25
|
-
.
|
|
26
|
-
|
|
27
|
-
|
|
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
|
-
.
|
|
43
|
-
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
|
|
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
|
-
.
|
|
60
|
-
|
|
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
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
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
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
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
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
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
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
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
|
-
//
|
|
107
|
-
|
|
108
|
-
const
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
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
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
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
|
|
154
|
+
// Pagination
|
|
132
155
|
paginate: async (options) => {
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
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
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
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
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
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
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
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
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
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(
|
|
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
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
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(
|
|
362
|
-
.
|
|
363
|
-
|
|
364
|
-
|
|
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
|
-
|
|
389
|
-
|
|
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
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
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
|
-
|
|
409
|
-
|
|
410
|
-
|
|
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
|
-
*
|
|
281
|
+
* Wrap repository with Proxy to handle dynamic method calls
|
|
427
282
|
*/
|
|
428
|
-
|
|
429
|
-
const
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
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
|
}
|