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
|
@@ -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
|
-
*
|
|
6
|
+
* Simple repository factory for creating table repositories
|
|
8
7
|
*/
|
|
9
8
|
class RepositoryFactory {
|
|
10
9
|
db;
|
|
11
|
-
|
|
12
|
-
constructor(db,
|
|
10
|
+
performanceConfig;
|
|
11
|
+
constructor(db, performanceConfig) {
|
|
13
12
|
this.db = db;
|
|
14
|
-
this.
|
|
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
|
-
//
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
51
|
+
// Django-style objects wrapper
|
|
52
|
+
objects: {
|
|
53
|
+
all: async () => {
|
|
54
|
+
return await this.db
|
|
25
55
|
.selectFrom(table.name)
|
|
26
56
|
.selectAll()
|
|
27
|
-
.
|
|
28
|
-
|
|
29
|
-
|
|
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
|
-
.
|
|
45
|
-
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
|
|
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
|
-
.
|
|
62
|
-
|
|
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
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
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
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
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
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
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
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
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
|
-
//
|
|
109
|
-
|
|
110
|
-
const
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
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
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
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
|
|
156
|
+
// Pagination
|
|
134
157
|
paginate: async (options) => {
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
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
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
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
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
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
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
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
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
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(
|
|
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
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
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(
|
|
364
|
-
.
|
|
365
|
-
|
|
366
|
-
|
|
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
|
-
|
|
391
|
-
|
|
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
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
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
|
-
|
|
411
|
-
|
|
412
|
-
|
|
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
|
-
*
|
|
283
|
+
* Wrap repository with Proxy to handle dynamic method calls
|
|
429
284
|
*/
|
|
430
|
-
|
|
431
|
-
const
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
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;
|