alepha 0.14.4 → 0.15.0
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/README.md +1 -4
- package/dist/api/audits/index.d.ts +619 -731
- package/dist/api/audits/index.d.ts.map +1 -1
- package/dist/api/files/index.d.ts +185 -298
- package/dist/api/files/index.d.ts.map +1 -1
- package/dist/api/files/index.js +0 -1
- package/dist/api/files/index.js.map +1 -1
- package/dist/api/jobs/index.d.ts +245 -356
- package/dist/api/jobs/index.d.ts.map +1 -1
- package/dist/api/notifications/index.d.ts +238 -350
- package/dist/api/notifications/index.d.ts.map +1 -1
- package/dist/api/parameters/index.d.ts +499 -611
- package/dist/api/parameters/index.d.ts.map +1 -1
- package/dist/api/users/index.browser.js +1 -2
- package/dist/api/users/index.browser.js.map +1 -1
- package/dist/api/users/index.d.ts +1697 -1804
- package/dist/api/users/index.d.ts.map +1 -1
- package/dist/api/users/index.js +178 -151
- package/dist/api/users/index.js.map +1 -1
- package/dist/api/verifications/index.d.ts +132 -132
- package/dist/api/verifications/index.d.ts.map +1 -1
- package/dist/batch/index.d.ts +122 -122
- package/dist/batch/index.d.ts.map +1 -1
- package/dist/batch/index.js +1 -2
- package/dist/batch/index.js.map +1 -1
- package/dist/bucket/index.d.ts +163 -163
- package/dist/bucket/index.d.ts.map +1 -1
- package/dist/cache/core/index.d.ts +46 -46
- package/dist/cache/core/index.d.ts.map +1 -1
- package/dist/cache/redis/index.d.ts.map +1 -1
- package/dist/cli/index.d.ts +302 -299
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +966 -564
- package/dist/cli/index.js.map +1 -1
- package/dist/command/index.d.ts +303 -299
- package/dist/command/index.d.ts.map +1 -1
- package/dist/command/index.js +11 -7
- package/dist/command/index.js.map +1 -1
- package/dist/core/index.browser.js +419 -99
- package/dist/core/index.browser.js.map +1 -1
- package/dist/core/index.d.ts +718 -625
- package/dist/core/index.d.ts.map +1 -1
- package/dist/core/index.js +420 -99
- package/dist/core/index.js.map +1 -1
- package/dist/core/index.native.js +419 -99
- package/dist/core/index.native.js.map +1 -1
- package/dist/datetime/index.d.ts +44 -44
- package/dist/datetime/index.d.ts.map +1 -1
- package/dist/datetime/index.js +4 -4
- package/dist/datetime/index.js.map +1 -1
- package/dist/email/index.d.ts +97 -50
- package/dist/email/index.d.ts.map +1 -1
- package/dist/email/index.js +129 -33
- package/dist/email/index.js.map +1 -1
- package/dist/fake/index.d.ts +7981 -14
- package/dist/fake/index.d.ts.map +1 -1
- package/dist/file/index.d.ts +523 -390
- package/dist/file/index.d.ts.map +1 -1
- package/dist/file/index.js +253 -1
- package/dist/file/index.js.map +1 -1
- package/dist/lock/core/index.d.ts +208 -208
- package/dist/lock/core/index.d.ts.map +1 -1
- package/dist/lock/redis/index.d.ts.map +1 -1
- package/dist/logger/index.d.ts +25 -26
- package/dist/logger/index.d.ts.map +1 -1
- package/dist/mcp/index.d.ts +197 -197
- package/dist/mcp/index.d.ts.map +1 -1
- package/dist/orm/chunk-DtkW-qnP.js +38 -0
- package/dist/orm/index.browser.js.map +1 -1
- package/dist/orm/index.bun.js +2814 -0
- package/dist/orm/index.bun.js.map +1 -0
- package/dist/orm/index.d.ts +1205 -1057
- package/dist/orm/index.d.ts.map +1 -1
- package/dist/orm/index.js +2056 -1753
- package/dist/orm/index.js.map +1 -1
- package/dist/queue/core/index.d.ts +248 -248
- package/dist/queue/core/index.d.ts.map +1 -1
- package/dist/queue/redis/index.d.ts.map +1 -1
- package/dist/redis/index.bun.js +285 -0
- package/dist/redis/index.bun.js.map +1 -0
- package/dist/redis/index.d.ts +118 -136
- package/dist/redis/index.d.ts.map +1 -1
- package/dist/redis/index.js +18 -38
- package/dist/redis/index.js.map +1 -1
- package/dist/retry/index.d.ts +69 -69
- package/dist/retry/index.d.ts.map +1 -1
- package/dist/router/index.d.ts +6 -6
- package/dist/router/index.d.ts.map +1 -1
- package/dist/scheduler/index.d.ts +25 -25
- package/dist/scheduler/index.d.ts.map +1 -1
- package/dist/security/index.browser.js +5 -1
- package/dist/security/index.browser.js.map +1 -1
- package/dist/security/index.d.ts +417 -254
- package/dist/security/index.d.ts.map +1 -1
- package/dist/security/index.js +386 -86
- package/dist/security/index.js.map +1 -1
- package/dist/server/auth/index.d.ts +277 -277
- package/dist/server/auth/index.d.ts.map +1 -1
- package/dist/server/auth/index.js +20 -20
- package/dist/server/auth/index.js.map +1 -1
- package/dist/server/cache/index.d.ts +60 -57
- package/dist/server/cache/index.d.ts.map +1 -1
- package/dist/server/cache/index.js +1 -1
- package/dist/server/cache/index.js.map +1 -1
- package/dist/server/compress/index.d.ts +3 -3
- package/dist/server/compress/index.d.ts.map +1 -1
- package/dist/server/cookies/index.d.ts +6 -6
- package/dist/server/cookies/index.d.ts.map +1 -1
- package/dist/server/cookies/index.js +3 -3
- package/dist/server/cookies/index.js.map +1 -1
- package/dist/server/core/index.d.ts +242 -150
- package/dist/server/core/index.d.ts.map +1 -1
- package/dist/server/core/index.js +288 -122
- package/dist/server/core/index.js.map +1 -1
- package/dist/server/cors/index.d.ts +11 -12
- package/dist/server/cors/index.d.ts.map +1 -1
- package/dist/server/health/index.d.ts +0 -1
- package/dist/server/health/index.d.ts.map +1 -1
- package/dist/server/helmet/index.d.ts +2 -2
- package/dist/server/helmet/index.d.ts.map +1 -1
- package/dist/server/links/index.browser.js.map +1 -1
- package/dist/server/links/index.d.ts +84 -85
- package/dist/server/links/index.d.ts.map +1 -1
- package/dist/server/links/index.js +1 -2
- package/dist/server/links/index.js.map +1 -1
- package/dist/server/metrics/index.d.ts.map +1 -1
- package/dist/server/multipart/index.d.ts +6 -6
- package/dist/server/multipart/index.d.ts.map +1 -1
- package/dist/server/proxy/index.d.ts +102 -103
- package/dist/server/proxy/index.d.ts.map +1 -1
- package/dist/server/rate-limit/index.d.ts +16 -16
- package/dist/server/rate-limit/index.d.ts.map +1 -1
- package/dist/server/static/index.d.ts +44 -44
- package/dist/server/static/index.d.ts.map +1 -1
- package/dist/server/swagger/index.d.ts +48 -49
- package/dist/server/swagger/index.d.ts.map +1 -1
- package/dist/server/swagger/index.js +1 -2
- package/dist/server/swagger/index.js.map +1 -1
- package/dist/sms/index.d.ts +13 -11
- package/dist/sms/index.d.ts.map +1 -1
- package/dist/sms/index.js +7 -7
- package/dist/sms/index.js.map +1 -1
- package/dist/thread/index.d.ts +71 -72
- package/dist/thread/index.d.ts.map +1 -1
- package/dist/topic/core/index.d.ts +318 -318
- package/dist/topic/core/index.d.ts.map +1 -1
- package/dist/topic/redis/index.d.ts +6 -6
- package/dist/topic/redis/index.d.ts.map +1 -1
- package/dist/vite/index.d.ts +5720 -159
- package/dist/vite/index.d.ts.map +1 -1
- package/dist/vite/index.js +41 -18
- package/dist/vite/index.js.map +1 -1
- package/dist/websocket/index.browser.js +6 -6
- package/dist/websocket/index.browser.js.map +1 -1
- package/dist/websocket/index.d.ts +247 -247
- package/dist/websocket/index.d.ts.map +1 -1
- package/dist/websocket/index.js +6 -6
- package/dist/websocket/index.js.map +1 -1
- package/package.json +9 -14
- package/src/api/files/controllers/AdminFileStatsController.ts +0 -1
- package/src/api/users/atoms/realmAuthSettingsAtom.ts +5 -0
- package/src/api/users/controllers/{UserRealmController.ts → RealmController.ts} +11 -11
- package/src/api/users/entities/users.ts +1 -1
- package/src/api/users/index.ts +8 -8
- package/src/api/users/primitives/{$userRealm.ts → $realm.ts} +17 -19
- package/src/api/users/providers/{UserRealmProvider.ts → RealmProvider.ts} +26 -30
- package/src/api/users/schemas/{userRealmConfigSchema.ts → realmConfigSchema.ts} +2 -2
- package/src/api/users/services/CredentialService.ts +7 -7
- package/src/api/users/services/IdentityService.ts +4 -4
- package/src/api/users/services/RegistrationService.spec.ts +25 -27
- package/src/api/users/services/RegistrationService.ts +38 -27
- package/src/api/users/services/SessionCrudService.ts +3 -3
- package/src/api/users/services/SessionService.spec.ts +3 -3
- package/src/api/users/services/SessionService.ts +28 -9
- package/src/api/users/services/UserService.ts +7 -7
- package/src/batch/providers/BatchProvider.ts +1 -2
- package/src/cli/apps/AlephaPackageBuilderCli.ts +38 -19
- package/src/cli/assets/apiHelloControllerTs.ts +18 -0
- package/src/cli/assets/apiIndexTs.ts +16 -0
- package/src/cli/assets/claudeMd.ts +303 -0
- package/src/cli/assets/mainBrowserTs.ts +2 -2
- package/src/cli/assets/mainServerTs.ts +24 -0
- package/src/cli/assets/webAppRouterTs.ts +15 -0
- package/src/cli/assets/webHelloComponentTsx.ts +16 -0
- package/src/cli/assets/webIndexTs.ts +16 -0
- package/src/cli/commands/build.ts +41 -21
- package/src/cli/commands/db.ts +21 -18
- package/src/cli/commands/deploy.ts +17 -5
- package/src/cli/commands/dev.ts +13 -17
- package/src/cli/commands/format.ts +8 -2
- package/src/cli/commands/init.ts +74 -29
- package/src/cli/commands/lint.ts +8 -2
- package/src/cli/commands/test.ts +8 -2
- package/src/cli/commands/typecheck.ts +5 -1
- package/src/cli/commands/verify.ts +4 -2
- package/src/cli/services/AlephaCliUtils.ts +39 -600
- package/src/cli/services/PackageManagerUtils.ts +301 -0
- package/src/cli/services/ProjectScaffolder.ts +306 -0
- package/src/command/helpers/Runner.ts +15 -3
- package/src/core/__tests__/Alepha-graph.spec.ts +4 -0
- package/src/core/index.shared.ts +1 -0
- package/src/core/index.ts +2 -0
- package/src/core/primitives/$hook.ts +6 -2
- package/src/core/primitives/$module.spec.ts +4 -0
- package/src/core/providers/AlsProvider.ts +1 -1
- package/src/core/providers/CodecManager.spec.ts +12 -6
- package/src/core/providers/CodecManager.ts +26 -6
- package/src/core/providers/EventManager.ts +169 -13
- package/src/core/providers/KeylessJsonSchemaCodec.spec.ts +621 -0
- package/src/core/providers/KeylessJsonSchemaCodec.ts +407 -0
- package/src/core/providers/StateManager.spec.ts +27 -16
- package/src/email/providers/LocalEmailProvider.spec.ts +111 -87
- package/src/email/providers/LocalEmailProvider.ts +52 -15
- package/src/email/providers/NodemailerEmailProvider.ts +167 -56
- package/src/file/errors/FileError.ts +7 -0
- package/src/file/index.ts +9 -1
- package/src/file/providers/MemoryFileSystemProvider.ts +393 -0
- package/src/orm/index.browser.ts +1 -19
- package/src/orm/index.bun.ts +77 -0
- package/src/orm/index.shared-server.ts +22 -0
- package/src/orm/index.shared.ts +15 -0
- package/src/orm/index.ts +19 -39
- package/src/orm/providers/drivers/BunPostgresProvider.ts +3 -5
- package/src/orm/providers/drivers/BunSqliteProvider.ts +1 -1
- package/src/orm/providers/drivers/CloudflareD1Provider.ts +4 -0
- package/src/orm/providers/drivers/DatabaseProvider.ts +4 -0
- package/src/orm/providers/drivers/PglitePostgresProvider.ts +4 -0
- package/src/orm/services/Repository.ts +8 -0
- package/src/redis/index.bun.ts +35 -0
- package/src/redis/providers/BunRedisProvider.ts +12 -43
- package/src/redis/providers/BunRedisSubscriberProvider.ts +2 -3
- package/src/redis/providers/NodeRedisProvider.ts +16 -34
- package/src/{server/security → security}/__tests__/BasicAuth.spec.ts +11 -11
- package/src/{server/security → security}/__tests__/ServerSecurityProvider-realm.spec.ts +21 -16
- package/src/{server/security/providers → security/__tests__}/ServerSecurityProvider.spec.ts +5 -5
- package/src/security/index.browser.ts +5 -0
- package/src/security/index.ts +90 -7
- package/src/security/primitives/{$realm.spec.ts → $issuer.spec.ts} +11 -11
- package/src/security/primitives/{$realm.ts → $issuer.ts} +20 -17
- package/src/security/primitives/$role.ts +5 -5
- package/src/security/primitives/$serviceAccount.spec.ts +5 -5
- package/src/security/primitives/$serviceAccount.ts +3 -3
- package/src/{server/security → security}/providers/ServerSecurityProvider.ts +5 -7
- package/src/server/auth/primitives/$auth.ts +10 -10
- package/src/server/auth/primitives/$authCredentials.ts +3 -3
- package/src/server/auth/primitives/$authGithub.ts +3 -3
- package/src/server/auth/primitives/$authGoogle.ts +3 -3
- package/src/server/auth/providers/ServerAuthProvider.ts +13 -13
- package/src/server/cache/providers/ServerCacheProvider.ts +1 -1
- package/src/server/cookies/providers/ServerCookiesProvider.ts +3 -3
- package/src/server/core/providers/NodeHttpServerProvider.ts +25 -6
- package/src/server/core/providers/ServerBodyParserProvider.ts +19 -23
- package/src/server/core/providers/ServerLoggerProvider.ts +23 -19
- package/src/server/core/providers/ServerProvider.ts +144 -21
- package/src/server/core/providers/ServerRouterProvider.ts +259 -115
- package/src/server/core/providers/ServerTimingProvider.ts +2 -2
- package/src/server/links/index.ts +1 -1
- package/src/server/links/providers/LinkProvider.ts +1 -1
- package/src/server/swagger/index.ts +1 -1
- package/src/sms/providers/LocalSmsProvider.spec.ts +153 -111
- package/src/sms/providers/LocalSmsProvider.ts +8 -7
- package/src/vite/helpers/boot.ts +28 -17
- package/src/vite/tasks/buildServer.ts +12 -1
- package/src/vite/tasks/devServer.ts +3 -1
- package/src/vite/tasks/generateCloudflare.ts +7 -0
- package/dist/server/security/index.browser.js +0 -13
- package/dist/server/security/index.browser.js.map +0 -1
- package/dist/server/security/index.d.ts +0 -173
- package/dist/server/security/index.d.ts.map +0 -1
- package/dist/server/security/index.js +0 -311
- package/dist/server/security/index.js.map +0 -1
- package/src/cli/assets/appRouterTs.ts +0 -9
- package/src/cli/assets/mainTs.ts +0 -13
- package/src/server/security/index.browser.ts +0 -10
- package/src/server/security/index.ts +0 -94
- /package/src/{server/security → security}/primitives/$basicAuth.ts +0 -0
- /package/src/{server/security → security}/providers/ServerBasicAuthProvider.ts +0 -0
package/dist/orm/index.js
CHANGED
|
@@ -1,23 +1,24 @@
|
|
|
1
|
+
import { n as __reExport, t as __exportAll } from "./chunk-DtkW-qnP.js";
|
|
1
2
|
import { createRequire } from "node:module";
|
|
2
3
|
import { $atom, $context, $env, $hook, $inject, $module, $use, Alepha, AlephaError, KIND, Primitive, Value, createPagination, createPrimitive, pageQuerySchema, pageSchema, pageSchema as pageSchema$1, t } from "alepha";
|
|
3
4
|
import { AlephaDateTime, DateTimeProvider } from "alepha/datetime";
|
|
5
|
+
import * as pg$2 from "drizzle-orm/pg-core";
|
|
6
|
+
import { alias, check, customType, foreignKey, index, pgEnum, pgSchema, pgTable, unique, uniqueIndex } from "drizzle-orm/pg-core";
|
|
4
7
|
import * as drizzle from "drizzle-orm";
|
|
5
8
|
import { and, arrayContained, arrayContains, arrayOverlaps, asc, between, desc, eq, getTableName, gt, gte, ilike, inArray, isNotNull, isNull, isSQLWrapper, like, lt, lte, ne, not, notBetween, notIlike, notInArray, notLike, or, sql, sql as sql$1 } from "drizzle-orm";
|
|
6
|
-
import * as pg$1 from "drizzle-orm/pg-core";
|
|
7
|
-
import { alias, check, customType, foreignKey, index, pgEnum, pgSchema, pgTable, unique, uniqueIndex } from "drizzle-orm/pg-core";
|
|
8
9
|
import { mkdir, readFile, stat, writeFile } from "node:fs/promises";
|
|
9
10
|
import { $logger } from "alepha/logger";
|
|
10
|
-
import {
|
|
11
|
+
import { $lock } from "alepha/lock";
|
|
11
12
|
import { randomUUID } from "node:crypto";
|
|
12
|
-
import * as pg$
|
|
13
|
+
import * as pg$1 from "drizzle-orm/sqlite-core";
|
|
13
14
|
import { check as check$1, foreignKey as foreignKey$1, index as index$1, sqliteTable, unique as unique$1, uniqueIndex as uniqueIndex$1 } from "drizzle-orm/sqlite-core";
|
|
14
|
-
import { $lock } from "alepha/lock";
|
|
15
15
|
import { drizzle as drizzle$1 } from "drizzle-orm/postgres-js";
|
|
16
16
|
import { migrate } from "drizzle-orm/postgres-js/migrator";
|
|
17
17
|
import postgres from "postgres";
|
|
18
18
|
import { drizzle as drizzle$2 } from "drizzle-orm/sqlite-proxy";
|
|
19
19
|
import { migrate as migrate$1 } from "drizzle-orm/sqlite-proxy/migrator";
|
|
20
20
|
import { migrate as migrate$2 } from "drizzle-orm/pglite/migrator";
|
|
21
|
+
import { isSQLWrapper as isSQLWrapper$1 } from "drizzle-orm/sql/sql";
|
|
21
22
|
import { $retry } from "alepha/retry";
|
|
22
23
|
|
|
23
24
|
export * from "drizzle-orm/pg-core"
|
|
@@ -139,74 +140,6 @@ var DbError = class extends AlephaError {
|
|
|
139
140
|
}
|
|
140
141
|
};
|
|
141
142
|
|
|
142
|
-
//#endregion
|
|
143
|
-
//#region ../../src/orm/errors/DbConflictError.ts
|
|
144
|
-
var DbConflictError = class extends DbError {
|
|
145
|
-
name = "DbConflictError";
|
|
146
|
-
status = 409;
|
|
147
|
-
};
|
|
148
|
-
|
|
149
|
-
//#endregion
|
|
150
|
-
//#region ../../src/orm/errors/DbEntityNotFoundError.ts
|
|
151
|
-
var DbEntityNotFoundError = class extends DbError {
|
|
152
|
-
name = "DbEntityNotFoundError";
|
|
153
|
-
status = 404;
|
|
154
|
-
constructor(entityName) {
|
|
155
|
-
super(`Entity from '${entityName}' was not found`);
|
|
156
|
-
}
|
|
157
|
-
};
|
|
158
|
-
|
|
159
|
-
//#endregion
|
|
160
|
-
//#region ../../src/orm/errors/DbVersionMismatchError.ts
|
|
161
|
-
/**
|
|
162
|
-
* Error thrown when there is a version mismatch.
|
|
163
|
-
* It's thrown by {@link Repository#save} when the updated entity version does not match the one in the database.
|
|
164
|
-
* This is used for optimistic concurrency control.
|
|
165
|
-
*/
|
|
166
|
-
var DbVersionMismatchError = class extends DbError {
|
|
167
|
-
name = "DbVersionMismatchError";
|
|
168
|
-
constructor(table, id) {
|
|
169
|
-
super(`Version mismatch for table '${table}' and id '${id}'`);
|
|
170
|
-
}
|
|
171
|
-
};
|
|
172
|
-
|
|
173
|
-
//#endregion
|
|
174
|
-
//#region ../../src/orm/helpers/pgAttr.ts
|
|
175
|
-
/**
|
|
176
|
-
* Decorates a typebox schema with a Postgres attribute.
|
|
177
|
-
*
|
|
178
|
-
* > It's just a fancy way to add Symbols to a field.
|
|
179
|
-
*
|
|
180
|
-
* @example
|
|
181
|
-
* ```ts
|
|
182
|
-
* import { t } from "alepha";
|
|
183
|
-
* import { PG_UPDATED_AT } from "../constants/PG_SYMBOLS";
|
|
184
|
-
*
|
|
185
|
-
* export const updatedAtSchema = pgAttr(
|
|
186
|
-
* t.datetime(), PG_UPDATED_AT,
|
|
187
|
-
* );
|
|
188
|
-
* ```
|
|
189
|
-
*/
|
|
190
|
-
const pgAttr = (type, attr, value) => {
|
|
191
|
-
Object.assign(type, { [attr]: value ?? {} });
|
|
192
|
-
return type;
|
|
193
|
-
};
|
|
194
|
-
/**
|
|
195
|
-
* Retrieves the fields of a schema that have a specific attribute.
|
|
196
|
-
*/
|
|
197
|
-
const getAttrFields = (schema$1, name) => {
|
|
198
|
-
const fields = [];
|
|
199
|
-
for (const key of Object.keys(schema$1.properties)) {
|
|
200
|
-
const value = schema$1.properties[key];
|
|
201
|
-
if (name in value) fields.push({
|
|
202
|
-
type: value,
|
|
203
|
-
key,
|
|
204
|
-
data: value[name]
|
|
205
|
-
});
|
|
206
|
-
}
|
|
207
|
-
return fields;
|
|
208
|
-
};
|
|
209
|
-
|
|
210
143
|
//#endregion
|
|
211
144
|
//#region ../../src/orm/providers/drivers/DatabaseProvider.ts
|
|
212
145
|
var DatabaseProvider = class {
|
|
@@ -218,6 +151,9 @@ var DatabaseProvider = class {
|
|
|
218
151
|
get name() {
|
|
219
152
|
return "default";
|
|
220
153
|
}
|
|
154
|
+
get driver() {
|
|
155
|
+
return this.dialect;
|
|
156
|
+
}
|
|
221
157
|
get schema() {
|
|
222
158
|
return "public";
|
|
223
159
|
}
|
|
@@ -302,1899 +238,2195 @@ var DatabaseProvider = class {
|
|
|
302
238
|
};
|
|
303
239
|
|
|
304
240
|
//#endregion
|
|
305
|
-
//#region ../../src/orm/
|
|
306
|
-
|
|
241
|
+
//#region ../../src/orm/primitives/$sequence.ts
|
|
242
|
+
/**
|
|
243
|
+
* Creates a PostgreSQL sequence primitive for generating unique numeric values.
|
|
244
|
+
*/
|
|
245
|
+
const $sequence = (options = {}) => {
|
|
246
|
+
return createPrimitive(SequencePrimitive, options);
|
|
247
|
+
};
|
|
248
|
+
var SequencePrimitive = class extends Primitive {
|
|
249
|
+
provider = this.$provider();
|
|
250
|
+
onInit() {
|
|
251
|
+
this.provider.registerSequence(this);
|
|
252
|
+
}
|
|
253
|
+
get name() {
|
|
254
|
+
return this.options.name ?? this.config.propertyKey;
|
|
255
|
+
}
|
|
256
|
+
async next() {
|
|
257
|
+
return this.provider.execute(sql$1`SELECT nextval('${sql$1.raw(this.provider.schema)}."${sql$1.raw(this.name)}"')`).then((rows) => Number(rows[0]?.nextval));
|
|
258
|
+
}
|
|
259
|
+
async current() {
|
|
260
|
+
return this.provider.execute(sql$1`SELECT last_value FROM ${sql$1.raw(this.provider.schema)}."${sql$1.raw(this.name)}"`).then((rows) => Number(rows[0]?.last_value));
|
|
261
|
+
}
|
|
262
|
+
$provider() {
|
|
263
|
+
return this.options.provider ?? this.alepha.inject(DatabaseProvider);
|
|
264
|
+
}
|
|
265
|
+
};
|
|
266
|
+
$sequence[KIND] = SequencePrimitive;
|
|
267
|
+
|
|
268
|
+
//#endregion
|
|
269
|
+
//#region ../../src/orm/providers/DrizzleKitProvider.ts
|
|
270
|
+
var DrizzleKitProvider = class {
|
|
271
|
+
log = $logger();
|
|
272
|
+
alepha = $inject(Alepha);
|
|
307
273
|
/**
|
|
308
|
-
*
|
|
274
|
+
* Synchronize database with current schema definitions.
|
|
275
|
+
*
|
|
276
|
+
* In development mode, it will generate and execute migrations based on the current state.
|
|
277
|
+
* In testing mode, it will generate migrations from scratch without applying them.
|
|
278
|
+
*
|
|
279
|
+
* Does nothing in production mode, you must handle migrations manually.
|
|
309
280
|
*/
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
281
|
+
async synchronize(provider) {
|
|
282
|
+
if (this.alepha.isProduction()) {
|
|
283
|
+
this.log.warn("Synchronization skipped in production mode.");
|
|
284
|
+
return;
|
|
285
|
+
}
|
|
286
|
+
if (provider.schema !== "public") await this.createSchemaIfNotExists(provider, provider.schema);
|
|
287
|
+
const now = Date.now();
|
|
288
|
+
if (this.alepha.isTest()) {
|
|
289
|
+
const { statements } = await this.generateMigration(provider);
|
|
290
|
+
await this.executeStatements(statements, provider);
|
|
291
|
+
} else {
|
|
292
|
+
const entry = await this.loadDevMigrations(provider);
|
|
293
|
+
const { statements, snapshot } = await this.generateMigration(provider, entry?.snapshot ? JSON.parse(entry.snapshot) : void 0);
|
|
294
|
+
await this.executeStatements(statements, provider, true);
|
|
295
|
+
await this.saveDevMigrations(provider, snapshot, entry);
|
|
325
296
|
}
|
|
297
|
+
this.log.info(`Db '${provider.name}' synchronization OK [${Date.now() - now}ms]`);
|
|
326
298
|
}
|
|
327
299
|
/**
|
|
328
|
-
*
|
|
300
|
+
* Mostly used for testing purposes. You can generate SQL migration statements without executing them.
|
|
329
301
|
*/
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
302
|
+
async generateMigration(provider, prevSnapshot) {
|
|
303
|
+
const kit = this.importDrizzleKit();
|
|
304
|
+
const models = this.getModels(provider);
|
|
305
|
+
if (Object.keys(models).length > 0) {
|
|
306
|
+
if (provider.dialect === "sqlite") {
|
|
307
|
+
const prev$1 = prevSnapshot ?? await kit.generateSQLiteDrizzleJson({});
|
|
308
|
+
const curr$1 = await kit.generateSQLiteDrizzleJson(models);
|
|
309
|
+
return {
|
|
310
|
+
models,
|
|
311
|
+
statements: await kit.generateSQLiteMigration(prev$1, curr$1),
|
|
312
|
+
snapshot: curr$1
|
|
313
|
+
};
|
|
337
314
|
}
|
|
315
|
+
const prev = prevSnapshot ?? await kit.generateDrizzleJson({});
|
|
316
|
+
const curr = await kit.generateDrizzleJson(models);
|
|
317
|
+
return {
|
|
318
|
+
models,
|
|
319
|
+
statements: await kit.generateMigration(prev, curr),
|
|
320
|
+
snapshot: curr
|
|
321
|
+
};
|
|
338
322
|
}
|
|
339
|
-
return
|
|
323
|
+
return {
|
|
324
|
+
models,
|
|
325
|
+
statements: [],
|
|
326
|
+
snapshot: {}
|
|
327
|
+
};
|
|
340
328
|
}
|
|
341
329
|
/**
|
|
342
|
-
*
|
|
330
|
+
* Load all tables, enums, sequences, etc. from the provider's repositories.
|
|
343
331
|
*/
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
332
|
+
getModels(provider) {
|
|
333
|
+
const models = {};
|
|
334
|
+
for (const [key, value] of provider.tables.entries()) {
|
|
335
|
+
if (models[key]) throw new AlephaError(`Model name conflict: '${key}' is already defined.`);
|
|
336
|
+
models[key] = value;
|
|
337
|
+
}
|
|
338
|
+
for (const [key, value] of provider.enums.entries()) {
|
|
339
|
+
if (models[key]) throw new AlephaError(`Model name conflict: '${key}' is already defined.`);
|
|
340
|
+
models[key] = value;
|
|
341
|
+
}
|
|
342
|
+
for (const [key, value] of provider.sequences.entries()) {
|
|
343
|
+
if (models[key]) throw new AlephaError(`Model name conflict: '${key}' is already defined.`);
|
|
344
|
+
models[key] = value;
|
|
345
|
+
}
|
|
346
|
+
return models;
|
|
348
347
|
}
|
|
349
348
|
/**
|
|
350
|
-
*
|
|
349
|
+
* Load the migration snapshot from the database.
|
|
351
350
|
*/
|
|
352
|
-
|
|
353
|
-
const
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
const childJoins = joins.filter((j) => j.parent === joinPath);
|
|
358
|
-
let joinSchema = join.schema;
|
|
359
|
-
if (childJoins.length > 0) joinSchema = this.buildSchemaWithJoins(join.schema, joins, joinPath);
|
|
360
|
-
schema$1.properties[join.key] = t.optional(joinSchema);
|
|
351
|
+
async loadDevMigrations(provider) {
|
|
352
|
+
const name = `${this.alepha.env.APP_NAME ?? "APP"}-${provider.constructor.name}`.toLowerCase();
|
|
353
|
+
if (provider.url.includes(":memory:")) {
|
|
354
|
+
this.log.trace(`In-memory database detected for '${name}', skipping migration snapshot load.`);
|
|
355
|
+
return;
|
|
361
356
|
}
|
|
362
|
-
|
|
357
|
+
if (provider.dialect === "sqlite") {
|
|
358
|
+
try {
|
|
359
|
+
const text = await readFile(`node_modules/.alepha/sqlite-${name}.json`, "utf-8");
|
|
360
|
+
return this.alepha.codec.decode(devMigrationsSchema, text);
|
|
361
|
+
} catch (e) {
|
|
362
|
+
this.log.trace(`No existing migration snapshot for '${name}'`, e);
|
|
363
|
+
}
|
|
364
|
+
return;
|
|
365
|
+
}
|
|
366
|
+
await provider.execute(sql$1`CREATE SCHEMA IF NOT EXISTS "drizzle";`);
|
|
367
|
+
await provider.execute(sql$1`
|
|
368
|
+
CREATE TABLE IF NOT EXISTS "drizzle"."__drizzle_dev_migrations" (
|
|
369
|
+
"id" SERIAL PRIMARY KEY,
|
|
370
|
+
"name" TEXT NOT NULL,
|
|
371
|
+
"created_at" TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
372
|
+
"snapshot" TEXT NOT NULL
|
|
373
|
+
);
|
|
374
|
+
`);
|
|
375
|
+
const rows = await provider.run(sql$1`SELECT * FROM "drizzle"."__drizzle_dev_migrations" WHERE "name" = ${name} LIMIT 1`, devMigrationsSchema);
|
|
376
|
+
if (rows.length === 0) {
|
|
377
|
+
this.log.trace(`No existing migration snapshot for '${name}'`);
|
|
378
|
+
return;
|
|
379
|
+
}
|
|
380
|
+
return this.alepha.codec.decode(devMigrationsSchema, rows[0]);
|
|
363
381
|
}
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
382
|
+
async saveDevMigrations(provider, curr, devMigrations) {
|
|
383
|
+
if (provider.url.includes(":memory:")) {
|
|
384
|
+
this.log.trace(`In-memory database detected for '${provider.constructor.name}', skipping migration snapshot save.`);
|
|
385
|
+
return;
|
|
386
|
+
}
|
|
387
|
+
const name = `${this.alepha.env.APP_NAME ?? "APP"}-${provider.constructor.name}`.toLowerCase();
|
|
388
|
+
if (provider.dialect === "sqlite") {
|
|
389
|
+
const filePath = `node_modules/.alepha/sqlite-${name}.json`;
|
|
390
|
+
await mkdir("node_modules/.alepha", { recursive: true }).catch(() => null);
|
|
391
|
+
await writeFile(filePath, JSON.stringify({
|
|
392
|
+
id: devMigrations?.id ?? 1,
|
|
393
|
+
name,
|
|
394
|
+
created_at: /* @__PURE__ */ new Date(),
|
|
395
|
+
snapshot: JSON.stringify(curr)
|
|
396
|
+
}, null, 2));
|
|
397
|
+
this.log.debug(`Saved migration snapshot to '${filePath}'`);
|
|
398
|
+
return;
|
|
399
|
+
}
|
|
400
|
+
if (!devMigrations) await provider.execute(sql$1`INSERT INTO "drizzle"."__drizzle_dev_migrations" ("name", "snapshot") VALUES (${name}, ${JSON.stringify(curr)})`);
|
|
377
401
|
else {
|
|
378
|
-
const
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
};
|
|
395
|
-
});
|
|
396
|
-
const sql$2 = this.toSQL(query[key], {
|
|
397
|
-
schema: join.schema,
|
|
398
|
-
col: join.col,
|
|
399
|
-
joins: recursiveJoins.length > 0 ? recursiveJoins : void 0,
|
|
400
|
-
dialect: options.dialect
|
|
401
|
-
});
|
|
402
|
-
if (sql$2) conditions.push(sql$2);
|
|
403
|
-
continue;
|
|
404
|
-
}
|
|
405
|
-
}
|
|
406
|
-
if (Array.isArray(operator)) {
|
|
407
|
-
const operations = operator.map((it) => {
|
|
408
|
-
if (isSQLWrapper(it)) return it;
|
|
409
|
-
return this.toSQL(it, {
|
|
410
|
-
schema: schema$1,
|
|
411
|
-
col,
|
|
412
|
-
joins,
|
|
413
|
-
dialect: options.dialect
|
|
414
|
-
});
|
|
415
|
-
}).filter((it) => it != null);
|
|
416
|
-
if (key === "and") return and(...operations);
|
|
417
|
-
if (key === "or") return or(...operations);
|
|
418
|
-
}
|
|
419
|
-
if (key === "not") {
|
|
420
|
-
const where = this.toSQL(operator, {
|
|
421
|
-
schema: schema$1,
|
|
422
|
-
col,
|
|
423
|
-
joins,
|
|
424
|
-
dialect: options.dialect
|
|
425
|
-
});
|
|
426
|
-
if (where) return not(where);
|
|
427
|
-
}
|
|
428
|
-
if (operator) {
|
|
429
|
-
const column = col(key);
|
|
430
|
-
const sql$2 = this.mapOperatorToSql(operator, column, schema$1, key, options.dialect);
|
|
431
|
-
if (sql$2) conditions.push(sql$2);
|
|
432
|
-
}
|
|
402
|
+
const newSnapshot = JSON.stringify(curr);
|
|
403
|
+
if (devMigrations.snapshot !== newSnapshot) await provider.execute(sql$1`UPDATE "drizzle"."__drizzle_dev_migrations" SET "snapshot" = ${newSnapshot} WHERE "id" = ${devMigrations.id}`);
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
async executeStatements(statements, provider, catchErrors = false) {
|
|
407
|
+
let nErrors = 0;
|
|
408
|
+
for (const statement of statements) {
|
|
409
|
+
if (statement.startsWith("DROP SCHEMA")) continue;
|
|
410
|
+
try {
|
|
411
|
+
await provider.execute(sql$1.raw(statement));
|
|
412
|
+
} catch (error) {
|
|
413
|
+
const errorMessage = `Error executing statement: ${statement}`;
|
|
414
|
+
if (catchErrors) {
|
|
415
|
+
nErrors++;
|
|
416
|
+
this.log.warn(errorMessage, { context: [error] });
|
|
417
|
+
} else throw error;
|
|
433
418
|
}
|
|
434
419
|
}
|
|
435
|
-
if (
|
|
436
|
-
return and(...conditions);
|
|
420
|
+
if (nErrors > 0) this.log.warn(`Executed ${statements.length} statements with ${nErrors} errors.`);
|
|
437
421
|
}
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
"gte",
|
|
448
|
-
"lt",
|
|
449
|
-
"lte",
|
|
450
|
-
"inArray",
|
|
451
|
-
"notInArray",
|
|
452
|
-
"isNull",
|
|
453
|
-
"isNotNull",
|
|
454
|
-
"like",
|
|
455
|
-
"notLike",
|
|
456
|
-
"ilike",
|
|
457
|
-
"notIlike",
|
|
458
|
-
"contains",
|
|
459
|
-
"startsWith",
|
|
460
|
-
"endsWith",
|
|
461
|
-
"between",
|
|
462
|
-
"notBetween",
|
|
463
|
-
"arrayContains",
|
|
464
|
-
"arrayContained",
|
|
465
|
-
"arrayOverlaps"
|
|
466
|
-
].some((key) => key in obj);
|
|
422
|
+
async createSchemaIfNotExists(provider, schemaName) {
|
|
423
|
+
if (!/^[a-z0-9_]+$/i.test(schemaName)) throw new Error(`Invalid schema name: ${schemaName}. Must only contain alphanumeric characters and underscores.`);
|
|
424
|
+
const sqlSchema = sql$1.raw(schemaName);
|
|
425
|
+
if (schemaName.startsWith("test_")) {
|
|
426
|
+
this.log.info(`Drop test schema '${schemaName}' ...`, schemaName);
|
|
427
|
+
await provider.execute(sql$1`DROP SCHEMA IF EXISTS ${sqlSchema} CASCADE`);
|
|
428
|
+
}
|
|
429
|
+
this.log.debug(`Ensuring schema '${schemaName}' exists`);
|
|
430
|
+
await provider.execute(sql$1`CREATE SCHEMA IF NOT EXISTS ${sqlSchema}`);
|
|
467
431
|
}
|
|
468
432
|
/**
|
|
469
|
-
*
|
|
433
|
+
* Try to load the official Drizzle Kit API.
|
|
434
|
+
* If not available, fallback to the local kit import.
|
|
470
435
|
*/
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
if (fieldSchema) return this.alepha.codec.encode(fieldSchema, value, { encoder: "drizzle" });
|
|
477
|
-
} catch (error) {}
|
|
478
|
-
return value;
|
|
479
|
-
};
|
|
480
|
-
const encodeArray = (values) => {
|
|
481
|
-
return values.map((v) => encodeValue(v));
|
|
482
|
-
};
|
|
483
|
-
if (typeof operator !== "object" || operator == null || !this.hasFilterOperatorProperties(operator)) return eq(column, encodeValue(operator));
|
|
484
|
-
const conditions = [];
|
|
485
|
-
if (operator?.eq != null) conditions.push(eq(column, encodeValue(operator.eq)));
|
|
486
|
-
if (operator?.ne != null) conditions.push(ne(column, encodeValue(operator.ne)));
|
|
487
|
-
if (operator?.gt != null) conditions.push(gt(column, encodeValue(operator.gt)));
|
|
488
|
-
if (operator?.gte != null) conditions.push(gte(column, encodeValue(operator.gte)));
|
|
489
|
-
if (operator?.lt != null) conditions.push(lt(column, encodeValue(operator.lt)));
|
|
490
|
-
if (operator?.lte != null) conditions.push(lte(column, encodeValue(operator.lte)));
|
|
491
|
-
if (operator?.inArray != null) {
|
|
492
|
-
if (!Array.isArray(operator.inArray) || operator.inArray.length === 0) throw new AlephaError("inArray operator requires at least one value");
|
|
493
|
-
conditions.push(inArray(column, encodeArray(operator.inArray)));
|
|
494
|
-
}
|
|
495
|
-
if (operator?.notInArray != null) {
|
|
496
|
-
if (!Array.isArray(operator.notInArray) || operator.notInArray.length === 0) throw new AlephaError("notInArray operator requires at least one value");
|
|
497
|
-
conditions.push(notInArray(column, encodeArray(operator.notInArray)));
|
|
498
|
-
}
|
|
499
|
-
if (operator?.isNull != null) conditions.push(isNull(column));
|
|
500
|
-
if (operator?.isNotNull != null) conditions.push(isNotNull(column));
|
|
501
|
-
if (operator?.like != null) conditions.push(like(column, encodeValue(operator.like)));
|
|
502
|
-
if (operator?.notLike != null) conditions.push(notLike(column, encodeValue(operator.notLike)));
|
|
503
|
-
if (operator?.ilike != null) conditions.push(ilike(column, encodeValue(operator.ilike)));
|
|
504
|
-
if (operator?.notIlike != null) conditions.push(notIlike(column, encodeValue(operator.notIlike)));
|
|
505
|
-
if (operator?.contains != null) {
|
|
506
|
-
const escapedValue = String(operator.contains).replace(/\\/g, "\\\\").replace(/%/g, "\\%").replace(/_/g, "\\_");
|
|
507
|
-
if (dialect === "sqlite") conditions.push(sql$1`LOWER(${column}) LIKE LOWER(${encodeValue(`%${escapedValue}%`)})`);
|
|
508
|
-
else conditions.push(ilike(column, encodeValue(`%${escapedValue}%`)));
|
|
509
|
-
}
|
|
510
|
-
if (operator?.startsWith != null) {
|
|
511
|
-
const escapedValue = String(operator.startsWith).replace(/\\/g, "\\\\").replace(/%/g, "\\%").replace(/_/g, "\\_");
|
|
512
|
-
if (dialect === "sqlite") conditions.push(sql$1`LOWER(${column}) LIKE LOWER(${encodeValue(`${escapedValue}%`)})`);
|
|
513
|
-
else conditions.push(ilike(column, encodeValue(`${escapedValue}%`)));
|
|
514
|
-
}
|
|
515
|
-
if (operator?.endsWith != null) {
|
|
516
|
-
const escapedValue = String(operator.endsWith).replace(/\\/g, "\\\\").replace(/%/g, "\\%").replace(/_/g, "\\_");
|
|
517
|
-
if (dialect === "sqlite") conditions.push(sql$1`LOWER(${column}) LIKE LOWER(${encodeValue(`%${escapedValue}`)})`);
|
|
518
|
-
else conditions.push(ilike(column, encodeValue(`%${escapedValue}`)));
|
|
519
|
-
}
|
|
520
|
-
if (operator?.between != null) {
|
|
521
|
-
if (!Array.isArray(operator.between) || operator.between.length !== 2) throw new Error("between operator requires exactly 2 values [min, max]");
|
|
522
|
-
conditions.push(between(column, encodeValue(operator.between[0]), encodeValue(operator.between[1])));
|
|
523
|
-
}
|
|
524
|
-
if (operator?.notBetween != null) {
|
|
525
|
-
if (!Array.isArray(operator.notBetween) || operator.notBetween.length !== 2) throw new Error("notBetween operator requires exactly 2 values [min, max]");
|
|
526
|
-
conditions.push(notBetween(column, encodeValue(operator.notBetween[0]), encodeValue(operator.notBetween[1])));
|
|
436
|
+
importDrizzleKit() {
|
|
437
|
+
try {
|
|
438
|
+
return createRequire(import.meta.url)("drizzle-kit/api");
|
|
439
|
+
} catch (_) {
|
|
440
|
+
throw new Error("Drizzle Kit is not installed. Please install it with `npm install -D drizzle-kit`.");
|
|
527
441
|
}
|
|
528
|
-
if (operator?.arrayContains != null) conditions.push(arrayContains(column, encodeValue(operator.arrayContains)));
|
|
529
|
-
if (operator?.arrayContained != null) conditions.push(arrayContained(column, encodeValue(operator.arrayContained)));
|
|
530
|
-
if (operator?.arrayOverlaps != null) conditions.push(arrayOverlaps(column, encodeValue(operator.arrayOverlaps)));
|
|
531
|
-
if (conditions.length === 0) return;
|
|
532
|
-
if (conditions.length === 1) return conditions[0];
|
|
533
|
-
return and(...conditions);
|
|
534
442
|
}
|
|
443
|
+
};
|
|
444
|
+
const devMigrationsSchema = t.object({
|
|
445
|
+
id: t.number(),
|
|
446
|
+
name: t.text(),
|
|
447
|
+
snapshot: t.string(),
|
|
448
|
+
created_at: t.string()
|
|
449
|
+
});
|
|
450
|
+
|
|
451
|
+
//#endregion
|
|
452
|
+
//#region ../../src/orm/errors/DbMigrationError.ts
|
|
453
|
+
var DbMigrationError = class extends DbError {
|
|
454
|
+
name = "DbMigrationError";
|
|
455
|
+
constructor(cause) {
|
|
456
|
+
super("Failed to migrate database", cause);
|
|
457
|
+
}
|
|
458
|
+
};
|
|
459
|
+
|
|
460
|
+
//#endregion
|
|
461
|
+
//#region ../../src/orm/types/byte.ts
|
|
462
|
+
/**
|
|
463
|
+
* Postgres bytea type.
|
|
464
|
+
*/
|
|
465
|
+
const byte = customType({ dataType: () => "bytea" });
|
|
466
|
+
|
|
467
|
+
//#endregion
|
|
468
|
+
//#region ../../src/orm/services/ModelBuilder.ts
|
|
469
|
+
/**
|
|
470
|
+
* Abstract base class for transforming Alepha Primitives (Entity, Sequence, etc...)
|
|
471
|
+
* into drizzle models (tables, enums, sequences, etc...).
|
|
472
|
+
*/
|
|
473
|
+
var ModelBuilder = class {
|
|
535
474
|
/**
|
|
536
|
-
*
|
|
537
|
-
* Format: "firstName,-lastName" -> [{ column: "firstName", direction: "asc" }, { column: "lastName", direction: "desc" }]
|
|
538
|
-
* - Columns separated by comma
|
|
539
|
-
* - Prefix with '-' for DESC direction
|
|
540
|
-
*
|
|
541
|
-
* @param sort Pagination sort string
|
|
542
|
-
* @returns OrderBy array or single object
|
|
475
|
+
* Convert camelCase to snake_case for column names.
|
|
543
476
|
*/
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
if (field.startsWith("-")) return {
|
|
547
|
-
column: field.substring(1),
|
|
548
|
-
direction: "desc"
|
|
549
|
-
};
|
|
550
|
-
return {
|
|
551
|
-
column: field,
|
|
552
|
-
direction: "asc"
|
|
553
|
-
};
|
|
554
|
-
});
|
|
555
|
-
return orderByClauses.length === 1 ? orderByClauses[0] : orderByClauses;
|
|
477
|
+
toColumnName(str) {
|
|
478
|
+
return str[0].toLowerCase() + str.slice(1).replace(/[A-Z]/g, (letter) => `_${letter.toLowerCase()}`);
|
|
556
479
|
}
|
|
557
480
|
/**
|
|
558
|
-
*
|
|
559
|
-
*
|
|
560
|
-
* 1. String: "name" -> [{ column: "name", direction: "asc" }]
|
|
561
|
-
* 2. Object: { column: "name", direction: "desc" } -> [{ column: "name", direction: "desc" }]
|
|
562
|
-
* 3. Array: [{ column: "name" }, { column: "age", direction: "desc" }] -> normalized array
|
|
563
|
-
*
|
|
564
|
-
* @param orderBy The orderBy parameter
|
|
565
|
-
* @returns Normalized array of order by clauses
|
|
566
|
-
*/
|
|
567
|
-
normalizeOrderBy(orderBy) {
|
|
568
|
-
if (typeof orderBy === "string") return [{
|
|
569
|
-
column: orderBy,
|
|
570
|
-
direction: "asc"
|
|
571
|
-
}];
|
|
572
|
-
if (!Array.isArray(orderBy) && typeof orderBy === "object") return [{
|
|
573
|
-
column: orderBy.column,
|
|
574
|
-
direction: orderBy.direction ?? "asc"
|
|
575
|
-
}];
|
|
576
|
-
if (Array.isArray(orderBy)) return orderBy.map((item) => ({
|
|
577
|
-
column: item.column,
|
|
578
|
-
direction: item.direction ?? "asc"
|
|
579
|
-
}));
|
|
580
|
-
return [];
|
|
581
|
-
}
|
|
582
|
-
/**
|
|
583
|
-
* Create a pagination object.
|
|
584
|
-
*
|
|
585
|
-
* @deprecated Use `createPagination` from alepha instead.
|
|
586
|
-
* This method now delegates to the framework-level helper.
|
|
481
|
+
* Build the table configuration function for any database.
|
|
482
|
+
* This includes indexes, foreign keys, constraints, and custom config.
|
|
587
483
|
*
|
|
588
|
-
* @param
|
|
589
|
-
* @param
|
|
590
|
-
* @param
|
|
591
|
-
* @param
|
|
484
|
+
* @param entity - The entity primitive
|
|
485
|
+
* @param builders - Database-specific builder functions
|
|
486
|
+
* @param tableResolver - Function to resolve entity references to table columns
|
|
487
|
+
* @param customConfigHandler - Optional handler for custom config
|
|
592
488
|
*/
|
|
593
|
-
|
|
594
|
-
|
|
489
|
+
buildTableConfig(entity, builders, tableResolver, customConfigHandler) {
|
|
490
|
+
if (!entity.options.indexes && !entity.options.foreignKeys && !entity.options.constraints && !entity.options.config) return;
|
|
491
|
+
return (self) => {
|
|
492
|
+
const configs = [];
|
|
493
|
+
if (entity.options.indexes) {
|
|
494
|
+
for (const indexDef of entity.options.indexes) if (typeof indexDef === "string") {
|
|
495
|
+
const columnName = this.toColumnName(indexDef);
|
|
496
|
+
const indexName = `${entity.name}_${columnName}_idx`;
|
|
497
|
+
if (self[indexDef]) configs.push(builders.index(indexName).on(self[indexDef]));
|
|
498
|
+
} else if (typeof indexDef === "object" && indexDef !== null) {
|
|
499
|
+
if ("column" in indexDef) {
|
|
500
|
+
const columnName = this.toColumnName(indexDef.column);
|
|
501
|
+
const indexName = indexDef.name || `${entity.name}_${columnName}_idx`;
|
|
502
|
+
if (self[indexDef.column]) if (indexDef.unique) configs.push(builders.uniqueIndex(indexName).on(self[indexDef.column]));
|
|
503
|
+
else configs.push(builders.index(indexName).on(self[indexDef.column]));
|
|
504
|
+
} else if ("columns" in indexDef) {
|
|
505
|
+
const columnNames = indexDef.columns.map((col) => this.toColumnName(col));
|
|
506
|
+
const indexName = indexDef.name || `${entity.name}_${columnNames.join("_")}_idx`;
|
|
507
|
+
const cols = indexDef.columns.map((col) => self[col]).filter(Boolean);
|
|
508
|
+
if (cols.length === indexDef.columns.length) if (indexDef.unique) configs.push(builders.uniqueIndex(indexName).on(...cols));
|
|
509
|
+
else configs.push(builders.index(indexName).on(...cols));
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
if (entity.options.foreignKeys) for (const fkDef of entity.options.foreignKeys) {
|
|
514
|
+
const columnNames = fkDef.columns.map((col) => this.toColumnName(col));
|
|
515
|
+
const cols = fkDef.columns.map((col) => self[col]).filter(Boolean);
|
|
516
|
+
if (cols.length === fkDef.columns.length) {
|
|
517
|
+
const fkName = fkDef.name || `${entity.name}_${columnNames.join("_")}_fk`;
|
|
518
|
+
const foreignColumns = fkDef.foreignColumns.map((colRef) => {
|
|
519
|
+
const entityCol = colRef();
|
|
520
|
+
if (!entityCol || !entityCol.entity || !entityCol.name) throw new Error(`Invalid foreign column reference in ${entity.name}`);
|
|
521
|
+
if (tableResolver) {
|
|
522
|
+
const foreignTable = tableResolver(entityCol.entity.name);
|
|
523
|
+
if (!foreignTable) throw new Error(`Foreign table ${entityCol.entity.name} not found for ${entity.name}`);
|
|
524
|
+
return foreignTable[entityCol.name];
|
|
525
|
+
}
|
|
526
|
+
return entityCol;
|
|
527
|
+
});
|
|
528
|
+
configs.push(builders.foreignKey({
|
|
529
|
+
name: fkName,
|
|
530
|
+
columns: cols,
|
|
531
|
+
foreignColumns
|
|
532
|
+
}));
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
if (entity.options.constraints) for (const constraintDef of entity.options.constraints) {
|
|
536
|
+
const columnNames = constraintDef.columns.map((col) => this.toColumnName(col));
|
|
537
|
+
const cols = constraintDef.columns.map((col) => self[col]).filter(Boolean);
|
|
538
|
+
if (cols.length === constraintDef.columns.length) {
|
|
539
|
+
if (constraintDef.unique) {
|
|
540
|
+
const constraintName = constraintDef.name || `${entity.name}_${columnNames.join("_")}_unique`;
|
|
541
|
+
configs.push(builders.unique(constraintName).on(...cols));
|
|
542
|
+
}
|
|
543
|
+
if (constraintDef.check) {
|
|
544
|
+
const constraintName = constraintDef.name || `${entity.name}_${columnNames.join("_")}_check`;
|
|
545
|
+
configs.push(builders.check(constraintName, constraintDef.check));
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
if (entity.options.config && customConfigHandler) configs.push(...customConfigHandler(entity.options.config, self));
|
|
550
|
+
else if (entity.options.config) {
|
|
551
|
+
const customConfigs = entity.options.config(self);
|
|
552
|
+
if (Array.isArray(customConfigs)) configs.push(...customConfigs);
|
|
553
|
+
}
|
|
554
|
+
return configs;
|
|
555
|
+
};
|
|
595
556
|
}
|
|
596
557
|
};
|
|
597
558
|
|
|
598
559
|
//#endregion
|
|
599
|
-
//#region ../../src/orm/services/
|
|
600
|
-
var
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
this.provider.registerEntity(entity);
|
|
611
|
-
}
|
|
612
|
-
/**
|
|
613
|
-
* Represents the primary key of the table.
|
|
614
|
-
* - Key is the name of the primary key column.
|
|
615
|
-
* - Type is the type (TypeBox) of the primary key column.
|
|
616
|
-
*
|
|
617
|
-
* ID is mandatory. If the table does not have a primary key, it will throw an error.
|
|
618
|
-
*/
|
|
619
|
-
get id() {
|
|
620
|
-
return this.getPrimaryKey(this.entity.schema);
|
|
621
|
-
}
|
|
622
|
-
/**
|
|
623
|
-
* Get Drizzle table object.
|
|
624
|
-
*/
|
|
625
|
-
get table() {
|
|
626
|
-
return this.provider.table(this.entity);
|
|
560
|
+
//#region ../../src/orm/services/PostgresModelBuilder.ts
|
|
561
|
+
var PostgresModelBuilder = class extends ModelBuilder {
|
|
562
|
+
schemas = /* @__PURE__ */ new Map();
|
|
563
|
+
getPgSchema(name) {
|
|
564
|
+
if (!this.schemas.has(name) && name !== "public") this.schemas.set(name, pgSchema(name));
|
|
565
|
+
const nsp = name !== "public" ? this.schemas.get(name) : {
|
|
566
|
+
enum: pgEnum,
|
|
567
|
+
table: pgTable
|
|
568
|
+
};
|
|
569
|
+
if (!nsp) throw new AlephaError(`Postgres schema ${name} not found`);
|
|
570
|
+
return nsp;
|
|
627
571
|
}
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
572
|
+
buildTable(entity, options) {
|
|
573
|
+
const tableName = entity.name;
|
|
574
|
+
if (options.tables.has(tableName)) return;
|
|
575
|
+
const nsp = this.getPgSchema(options.schema);
|
|
576
|
+
const columns = this.schemaToPgColumns(tableName, entity.schema, nsp, options.enums, options.tables);
|
|
577
|
+
const configFn = this.getTableConfig(entity, options.tables);
|
|
578
|
+
const table = nsp.table(tableName, columns, configFn);
|
|
579
|
+
options.tables.set(tableName, table);
|
|
633
580
|
}
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
581
|
+
buildSequence(sequence, options) {
|
|
582
|
+
const sequenceName = sequence.name;
|
|
583
|
+
if (options.sequences.has(sequenceName)) return;
|
|
584
|
+
const nsp = this.getPgSchema(options.schema);
|
|
585
|
+
options.sequences.set(sequenceName, nsp.sequence(sequenceName, sequence.options));
|
|
639
586
|
}
|
|
640
587
|
/**
|
|
641
|
-
*
|
|
642
|
-
*
|
|
643
|
-
* This method allows executing raw SQL queries against the database.
|
|
644
|
-
* This is by far the easiest way to run custom queries that are not covered by the repository's built-in methods!
|
|
645
|
-
*
|
|
646
|
-
* You must use the `sql` tagged template function from Drizzle ORM to create the query. https://orm.drizzle.team/docs/sql
|
|
647
|
-
*
|
|
648
|
-
* @example
|
|
649
|
-
* ```ts
|
|
650
|
-
* class App {
|
|
651
|
-
* repository = $repository({ ... });
|
|
652
|
-
* async getAdults() {
|
|
653
|
-
* const users = repository.table; // Drizzle table object
|
|
654
|
-
* await repository.query(sql`SELECT * FROM ${users} WHERE ${users.age} > ${18}`);
|
|
655
|
-
* // or better
|
|
656
|
-
* await repository.query((users) => sql`SELECT * FROM ${users} WHERE ${users.age} > ${18}`);
|
|
657
|
-
* }
|
|
658
|
-
* }
|
|
659
|
-
* ```
|
|
588
|
+
* Get PostgreSQL-specific config builder for the table.
|
|
660
589
|
*/
|
|
661
|
-
|
|
662
|
-
const
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
590
|
+
getTableConfig(entity, tables) {
|
|
591
|
+
const pgBuilders = {
|
|
592
|
+
index,
|
|
593
|
+
uniqueIndex,
|
|
594
|
+
unique,
|
|
595
|
+
check,
|
|
596
|
+
foreignKey
|
|
597
|
+
};
|
|
598
|
+
const tableResolver = (entityName) => {
|
|
599
|
+
return tables.get(entityName);
|
|
600
|
+
};
|
|
601
|
+
return this.buildTableConfig(entity, pgBuilders, tableResolver);
|
|
667
602
|
}
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
603
|
+
schemaToPgColumns = (tableName, schema$1, nsp, enums, tables) => {
|
|
604
|
+
return Object.entries(schema$1.properties).reduce((columns, [key, value]) => {
|
|
605
|
+
let col = this.mapFieldToColumn(tableName, key, value, nsp, enums);
|
|
606
|
+
if ("default" in value && value.default != null) col = col.default(value.default);
|
|
607
|
+
if (PG_PRIMARY_KEY in value) col = col.primaryKey();
|
|
608
|
+
if (PG_REF in value) {
|
|
609
|
+
const config = value[PG_REF];
|
|
610
|
+
col = col.references(() => {
|
|
611
|
+
const ref = config.ref();
|
|
612
|
+
const table = tables.get(ref.entity.name);
|
|
613
|
+
if (!table) throw new AlephaError(`Referenced table ${ref.entity.name} not found for ${tableName}.${key}`);
|
|
614
|
+
const target = table[ref.name];
|
|
615
|
+
if (!target) throw new AlephaError(`Referenced column ${ref.name} not found in table ${ref.entity.name} for ${tableName}.${key}`);
|
|
616
|
+
return target;
|
|
617
|
+
}, config.actions);
|
|
618
|
+
}
|
|
619
|
+
if (schema$1.required?.includes(key)) col = col.notNull();
|
|
620
|
+
return {
|
|
621
|
+
...columns,
|
|
622
|
+
[key]: col
|
|
623
|
+
};
|
|
624
|
+
}, {});
|
|
625
|
+
};
|
|
626
|
+
mapFieldToColumn = (tableName, fieldName, value, nsp, enums) => {
|
|
627
|
+
const key = this.toColumnName(fieldName);
|
|
628
|
+
if ("anyOf" in value && Array.isArray(value.anyOf) && value.anyOf.length === 2 && value.anyOf.some((it) => t.schema.isNull(it))) value = value.anyOf.find((it) => !t.schema.isNull(it));
|
|
629
|
+
if (t.schema.isInteger(value)) {
|
|
630
|
+
if (PG_SERIAL in value) return pg$2.serial(key);
|
|
631
|
+
if (PG_IDENTITY in value) {
|
|
632
|
+
const options = value[PG_IDENTITY];
|
|
633
|
+
if (options.mode === "byDefault") return pg$2.integer().generatedByDefaultAsIdentity(options);
|
|
634
|
+
return pg$2.integer().generatedAlwaysAsIdentity(options);
|
|
678
635
|
}
|
|
636
|
+
return pg$2.integer(key);
|
|
679
637
|
}
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
if (
|
|
688
|
-
|
|
689
|
-
|
|
638
|
+
if (t.schema.isBigInt(value)) {
|
|
639
|
+
if (PG_IDENTITY in value) {
|
|
640
|
+
const options = value[PG_IDENTITY];
|
|
641
|
+
if (options.mode === "byDefault") return pg$2.bigint({ mode: "bigint" }).generatedByDefaultAsIdentity(options);
|
|
642
|
+
return pg$2.bigint({ mode: "bigint" }).generatedAlwaysAsIdentity(options);
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
if (t.schema.isNumber(value)) {
|
|
646
|
+
if (PG_IDENTITY in value) {
|
|
647
|
+
const options = value[PG_IDENTITY];
|
|
648
|
+
if (options.mode === "byDefault") return pg$2.bigint({ mode: "number" }).generatedByDefaultAsIdentity(options);
|
|
649
|
+
return pg$2.bigint({ mode: "number" }).generatedAlwaysAsIdentity(options);
|
|
650
|
+
}
|
|
651
|
+
if (value.format === "int64") return pg$2.bigint(key, { mode: "number" });
|
|
652
|
+
return pg$2.numeric(key);
|
|
653
|
+
}
|
|
654
|
+
if (t.schema.isString(value)) return this.mapStringToColumn(key, value);
|
|
655
|
+
if (t.schema.isBoolean(value)) return pg$2.boolean(key);
|
|
656
|
+
if (t.schema.isObject(value)) return schema(key, value);
|
|
657
|
+
if (t.schema.isRecord(value)) return schema(key, value);
|
|
658
|
+
const isTypeEnum = (value$1) => t.schema.isUnsafe(value$1) && "type" in value$1 && value$1.type === "string" && "enum" in value$1 && Array.isArray(value$1.enum);
|
|
659
|
+
if (t.schema.isArray(value)) {
|
|
660
|
+
if (t.schema.isObject(value.items)) return schema(key, value);
|
|
661
|
+
if (t.schema.isRecord(value.items)) return schema(key, value);
|
|
662
|
+
if (t.schema.isString(value.items)) return pg$2.text(key).array();
|
|
663
|
+
if (t.schema.isInteger(value.items)) return pg$2.integer(key).array();
|
|
664
|
+
if (t.schema.isNumber(value.items)) return pg$2.numeric(key).array();
|
|
665
|
+
if (t.schema.isBoolean(value.items)) return pg$2.boolean(key).array();
|
|
666
|
+
if (isTypeEnum(value.items)) return pg$2.text(key).array();
|
|
667
|
+
}
|
|
668
|
+
if (isTypeEnum(value)) {
|
|
669
|
+
if (!value.enum.every((it) => typeof it === "string")) throw new AlephaError(`Enum for ${fieldName} must be an array of strings, got ${JSON.stringify(value.enum)}`);
|
|
670
|
+
if (PG_ENUM in value && value[PG_ENUM]) {
|
|
671
|
+
const enumName = value[PG_ENUM].name ?? `${tableName}_${key}_enum`;
|
|
672
|
+
if (enums.has(enumName)) {
|
|
673
|
+
const values = enums.get(enumName).enumValues.join(",");
|
|
674
|
+
const newValues = value.enum.join(",");
|
|
675
|
+
if (values !== newValues) throw new AlephaError(`Enum name conflict for ${enumName}: [${values}] vs [${newValues}]`);
|
|
676
|
+
}
|
|
677
|
+
enums.set(enumName, nsp.enum(enumName, value.enum));
|
|
678
|
+
return enums.get(enumName)(key);
|
|
679
|
+
}
|
|
680
|
+
return this.mapStringToColumn(key, value);
|
|
681
|
+
}
|
|
682
|
+
throw new AlephaError(`Unsupported schema type for ${fieldName} as ${JSON.stringify(value)}`);
|
|
683
|
+
};
|
|
690
684
|
/**
|
|
691
|
-
*
|
|
685
|
+
* Map a string to a PG column.
|
|
686
|
+
*
|
|
687
|
+
* @param key The key of the field.
|
|
688
|
+
* @param value The value of the field.
|
|
692
689
|
*/
|
|
693
|
-
|
|
694
|
-
|
|
690
|
+
mapStringToColumn = (key, value) => {
|
|
691
|
+
if ("format" in value) {
|
|
692
|
+
if (value.format === "uuid") {
|
|
693
|
+
if (PG_PRIMARY_KEY in value) return pg$2.uuid(key).defaultRandom();
|
|
694
|
+
return pg$2.uuid(key);
|
|
695
|
+
}
|
|
696
|
+
if (value.format === "byte") return byte(key);
|
|
697
|
+
if (value.format === "date-time") {
|
|
698
|
+
if (PG_CREATED_AT in value) return pg$2.timestamp(key, {
|
|
699
|
+
mode: "string",
|
|
700
|
+
withTimezone: true
|
|
701
|
+
}).defaultNow();
|
|
702
|
+
if (PG_UPDATED_AT in value) return pg$2.timestamp(key, {
|
|
703
|
+
mode: "string",
|
|
704
|
+
withTimezone: true
|
|
705
|
+
}).defaultNow();
|
|
706
|
+
return pg$2.timestamp(key, {
|
|
707
|
+
mode: "string",
|
|
708
|
+
withTimezone: true
|
|
709
|
+
});
|
|
710
|
+
}
|
|
711
|
+
if (value.format === "date") return pg$2.date(key, { mode: "string" });
|
|
712
|
+
}
|
|
713
|
+
return pg$2.text(key);
|
|
714
|
+
};
|
|
715
|
+
};
|
|
716
|
+
|
|
717
|
+
//#endregion
|
|
718
|
+
//#region ../../src/orm/providers/drivers/BunPostgresProvider.ts
|
|
719
|
+
const envSchema$4 = t.object({
|
|
720
|
+
DATABASE_URL: t.optional(t.text()),
|
|
721
|
+
POSTGRES_SCHEMA: t.optional(t.text())
|
|
722
|
+
});
|
|
723
|
+
/**
|
|
724
|
+
* Bun PostgreSQL provider using Drizzle ORM with Bun's native SQL client.
|
|
725
|
+
*
|
|
726
|
+
* This provider uses Bun's built-in SQL class for PostgreSQL connections,
|
|
727
|
+
* which provides excellent performance on the Bun runtime.
|
|
728
|
+
*
|
|
729
|
+
* @example
|
|
730
|
+
* ```ts
|
|
731
|
+
* // Set DATABASE_URL environment variable
|
|
732
|
+
* // DATABASE_URL=postgres://user:password@localhost:5432/database
|
|
733
|
+
*
|
|
734
|
+
* // Or configure programmatically
|
|
735
|
+
* alepha.with({
|
|
736
|
+
* provide: DatabaseProvider,
|
|
737
|
+
* use: BunPostgresProvider,
|
|
738
|
+
* });
|
|
739
|
+
* ```
|
|
740
|
+
*/
|
|
741
|
+
var BunPostgresProvider = class extends DatabaseProvider {
|
|
742
|
+
log = $logger();
|
|
743
|
+
env = $env(envSchema$4);
|
|
744
|
+
kit = $inject(DrizzleKitProvider);
|
|
745
|
+
builder = $inject(PostgresModelBuilder);
|
|
746
|
+
client;
|
|
747
|
+
bunDb;
|
|
748
|
+
dialect = "postgresql";
|
|
749
|
+
get name() {
|
|
750
|
+
return "postgres";
|
|
695
751
|
}
|
|
696
752
|
/**
|
|
697
|
-
*
|
|
753
|
+
* In testing mode, the schema name will be generated and deleted after the test.
|
|
698
754
|
*/
|
|
699
|
-
|
|
700
|
-
|
|
755
|
+
schemaForTesting = this.alepha.isTest() ? this.env.POSTGRES_SCHEMA?.startsWith("test_") ? this.env.POSTGRES_SCHEMA : this.generateTestSchemaName() : void 0;
|
|
756
|
+
get url() {
|
|
757
|
+
if (!this.env.DATABASE_URL) throw new AlephaError("DATABASE_URL is not defined in the environment");
|
|
758
|
+
return this.env.DATABASE_URL;
|
|
701
759
|
}
|
|
702
760
|
/**
|
|
703
|
-
*
|
|
761
|
+
* Execute a SQL statement.
|
|
704
762
|
*/
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
763
|
+
execute(statement) {
|
|
764
|
+
try {
|
|
765
|
+
return this.db.execute(statement);
|
|
766
|
+
} catch (error) {
|
|
767
|
+
throw new DbError("Error executing statement", error);
|
|
768
|
+
}
|
|
711
769
|
}
|
|
712
770
|
/**
|
|
713
|
-
*
|
|
771
|
+
* Get Postgres schema used by this provider.
|
|
714
772
|
*/
|
|
715
|
-
|
|
716
|
-
|
|
773
|
+
get schema() {
|
|
774
|
+
if (this.schemaForTesting) return this.schemaForTesting;
|
|
775
|
+
if (this.env.POSTGRES_SCHEMA) return this.env.POSTGRES_SCHEMA;
|
|
776
|
+
return "public";
|
|
717
777
|
}
|
|
718
778
|
/**
|
|
719
|
-
*
|
|
779
|
+
* Get the Drizzle Postgres database instance.
|
|
720
780
|
*/
|
|
721
|
-
|
|
722
|
-
|
|
781
|
+
get db() {
|
|
782
|
+
if (!this.bunDb) throw new AlephaError("Database not initialized");
|
|
783
|
+
return this.bunDb;
|
|
723
784
|
}
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
rawDelete(opts = {}) {
|
|
728
|
-
return (opts.tx ?? this.db).delete(this.table);
|
|
785
|
+
async executeMigrations(migrationsFolder) {
|
|
786
|
+
const { migrate: migrate$3 } = await import("drizzle-orm/bun-sql/migrator");
|
|
787
|
+
await migrate$3(this.bunDb, { migrationsFolder });
|
|
729
788
|
}
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
});
|
|
740
|
-
const columns = query.columns ?? query.distinct;
|
|
741
|
-
const builder = query.distinct ? this.rawSelectDistinct(opts, query.distinct) : this.rawSelect(opts);
|
|
742
|
-
const joins = [];
|
|
743
|
-
if (query.with) this.relationManager.buildJoins(this.provider, builder, joins, query.with, this.table);
|
|
744
|
-
const where = this.withDeletedAt(query.where ?? {}, opts);
|
|
745
|
-
builder.where(() => this.toSQL(where, joins));
|
|
746
|
-
if (query.offset) {
|
|
747
|
-
builder.offset(query.offset);
|
|
748
|
-
if (this.provider.dialect === "sqlite" && !query.limit) query.limit = 1e3;
|
|
749
|
-
}
|
|
750
|
-
if (query.limit) builder.limit(query.limit);
|
|
751
|
-
if (query.orderBy) {
|
|
752
|
-
const orderByClauses = this.queryManager.normalizeOrderBy(query.orderBy);
|
|
753
|
-
builder.orderBy(...orderByClauses.map((clause) => clause.direction === "desc" ? desc(this.col(clause.column)) : asc(this.col(clause.column))));
|
|
789
|
+
onStart = $hook({
|
|
790
|
+
on: "start",
|
|
791
|
+
handler: async () => {
|
|
792
|
+
await this.connect();
|
|
793
|
+
if (!this.alepha.isServerless()) try {
|
|
794
|
+
await this.migrateLock.run();
|
|
795
|
+
} catch (error) {
|
|
796
|
+
throw new DbMigrationError(error);
|
|
797
|
+
}
|
|
754
798
|
}
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
799
|
+
});
|
|
800
|
+
onStop = $hook({
|
|
801
|
+
on: "stop",
|
|
802
|
+
handler: async () => {
|
|
803
|
+
if (this.alepha.isTest() && this.schemaForTesting && this.schemaForTesting.startsWith("test_")) {
|
|
804
|
+
if (!/^test_[a-z0-9_]+$/i.test(this.schemaForTesting)) throw new AlephaError(`Invalid test schema name: ${this.schemaForTesting}. Must match pattern: test_[a-z0-9_]+`);
|
|
805
|
+
this.log.warn(`Deleting test schema '${this.schemaForTesting}' ...`);
|
|
806
|
+
await this.execute(sql$1`DROP SCHEMA IF EXISTS ${sql$1.raw(this.schemaForTesting)} CASCADE`);
|
|
807
|
+
this.log.info(`Test schema '${this.schemaForTesting}' deleted`);
|
|
808
|
+
}
|
|
809
|
+
await this.close();
|
|
759
810
|
}
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
await this.
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
});
|
|
783
|
-
return rows;
|
|
784
|
-
} catch (error) {
|
|
785
|
-
throw new DbError("Query select has failed", error);
|
|
811
|
+
});
|
|
812
|
+
async connect() {
|
|
813
|
+
this.log.debug("Connect ..");
|
|
814
|
+
if (typeof Bun === "undefined") throw new AlephaError("BunPostgresProvider requires the Bun runtime. Use NodePostgresProvider for Node.js.");
|
|
815
|
+
const { drizzle: drizzle$3 } = await import("drizzle-orm/bun-sql");
|
|
816
|
+
this.client = new Bun.SQL(this.url);
|
|
817
|
+
await this.client.unsafe("SELECT 1");
|
|
818
|
+
this.bunDb = drizzle$3({
|
|
819
|
+
client: this.client,
|
|
820
|
+
logger: { logQuery: (query, params) => {
|
|
821
|
+
this.log.trace(query, { params });
|
|
822
|
+
} }
|
|
823
|
+
});
|
|
824
|
+
this.log.info("Connection OK");
|
|
825
|
+
}
|
|
826
|
+
async close() {
|
|
827
|
+
if (this.client) {
|
|
828
|
+
this.log.debug("Close...");
|
|
829
|
+
await this.client.close();
|
|
830
|
+
this.client = void 0;
|
|
831
|
+
this.bunDb = void 0;
|
|
832
|
+
this.log.info("Connection closed");
|
|
786
833
|
}
|
|
787
834
|
}
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
835
|
+
migrateLock = $lock({ handler: async () => {
|
|
836
|
+
await this.migrate();
|
|
837
|
+
} });
|
|
838
|
+
};
|
|
839
|
+
|
|
840
|
+
//#endregion
|
|
841
|
+
//#region ../../src/orm/services/SqliteModelBuilder.ts
|
|
842
|
+
var SqliteModelBuilder = class extends ModelBuilder {
|
|
843
|
+
buildTable(entity, options) {
|
|
844
|
+
const tableName = entity.name;
|
|
845
|
+
if (options.tables.has(tableName)) return;
|
|
846
|
+
const table = sqliteTable(tableName, this.schemaToSqliteColumns(tableName, entity.schema, options.enums, options.tables), this.getTableConfig(entity, options.tables));
|
|
847
|
+
options.tables.set(tableName, table);
|
|
848
|
+
}
|
|
849
|
+
buildSequence(sequence, options) {
|
|
850
|
+
throw new AlephaError("SQLite does not support sequences");
|
|
798
851
|
}
|
|
799
852
|
/**
|
|
800
|
-
*
|
|
801
|
-
*
|
|
802
|
-
* It uses the same parameters as `find()`, but adds pagination metadata to the response.
|
|
803
|
-
*
|
|
804
|
-
* > Pagination CAN also do a count query to get the total number of elements.
|
|
853
|
+
* Get SQLite-specific config builder for the table.
|
|
805
854
|
*/
|
|
806
|
-
|
|
807
|
-
const
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
};
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
855
|
+
getTableConfig(entity, tables) {
|
|
856
|
+
const sqliteBuilders = {
|
|
857
|
+
index: index$1,
|
|
858
|
+
uniqueIndex: uniqueIndex$1,
|
|
859
|
+
unique: unique$1,
|
|
860
|
+
check: check$1,
|
|
861
|
+
foreignKey: foreignKey$1
|
|
862
|
+
};
|
|
863
|
+
const tableResolver = (entityName) => {
|
|
864
|
+
return tables.get(entityName);
|
|
865
|
+
};
|
|
866
|
+
return this.buildTableConfig(entity, sqliteBuilders, tableResolver, (config, self) => {
|
|
867
|
+
const customConfigs = config(self);
|
|
868
|
+
return Array.isArray(customConfigs) ? customConfigs : [];
|
|
869
|
+
});
|
|
870
|
+
}
|
|
871
|
+
schemaToSqliteColumns = (tableName, schema$1, enums, tables) => {
|
|
872
|
+
return Object.entries(schema$1.properties).reduce((columns, [key, value]) => {
|
|
873
|
+
let col = this.mapFieldToSqliteColumn(tableName, key, value, enums);
|
|
874
|
+
if ("default" in value && value.default != null) col = col.default(value.default);
|
|
875
|
+
if (PG_PRIMARY_KEY in value) col = col.primaryKey();
|
|
876
|
+
if (PG_REF in value) {
|
|
877
|
+
const config = value[PG_REF];
|
|
878
|
+
col = col.references(() => {
|
|
879
|
+
const ref = config.ref();
|
|
880
|
+
const table = tables.get(ref.entity.name);
|
|
881
|
+
if (!table) throw new AlephaError(`Referenced table ${ref.entity.name} not found for ${tableName}.${key}`);
|
|
882
|
+
const target = table[ref.name];
|
|
883
|
+
if (!target) throw new AlephaError(`Referenced column ${ref.name} not found in table ${ref.entity.name} for ${tableName}.${key}`);
|
|
884
|
+
return target;
|
|
885
|
+
}, config.actions);
|
|
886
|
+
}
|
|
887
|
+
if (schema$1.required?.includes(key)) col = col.notNull();
|
|
888
|
+
return {
|
|
889
|
+
...columns,
|
|
890
|
+
[key]: col
|
|
891
|
+
};
|
|
892
|
+
}, {});
|
|
893
|
+
};
|
|
894
|
+
mapFieldToSqliteColumn = (tableName, fieldName, value, enums) => {
|
|
895
|
+
const key = this.toColumnName(fieldName);
|
|
896
|
+
if ("anyOf" in value && Array.isArray(value.anyOf) && value.anyOf.length === 2 && value.anyOf.some((it) => t.schema.isNull(it))) value = value.anyOf.find((it) => !t.schema.isNull(it));
|
|
897
|
+
if (t.schema.isInteger(value)) {
|
|
898
|
+
if (PG_SERIAL in value || PG_IDENTITY in value) return pg$1.integer(key, { mode: "number" }).primaryKey({ autoIncrement: true });
|
|
899
|
+
return pg$1.integer(key);
|
|
833
900
|
}
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
901
|
+
if (t.schema.isBigInt(value)) {
|
|
902
|
+
if (PG_PRIMARY_KEY in value || PG_IDENTITY in value) return pg$1.integer(key, { mode: "number" }).primaryKey({ autoIncrement: true });
|
|
903
|
+
return pg$1.integer(key, { mode: "number" });
|
|
904
|
+
}
|
|
905
|
+
if (t.schema.isNumber(value)) {
|
|
906
|
+
if (PG_IDENTITY in value) return pg$1.integer(key, { mode: "number" }).primaryKey({ autoIncrement: true });
|
|
907
|
+
return pg$1.numeric(key);
|
|
908
|
+
}
|
|
909
|
+
if (t.schema.isString(value)) return this.mapStringToSqliteColumn(key, value);
|
|
910
|
+
if (t.schema.isBoolean(value)) return this.sqliteBool(key, value);
|
|
911
|
+
if (t.schema.isObject(value)) return this.sqliteJson(key, value);
|
|
912
|
+
if (t.schema.isRecord(value)) return this.sqliteJson(key, value);
|
|
913
|
+
if (t.schema.isAny(value)) return this.sqliteJson(key, value);
|
|
914
|
+
if (t.schema.isArray(value)) {
|
|
915
|
+
if (t.schema.isObject(value.items)) return this.sqliteJson(key, value);
|
|
916
|
+
if (t.schema.isRecord(value.items)) return this.sqliteJson(key, value);
|
|
917
|
+
if (t.schema.isAny(value.items)) return this.sqliteJson(key, value);
|
|
918
|
+
if (t.schema.isString(value.items)) return this.sqliteJson(key, value);
|
|
919
|
+
if (t.schema.isInteger(value.items)) return this.sqliteJson(key, value);
|
|
920
|
+
if (t.schema.isNumber(value.items)) return this.sqliteJson(key, value);
|
|
921
|
+
if (t.schema.isBoolean(value.items)) return this.sqliteJson(key, value);
|
|
922
|
+
}
|
|
923
|
+
if (t.schema.isUnsafe(value) && "type" in value && value.type === "string") return this.mapStringToSqliteColumn(key, value);
|
|
924
|
+
throw new Error(`Unsupported schema for field '${tableName}.${fieldName}' (schema: ${JSON.stringify(value)})`);
|
|
925
|
+
};
|
|
926
|
+
mapStringToSqliteColumn = (key, value) => {
|
|
927
|
+
if (value.format === "uuid") {
|
|
928
|
+
if (PG_PRIMARY_KEY in value) return pg$1.text(key).primaryKey().$defaultFn(() => randomUUID());
|
|
929
|
+
return pg$1.text(key);
|
|
930
|
+
}
|
|
931
|
+
if (value.format === "byte") return this.sqliteJson(key, value);
|
|
932
|
+
if (value.format === "date-time") {
|
|
933
|
+
if (PG_CREATED_AT in value) return this.sqliteDateTime(key, {}).default(sql$1`(unixepoch('subsec') * 1000)`);
|
|
934
|
+
if (PG_UPDATED_AT in value) return this.sqliteDateTime(key, {}).default(sql$1`(unixepoch('subsec') * 1000)`);
|
|
935
|
+
return this.sqliteDateTime(key, {});
|
|
936
|
+
}
|
|
937
|
+
if (value.format === "date") return this.sqliteDate(key, {});
|
|
938
|
+
return pg$1.text(key);
|
|
939
|
+
};
|
|
940
|
+
sqliteJson = (name, document) => pg$1.customType({
|
|
941
|
+
dataType: () => "text",
|
|
942
|
+
toDriver: (value) => JSON.stringify(value),
|
|
943
|
+
fromDriver: (value) => {
|
|
944
|
+
return value && typeof value === "string" ? JSON.parse(value) : value;
|
|
945
|
+
}
|
|
946
|
+
})(name, { document }).$type();
|
|
947
|
+
sqliteDateTime = pg$1.customType({
|
|
948
|
+
dataType: () => "integer",
|
|
949
|
+
toDriver: (value) => new Date(value).getTime(),
|
|
950
|
+
fromDriver: (value) => {
|
|
951
|
+
return new Date(value).toISOString();
|
|
952
|
+
}
|
|
953
|
+
});
|
|
954
|
+
sqliteBool = pg$1.customType({
|
|
955
|
+
dataType: () => "integer",
|
|
956
|
+
toDriver: (value) => value ? 1 : 0,
|
|
957
|
+
fromDriver: (value) => value === 1
|
|
958
|
+
});
|
|
959
|
+
sqliteDate = pg$1.customType({
|
|
960
|
+
dataType: () => "integer",
|
|
961
|
+
toDriver: (value) => new Date(value).getTime(),
|
|
962
|
+
fromDriver: (value) => {
|
|
963
|
+
return new Date(value).toISOString().split("T")[0];
|
|
964
|
+
}
|
|
965
|
+
});
|
|
966
|
+
};
|
|
967
|
+
|
|
968
|
+
//#endregion
|
|
969
|
+
//#region ../../src/orm/providers/drivers/BunSqliteProvider.ts
|
|
970
|
+
const envSchema$3 = t.object({ DATABASE_URL: t.optional(t.text()) });
|
|
971
|
+
/**
|
|
972
|
+
* Configuration options for the Bun SQLite database provider.
|
|
973
|
+
*/
|
|
974
|
+
const bunSqliteOptions = $atom({
|
|
975
|
+
name: "alepha.postgres.bun-sqlite.options",
|
|
976
|
+
schema: t.object({ path: t.optional(t.string({ description: "Filepath or :memory:. If empty, provider will use DATABASE_URL from env." })) }),
|
|
977
|
+
default: {}
|
|
978
|
+
});
|
|
979
|
+
/**
|
|
980
|
+
* Bun SQLite provider using Drizzle ORM with Bun's native SQLite client.
|
|
981
|
+
*
|
|
982
|
+
* This provider uses Bun's built-in `bun:sqlite` for SQLite connections,
|
|
983
|
+
* which provides excellent performance on the Bun runtime.
|
|
984
|
+
*
|
|
985
|
+
* @example
|
|
986
|
+
* ```ts
|
|
987
|
+
* // Set DATABASE_URL environment variable
|
|
988
|
+
* // DATABASE_URL=sqlite://./my-database.db
|
|
989
|
+
*
|
|
990
|
+
* // Or configure programmatically
|
|
991
|
+
* alepha.with({
|
|
992
|
+
* provide: DatabaseProvider,
|
|
993
|
+
* use: BunSqliteProvider,
|
|
994
|
+
* });
|
|
995
|
+
*
|
|
996
|
+
* // Or use options atom
|
|
997
|
+
* alepha.store.mut(bunSqliteOptions, (old) => ({
|
|
998
|
+
* ...old,
|
|
999
|
+
* path: ":memory:",
|
|
1000
|
+
* }));
|
|
1001
|
+
* ```
|
|
1002
|
+
*/
|
|
1003
|
+
var BunSqliteProvider = class extends DatabaseProvider {
|
|
1004
|
+
kit = $inject(DrizzleKitProvider);
|
|
1005
|
+
log = $logger();
|
|
1006
|
+
env = $env(envSchema$3);
|
|
1007
|
+
builder = $inject(SqliteModelBuilder);
|
|
1008
|
+
options = $use(bunSqliteOptions);
|
|
1009
|
+
sqlite;
|
|
1010
|
+
bunDb;
|
|
1011
|
+
get name() {
|
|
1012
|
+
return "sqlite";
|
|
841
1013
|
}
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
1014
|
+
dialect = "sqlite";
|
|
1015
|
+
get url() {
|
|
1016
|
+
const path = this.options.path ?? this.env.DATABASE_URL;
|
|
1017
|
+
if (path) {
|
|
1018
|
+
if (path.startsWith("postgres://")) throw new AlephaError("Postgres URL is not supported for SQLite provider.");
|
|
1019
|
+
return path;
|
|
1020
|
+
}
|
|
1021
|
+
if (this.alepha.isTest() || this.alepha.isServerless()) return ":memory:";
|
|
1022
|
+
else return "node_modules/.alepha/bun-sqlite.db";
|
|
850
1023
|
}
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
createQuery() {
|
|
855
|
-
return {};
|
|
1024
|
+
get db() {
|
|
1025
|
+
if (!this.bunDb) throw new AlephaError("Database not initialized");
|
|
1026
|
+
return this.bunDb;
|
|
856
1027
|
}
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
*/
|
|
860
|
-
createQueryWhere() {
|
|
861
|
-
return {};
|
|
1028
|
+
async execute(query) {
|
|
1029
|
+
return this.bunDb.all(query);
|
|
862
1030
|
}
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
entity
|
|
1031
|
+
onStart = $hook({
|
|
1032
|
+
on: "start",
|
|
1033
|
+
handler: async () => {
|
|
1034
|
+
if (typeof Bun === "undefined") throw new AlephaError("BunSqliteProvider requires the Bun runtime. Use NodeSqliteProvider for Node.js.");
|
|
1035
|
+
const { Database } = await import("bun:sqlite");
|
|
1036
|
+
const { drizzle: drizzle$3 } = await import("drizzle-orm/bun-sqlite");
|
|
1037
|
+
const filepath = this.url.replace("sqlite://", "").replace("sqlite:", "");
|
|
1038
|
+
if (filepath !== ":memory:" && filepath !== "") {
|
|
1039
|
+
const dirname = filepath.split("/").slice(0, -1).join("/");
|
|
1040
|
+
if (dirname) await mkdir(dirname, { recursive: true }).catch(() => null);
|
|
1041
|
+
}
|
|
1042
|
+
this.sqlite = new Database(filepath);
|
|
1043
|
+
this.bunDb = drizzle$3({
|
|
1044
|
+
client: this.sqlite,
|
|
1045
|
+
logger: { logQuery: (query, params) => {
|
|
1046
|
+
this.log.trace(query, { params });
|
|
1047
|
+
} }
|
|
881
1048
|
});
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
throw this.handleError(error, "Insert query has failed");
|
|
1049
|
+
await this.migrate();
|
|
1050
|
+
this.log.info(`Using Bun SQLite database at ${filepath}`);
|
|
885
1051
|
}
|
|
886
|
-
}
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
async createMany(values, opts = {}) {
|
|
897
|
-
if (values.length === 0) return [];
|
|
898
|
-
await this.alepha.events.emit("repository:create:before", {
|
|
899
|
-
tableName: this.tableName,
|
|
900
|
-
data: values
|
|
901
|
-
});
|
|
902
|
-
const batchSize = opts.batchSize ?? 1e3;
|
|
903
|
-
const allEntities = [];
|
|
904
|
-
try {
|
|
905
|
-
for (let i = 0; i < values.length; i += batchSize) {
|
|
906
|
-
const batch = values.slice(i, i + batchSize);
|
|
907
|
-
const entities = await this.rawInsert(opts).values(batch.map((data) => this.cast(data, true))).returning(this.table).then((rows) => rows.map((it) => this.clean(it, this.entity.schema)));
|
|
908
|
-
allEntities.push(...entities);
|
|
1052
|
+
});
|
|
1053
|
+
onStop = $hook({
|
|
1054
|
+
on: "stop",
|
|
1055
|
+
handler: async () => {
|
|
1056
|
+
if (this.sqlite) {
|
|
1057
|
+
this.log.debug("Closing Bun SQLite connection...");
|
|
1058
|
+
this.sqlite.close();
|
|
1059
|
+
this.sqlite = void 0;
|
|
1060
|
+
this.bunDb = void 0;
|
|
1061
|
+
this.log.info("Bun SQLite connection closed");
|
|
909
1062
|
}
|
|
910
|
-
await this.alepha.events.emit("repository:create:after", {
|
|
911
|
-
tableName: this.tableName,
|
|
912
|
-
data: values,
|
|
913
|
-
entity: allEntities
|
|
914
|
-
});
|
|
915
|
-
return allEntities;
|
|
916
|
-
} catch (error) {
|
|
917
|
-
throw this.handleError(error, "Insert query has failed");
|
|
918
1063
|
}
|
|
1064
|
+
});
|
|
1065
|
+
async executeMigrations(migrationsFolder) {
|
|
1066
|
+
const { migrate: migrate$3 } = await import("drizzle-orm/bun-sqlite/migrator");
|
|
1067
|
+
await migrate$3(this.bunDb, { migrationsFolder });
|
|
1068
|
+
}
|
|
1069
|
+
};
|
|
1070
|
+
|
|
1071
|
+
//#endregion
|
|
1072
|
+
//#region ../../src/orm/providers/drivers/CloudflareD1Provider.ts
|
|
1073
|
+
/**
|
|
1074
|
+
* Cloudflare D1 SQLite provider using Drizzle ORM.
|
|
1075
|
+
*
|
|
1076
|
+
* This provider requires a D1 binding to be set via `cloudflareD1Options` before starting.
|
|
1077
|
+
* The binding is typically obtained from the Cloudflare Workers environment.
|
|
1078
|
+
*
|
|
1079
|
+
* @example
|
|
1080
|
+
* ```ts
|
|
1081
|
+
* // In your Cloudflare Worker
|
|
1082
|
+
* alepha.set(cloudflareD1Options, { binding: env.DB });
|
|
1083
|
+
* ```
|
|
1084
|
+
*/
|
|
1085
|
+
var CloudflareD1Provider = class extends DatabaseProvider {
|
|
1086
|
+
kit = $inject(DrizzleKitProvider);
|
|
1087
|
+
log = $logger();
|
|
1088
|
+
builder = $inject(SqliteModelBuilder);
|
|
1089
|
+
env = $env(t.object({ DATABASE_URL: t.string({ description: "Expect to be 'cloudflare-d1://name:id'" }) }));
|
|
1090
|
+
d1;
|
|
1091
|
+
drizzleDb;
|
|
1092
|
+
get name() {
|
|
1093
|
+
return "sqlite";
|
|
1094
|
+
}
|
|
1095
|
+
get driver() {
|
|
1096
|
+
return "d1";
|
|
1097
|
+
}
|
|
1098
|
+
dialect = "sqlite";
|
|
1099
|
+
get url() {
|
|
1100
|
+
return this.env.DATABASE_URL;
|
|
1101
|
+
}
|
|
1102
|
+
get db() {
|
|
1103
|
+
if (!this.drizzleDb) throw new AlephaError("D1 database not initialized");
|
|
1104
|
+
return this.drizzleDb;
|
|
1105
|
+
}
|
|
1106
|
+
async execute(query) {
|
|
1107
|
+
const { rows } = await this.db.run(query);
|
|
1108
|
+
return rows;
|
|
1109
|
+
}
|
|
1110
|
+
onStart = $hook({
|
|
1111
|
+
on: "start",
|
|
1112
|
+
handler: async () => {
|
|
1113
|
+
const [bindingName] = this.env.DATABASE_URL.replace("cloudflare-d1://", "").split(":");
|
|
1114
|
+
const cloudflareEnv = this.alepha.store.get("cloudflare.env");
|
|
1115
|
+
if (!cloudflareEnv) throw new AlephaError("Cloudflare Workers environment not found in Alepha store under 'cloudflare.env'.");
|
|
1116
|
+
const binding = cloudflareEnv[bindingName];
|
|
1117
|
+
if (!binding) throw new AlephaError(`D1 binding '${bindingName}' not found in Cloudflare Workers environment.`);
|
|
1118
|
+
this.d1 = binding;
|
|
1119
|
+
const { drizzle: drizzle$3 } = await import("drizzle-orm/d1");
|
|
1120
|
+
this.drizzleDb = drizzle$3(this.d1);
|
|
1121
|
+
await this.migrate();
|
|
1122
|
+
this.log.info("Using Cloudflare D1 database");
|
|
1123
|
+
}
|
|
1124
|
+
});
|
|
1125
|
+
async executeMigrations(migrationsFolder) {
|
|
1126
|
+
const { migrate: migrate$3 } = await import("drizzle-orm/d1/migrator");
|
|
1127
|
+
await migrate$3(this.db, { migrationsFolder });
|
|
919
1128
|
}
|
|
920
1129
|
/**
|
|
921
|
-
*
|
|
1130
|
+
* Override development migration to skip sync (not supported on D1).
|
|
1131
|
+
* D1 requires proper migrations to be applied.
|
|
922
1132
|
*/
|
|
923
|
-
async
|
|
924
|
-
await this.
|
|
925
|
-
tableName: this.tableName,
|
|
926
|
-
where,
|
|
927
|
-
data
|
|
928
|
-
});
|
|
929
|
-
let row = data;
|
|
930
|
-
const updatedAtField = getAttrFields(this.entity.schema, PG_UPDATED_AT)?.[0];
|
|
931
|
-
if (updatedAtField) row[updatedAtField.key] = this.dateTimeProvider.of(opts.now).toISOString();
|
|
932
|
-
where = this.withDeletedAt(where, opts);
|
|
933
|
-
row = this.cast(row, false);
|
|
934
|
-
delete row[this.id.key];
|
|
935
|
-
const response = await this.rawUpdate(opts).set(row).where(this.toSQL(where)).returning(this.table).catch((error) => {
|
|
936
|
-
throw this.handleError(error, "Update query has failed");
|
|
937
|
-
});
|
|
938
|
-
if (!response[0]) throw new DbEntityNotFoundError(this.tableName);
|
|
939
|
-
try {
|
|
940
|
-
const entity = this.clean(response[0], this.entity.schema);
|
|
941
|
-
await this.alepha.events.emit("repository:update:after", {
|
|
942
|
-
tableName: this.tableName,
|
|
943
|
-
where,
|
|
944
|
-
data,
|
|
945
|
-
entities: [entity]
|
|
946
|
-
});
|
|
947
|
-
return entity;
|
|
948
|
-
} catch (error) {
|
|
949
|
-
throw this.handleError(error, "Update query has failed");
|
|
950
|
-
}
|
|
1133
|
+
async runDevelopmentMigration(migrationsFolder) {
|
|
1134
|
+
await this.executeMigrations(migrationsFolder);
|
|
951
1135
|
}
|
|
952
1136
|
/**
|
|
953
|
-
*
|
|
954
|
-
*
|
|
955
|
-
* @example
|
|
956
|
-
* ```ts
|
|
957
|
-
* const entity = await repository.findById(1);
|
|
958
|
-
* entity.name = "New Name"; // update a field
|
|
959
|
-
* delete entity.description; // delete a field
|
|
960
|
-
* await repository.save(entity);
|
|
961
|
-
* ```
|
|
962
|
-
*
|
|
963
|
-
* Difference with `updateById/updateOne`:
|
|
964
|
-
*
|
|
965
|
-
* - requires the entity to be fetched first (whole object is expected)
|
|
966
|
-
* - check pg.version() if present -> optimistic locking
|
|
967
|
-
* - validate entity against schema
|
|
968
|
-
* - undefined values will be set to null, not ignored!
|
|
969
|
-
*
|
|
970
|
-
* @see {@link DbVersionMismatchError}
|
|
1137
|
+
* Override test migration to run migrations instead of sync.
|
|
1138
|
+
* D1 doesn't support schema synchronization.
|
|
971
1139
|
*/
|
|
972
|
-
async
|
|
973
|
-
const
|
|
974
|
-
const id = row[this.id.key];
|
|
975
|
-
if (id == null) throw new AlephaError("Cannot save entity without ID - missing primary key in value");
|
|
976
|
-
for (const key of Object.keys(this.entity.schema.properties)) if (row[key] === void 0) row[key] = null;
|
|
977
|
-
let where = this.createQueryWhere();
|
|
978
|
-
where.id = { eq: id };
|
|
979
|
-
const versionField = getAttrFields(this.entity.schema, PG_VERSION)?.[0];
|
|
980
|
-
if (versionField && typeof row[versionField.key] === "number") {
|
|
981
|
-
where = { and: [where, { [versionField.key]: { eq: row[versionField.key] } }] };
|
|
982
|
-
row[versionField.key] += 1;
|
|
983
|
-
}
|
|
1140
|
+
async runTestMigration() {
|
|
1141
|
+
const migrationsFolder = this.getMigrationsFolder();
|
|
984
1142
|
try {
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
} catch (error) {
|
|
989
|
-
if (error instanceof DbEntityNotFoundError && versionField) try {
|
|
990
|
-
await this.findById(id);
|
|
991
|
-
throw new DbVersionMismatchError(this.tableName, id);
|
|
992
|
-
} catch (lookupError) {
|
|
993
|
-
if (lookupError instanceof DbEntityNotFoundError) throw error;
|
|
994
|
-
if (lookupError instanceof DbVersionMismatchError) throw lookupError;
|
|
995
|
-
throw lookupError;
|
|
996
|
-
}
|
|
997
|
-
throw error;
|
|
1143
|
+
await this.executeMigrations(migrationsFolder);
|
|
1144
|
+
} catch {
|
|
1145
|
+
this.log.warn("D1 migrations failed in test environment - ensure migrations exist");
|
|
998
1146
|
}
|
|
999
1147
|
}
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1148
|
+
};
|
|
1149
|
+
|
|
1150
|
+
//#endregion
|
|
1151
|
+
//#region ../../src/orm/providers/drivers/NodePostgresProvider.ts
|
|
1152
|
+
const envSchema$2 = t.object({
|
|
1153
|
+
DATABASE_URL: t.optional(t.text()),
|
|
1154
|
+
POSTGRES_SCHEMA: t.optional(t.text())
|
|
1155
|
+
});
|
|
1156
|
+
var NodePostgresProvider = class NodePostgresProvider extends DatabaseProvider {
|
|
1157
|
+
static SSL_MODES = [
|
|
1158
|
+
"require",
|
|
1159
|
+
"allow",
|
|
1160
|
+
"prefer",
|
|
1161
|
+
"verify-full"
|
|
1162
|
+
];
|
|
1163
|
+
log = $logger();
|
|
1164
|
+
env = $env(envSchema$2);
|
|
1165
|
+
kit = $inject(DrizzleKitProvider);
|
|
1166
|
+
builder = $inject(PostgresModelBuilder);
|
|
1167
|
+
client;
|
|
1168
|
+
pg;
|
|
1169
|
+
dialect = "postgresql";
|
|
1170
|
+
get name() {
|
|
1171
|
+
return "postgres";
|
|
1005
1172
|
}
|
|
1006
1173
|
/**
|
|
1007
|
-
*
|
|
1174
|
+
* In testing mode, the schema name will be generated and deleted after the test.
|
|
1008
1175
|
*/
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
data
|
|
1014
|
-
});
|
|
1015
|
-
const updatedAtField = getAttrFields(this.entity.schema, PG_UPDATED_AT)?.[0];
|
|
1016
|
-
if (updatedAtField) data[updatedAtField.key] = this.dateTimeProvider.of(opts.now).toISOString();
|
|
1017
|
-
where = this.withDeletedAt(where, opts);
|
|
1018
|
-
data = this.cast(data, false);
|
|
1019
|
-
try {
|
|
1020
|
-
const entities = await this.rawUpdate(opts).set(data).where(this.toSQL(where)).returning();
|
|
1021
|
-
await this.alepha.events.emit("repository:update:after", {
|
|
1022
|
-
tableName: this.tableName,
|
|
1023
|
-
where,
|
|
1024
|
-
data,
|
|
1025
|
-
entities
|
|
1026
|
-
});
|
|
1027
|
-
return entities.map((it) => it[this.id.key]);
|
|
1028
|
-
} catch (error) {
|
|
1029
|
-
throw this.handleError(error, "Update query has failed");
|
|
1030
|
-
}
|
|
1176
|
+
schemaForTesting = this.alepha.isTest() ? this.env.POSTGRES_SCHEMA?.startsWith("test_") ? this.env.POSTGRES_SCHEMA : this.generateTestSchemaName() : void 0;
|
|
1177
|
+
get url() {
|
|
1178
|
+
if (!this.env.DATABASE_URL) throw new AlephaError("DATABASE_URL is not defined in the environment");
|
|
1179
|
+
return this.env.DATABASE_URL;
|
|
1031
1180
|
}
|
|
1032
1181
|
/**
|
|
1033
|
-
*
|
|
1034
|
-
* @returns Array of deleted entity IDs
|
|
1182
|
+
* Execute a SQL statement.
|
|
1035
1183
|
*/
|
|
1036
|
-
|
|
1037
|
-
const deletedAt = this.deletedAt();
|
|
1038
|
-
if (deletedAt && !opts.force) return await this.updateMany(where, { [deletedAt.key]: opts.now ?? this.dateTimeProvider.nowISOString() }, opts);
|
|
1039
|
-
await this.alepha.events.emit("repository:delete:before", {
|
|
1040
|
-
tableName: this.tableName,
|
|
1041
|
-
where
|
|
1042
|
-
});
|
|
1184
|
+
execute(statement) {
|
|
1043
1185
|
try {
|
|
1044
|
-
|
|
1045
|
-
await this.alepha.events.emit("repository:delete:after", {
|
|
1046
|
-
tableName: this.tableName,
|
|
1047
|
-
where,
|
|
1048
|
-
ids
|
|
1049
|
-
});
|
|
1050
|
-
return ids;
|
|
1186
|
+
return this.db.execute(statement);
|
|
1051
1187
|
} catch (error) {
|
|
1052
|
-
throw new DbError("
|
|
1188
|
+
throw new DbError("Error executing statement", error);
|
|
1053
1189
|
}
|
|
1054
1190
|
}
|
|
1055
1191
|
/**
|
|
1056
|
-
*
|
|
1057
|
-
* @returns Array of deleted entity IDs
|
|
1192
|
+
* Get Postgres schema used by this provider.
|
|
1058
1193
|
*/
|
|
1059
|
-
|
|
1060
|
-
return this.
|
|
1194
|
+
get schema() {
|
|
1195
|
+
if (this.schemaForTesting) return this.schemaForTesting;
|
|
1196
|
+
if (this.env.POSTGRES_SCHEMA) return this.env.POSTGRES_SCHEMA;
|
|
1197
|
+
return "public";
|
|
1061
1198
|
}
|
|
1062
1199
|
/**
|
|
1063
|
-
*
|
|
1064
|
-
*
|
|
1065
|
-
* You must fetch the entity first in order to delete it.
|
|
1066
|
-
* @returns Array containing the deleted entity ID
|
|
1200
|
+
* Get the Drizzle Postgres database instance.
|
|
1067
1201
|
*/
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
const deletedAt = this.deletedAt();
|
|
1072
|
-
if (deletedAt && !opts.force) {
|
|
1073
|
-
opts.now ??= this.dateTimeProvider.nowISOString();
|
|
1074
|
-
entity[deletedAt.key] = opts.now;
|
|
1075
|
-
}
|
|
1076
|
-
return await this.deleteById(id, opts);
|
|
1202
|
+
get db() {
|
|
1203
|
+
if (!this.pg) throw new AlephaError("Database not initialized");
|
|
1204
|
+
return this.pg;
|
|
1077
1205
|
}
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1206
|
+
async executeMigrations(migrationsFolder) {
|
|
1207
|
+
await migrate(this.db, { migrationsFolder });
|
|
1208
|
+
}
|
|
1209
|
+
onStart = $hook({
|
|
1210
|
+
on: "start",
|
|
1211
|
+
handler: async () => {
|
|
1212
|
+
await this.connect();
|
|
1213
|
+
if (!this.alepha.isServerless()) try {
|
|
1214
|
+
await this.migrateLock.run();
|
|
1215
|
+
} catch (error) {
|
|
1216
|
+
throw new DbMigrationError(error);
|
|
1217
|
+
}
|
|
1218
|
+
}
|
|
1219
|
+
});
|
|
1220
|
+
onStop = $hook({
|
|
1221
|
+
on: "stop",
|
|
1222
|
+
handler: async () => {
|
|
1223
|
+
if (this.alepha.isTest() && this.schemaForTesting && this.schemaForTesting.startsWith("test_")) {
|
|
1224
|
+
if (!/^test_[a-z0-9_]+$/i.test(this.schemaForTesting)) throw new AlephaError(`Invalid test schema name: ${this.schemaForTesting}. Must match pattern: test_[a-z0-9_]+`);
|
|
1225
|
+
this.log.warn(`Deleting test schema '${this.schemaForTesting}' ...`);
|
|
1226
|
+
await this.execute(sql$1`DROP SCHEMA IF EXISTS ${sql$1.raw(this.schemaForTesting)} CASCADE`);
|
|
1227
|
+
this.log.info(`Test schema '${this.schemaForTesting}' deleted`);
|
|
1228
|
+
}
|
|
1229
|
+
await this.close();
|
|
1230
|
+
}
|
|
1231
|
+
});
|
|
1232
|
+
async connect() {
|
|
1233
|
+
this.log.debug("Connect ..");
|
|
1234
|
+
const client = postgres(this.getClientOptions());
|
|
1235
|
+
await client`SELECT 1`;
|
|
1236
|
+
this.client = client;
|
|
1237
|
+
this.pg = drizzle$1(client, { logger: { logQuery: (query, params) => {
|
|
1238
|
+
this.log.trace(query, { params });
|
|
1239
|
+
} } });
|
|
1240
|
+
this.log.info("Connection OK");
|
|
1084
1241
|
}
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
return result;
|
|
1242
|
+
async close() {
|
|
1243
|
+
if (this.client) {
|
|
1244
|
+
this.log.debug("Close...");
|
|
1245
|
+
await this.client.end();
|
|
1246
|
+
this.client = void 0;
|
|
1247
|
+
this.pg = void 0;
|
|
1248
|
+
this.log.info("Connection closed");
|
|
1249
|
+
}
|
|
1094
1250
|
}
|
|
1251
|
+
migrateLock = $lock({ handler: async () => {
|
|
1252
|
+
await this.migrate();
|
|
1253
|
+
} });
|
|
1095
1254
|
/**
|
|
1096
|
-
*
|
|
1255
|
+
* Map the DATABASE_URL to postgres client options.
|
|
1097
1256
|
*/
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
return
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1257
|
+
getClientOptions() {
|
|
1258
|
+
const url = new URL(this.url);
|
|
1259
|
+
return {
|
|
1260
|
+
host: url.hostname,
|
|
1261
|
+
user: decodeURIComponent(url.username),
|
|
1262
|
+
database: decodeURIComponent(url.pathname.replace("/", "")),
|
|
1263
|
+
password: decodeURIComponent(url.password),
|
|
1264
|
+
port: Number(url.port || 5432),
|
|
1265
|
+
ssl: this.ssl(url),
|
|
1266
|
+
onnotice: () => {}
|
|
1267
|
+
};
|
|
1107
1268
|
}
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
const
|
|
1111
|
-
if (!deletedAt) return where;
|
|
1112
|
-
return { and: [where, { [deletedAt.key]: { isNull: true } }] };
|
|
1269
|
+
ssl(url) {
|
|
1270
|
+
const mode = url.searchParams.get("sslmode");
|
|
1271
|
+
for (const it of NodePostgresProvider.SSL_MODES) if (mode === it) return it;
|
|
1113
1272
|
}
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1273
|
+
};
|
|
1274
|
+
|
|
1275
|
+
//#endregion
|
|
1276
|
+
//#region ../../src/orm/providers/drivers/NodeSqliteProvider.ts
|
|
1277
|
+
const envSchema$1 = t.object({ DATABASE_URL: t.optional(t.text()) });
|
|
1278
|
+
/**
|
|
1279
|
+
* Configuration options for the Node.js SQLite database provider.
|
|
1280
|
+
*/
|
|
1281
|
+
const nodeSqliteOptions = $atom({
|
|
1282
|
+
name: "alepha.postgres.node-sqlite.options",
|
|
1283
|
+
schema: t.object({ path: t.optional(t.string({ description: "Filepath or :memory:. If empty, provider will use DATABASE_URL from env." })) }),
|
|
1284
|
+
default: {}
|
|
1285
|
+
});
|
|
1286
|
+
/**
|
|
1287
|
+
* Add a fake support for SQLite in Node.js based on Postgres interfaces.
|
|
1288
|
+
*
|
|
1289
|
+
* This is NOT a real SQLite provider, it's a workaround to use SQLite with Drizzle ORM.
|
|
1290
|
+
* This is NOT recommended for production use.
|
|
1291
|
+
*/
|
|
1292
|
+
var NodeSqliteProvider = class extends DatabaseProvider {
|
|
1293
|
+
kit = $inject(DrizzleKitProvider);
|
|
1294
|
+
log = $logger();
|
|
1295
|
+
env = $env(envSchema$1);
|
|
1296
|
+
builder = $inject(SqliteModelBuilder);
|
|
1297
|
+
options = $use(nodeSqliteOptions);
|
|
1298
|
+
sqlite;
|
|
1299
|
+
get name() {
|
|
1300
|
+
return "sqlite";
|
|
1117
1301
|
}
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1302
|
+
dialect = "sqlite";
|
|
1303
|
+
get url() {
|
|
1304
|
+
const path = this.options.path ?? this.env.DATABASE_URL;
|
|
1305
|
+
if (path) {
|
|
1306
|
+
if (path.startsWith("postgres://")) throw new AlephaError("Postgres URL is not supported for SQLite provider.");
|
|
1307
|
+
return path;
|
|
1308
|
+
}
|
|
1309
|
+
if (this.alepha.isTest() || this.alepha.isServerless()) return ":memory:";
|
|
1310
|
+
else return "node_modules/.alepha/sqlite.db";
|
|
1124
1311
|
}
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
if (t.schema.isDateTime(value)) row[key] = this.dateTimeProvider.of(row[key]).toISOString();
|
|
1133
|
-
else if (t.schema.isDate(value)) row[key] = this.dateTimeProvider.of(`${row[key]}T00:00:00Z`).toISOString().split("T")[0];
|
|
1134
|
-
}
|
|
1135
|
-
if (typeof row[key] === "bigint" && t.schema.isBigInt(value)) row[key] = row[key].toString();
|
|
1312
|
+
async execute(query) {
|
|
1313
|
+
const { sql: sql$2, params, method } = this.db.all(query).getQuery();
|
|
1314
|
+
this.log.trace(`${sql$2}`, params);
|
|
1315
|
+
const statement = this.sqlite.prepare(sql$2);
|
|
1316
|
+
if (method === "run") {
|
|
1317
|
+
statement.run(...params);
|
|
1318
|
+
return [];
|
|
1136
1319
|
}
|
|
1137
|
-
|
|
1320
|
+
if (method === "get") {
|
|
1321
|
+
const data = statement.get(...params);
|
|
1322
|
+
return data ? [{ ...data }] : [];
|
|
1323
|
+
}
|
|
1324
|
+
return statement.all(...params);
|
|
1138
1325
|
}
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
const joinedData = {};
|
|
1146
|
-
for (const join of joinsAtThisLevel) {
|
|
1147
|
-
joinedData[join.key] = cleanRow[join.key];
|
|
1148
|
-
delete cleanRow[join.key];
|
|
1326
|
+
db = drizzle$2(async (sql$2, params, method) => {
|
|
1327
|
+
const statement = this.sqlite.prepare(sql$2);
|
|
1328
|
+
this.log.trace(`${sql$2}`, { params });
|
|
1329
|
+
if (method === "get") {
|
|
1330
|
+
const data = statement.get(...params);
|
|
1331
|
+
return { rows: data ? [{ ...data }] : [] };
|
|
1149
1332
|
}
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
if (joinedValue != null) {
|
|
1154
|
-
const joinPath = parentPath ? `${parentPath}.${join.key}` : join.key;
|
|
1155
|
-
if (joins.filter((j) => j.parent === joinPath).length > 0) entity[join.key] = this.cleanWithJoins(joinedValue, join.schema, joins, joinPath);
|
|
1156
|
-
else entity[join.key] = this.clean(joinedValue, join.schema);
|
|
1157
|
-
} else entity[join.key] = void 0;
|
|
1333
|
+
if (method === "run") {
|
|
1334
|
+
statement.run(...params);
|
|
1335
|
+
return { rows: [] };
|
|
1158
1336
|
}
|
|
1159
|
-
return
|
|
1337
|
+
if (method === "all") return { rows: statement.all(...params).map((row) => Object.values(row)) };
|
|
1338
|
+
if (method === "values") return { rows: statement.all(...params).map((row) => Object.values(row)) };
|
|
1339
|
+
throw new AlephaError(`Unsupported method: ${method}`);
|
|
1340
|
+
});
|
|
1341
|
+
onStart = $hook({
|
|
1342
|
+
on: "start",
|
|
1343
|
+
handler: async () => {
|
|
1344
|
+
const { DatabaseSync } = await import("node:sqlite");
|
|
1345
|
+
const filepath = this.url.replace("sqlite://", "").replace("sqlite:", "");
|
|
1346
|
+
if (filepath !== ":memory:" && filepath !== "") {
|
|
1347
|
+
const dirname = filepath.split("/").slice(0, -1).join("/");
|
|
1348
|
+
if (dirname) await mkdir(dirname, { recursive: true }).catch(() => null);
|
|
1349
|
+
}
|
|
1350
|
+
this.sqlite = new DatabaseSync(filepath);
|
|
1351
|
+
await this.migrate();
|
|
1352
|
+
this.log.info(`Using SQLite database at ${filepath}`);
|
|
1353
|
+
}
|
|
1354
|
+
});
|
|
1355
|
+
async executeMigrations(migrationsFolder) {
|
|
1356
|
+
await migrate$1(this.db, async (migrationQueries) => {
|
|
1357
|
+
this.log.debug("Executing migration queries", { migrationQueries });
|
|
1358
|
+
for (const query of migrationQueries) this.sqlite.prepare(query).run();
|
|
1359
|
+
}, { migrationsFolder });
|
|
1160
1360
|
}
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
dialect: this.provider.dialect
|
|
1172
|
-
});
|
|
1361
|
+
};
|
|
1362
|
+
|
|
1363
|
+
//#endregion
|
|
1364
|
+
//#region ../../src/orm/providers/drivers/PglitePostgresProvider.ts
|
|
1365
|
+
const envSchema = t.object({ DATABASE_URL: t.optional(t.text()) });
|
|
1366
|
+
var PglitePostgresProvider = class PglitePostgresProvider extends DatabaseProvider {
|
|
1367
|
+
static importPglite() {
|
|
1368
|
+
try {
|
|
1369
|
+
return createRequire(import.meta.url)("@electric-sql/pglite");
|
|
1370
|
+
} catch {}
|
|
1173
1371
|
}
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
return
|
|
1372
|
+
env = $env(envSchema);
|
|
1373
|
+
log = $logger();
|
|
1374
|
+
kit = $inject(DrizzleKitProvider);
|
|
1375
|
+
builder = $inject(PostgresModelBuilder);
|
|
1376
|
+
client;
|
|
1377
|
+
pglite;
|
|
1378
|
+
get name() {
|
|
1379
|
+
return "postgres";
|
|
1182
1380
|
}
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
if (
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1381
|
+
get driver() {
|
|
1382
|
+
return "pglite";
|
|
1383
|
+
}
|
|
1384
|
+
dialect = "postgresql";
|
|
1385
|
+
get url() {
|
|
1386
|
+
let path = this.env.DATABASE_URL;
|
|
1387
|
+
if (!path) if (this.alepha.isTest()) path = ":memory:";
|
|
1388
|
+
else path = "node_modules/.alepha/pglite";
|
|
1389
|
+
else if (path.includes(":memory:")) path = ":memory:";
|
|
1390
|
+
else if (path.startsWith("file://")) path = path.replace("file://", "");
|
|
1391
|
+
return path;
|
|
1392
|
+
}
|
|
1393
|
+
get db() {
|
|
1394
|
+
if (!this.pglite) throw new AlephaError("Database not initialized");
|
|
1395
|
+
return this.pglite;
|
|
1396
|
+
}
|
|
1397
|
+
async execute(statement) {
|
|
1398
|
+
const { rows } = await this.db.execute(statement);
|
|
1399
|
+
return rows;
|
|
1400
|
+
}
|
|
1401
|
+
onStart = $hook({
|
|
1402
|
+
on: "start",
|
|
1403
|
+
handler: async () => {
|
|
1404
|
+
if (Object.keys(this.kit.getModels(this)).length === 0) return;
|
|
1405
|
+
const module = PglitePostgresProvider.importPglite();
|
|
1406
|
+
if (!module) throw new AlephaError("@electric-sql/pglite is not installed. Please install it to use the pglite driver.");
|
|
1407
|
+
const { drizzle: drizzle$3 } = createRequire(import.meta.url)("drizzle-orm/pglite");
|
|
1408
|
+
const path = this.url;
|
|
1409
|
+
if (path !== ":memory:") {
|
|
1410
|
+
await mkdir(path, { recursive: true }).catch(() => null);
|
|
1411
|
+
this.client = new module.PGlite(path);
|
|
1412
|
+
} else this.client = new module.PGlite();
|
|
1413
|
+
this.pglite = drizzle$3({ client: this.client });
|
|
1414
|
+
await this.migrate();
|
|
1415
|
+
this.log.info(`Using PGlite database at ${path}`);
|
|
1416
|
+
}
|
|
1417
|
+
});
|
|
1418
|
+
onStop = $hook({
|
|
1419
|
+
on: "stop",
|
|
1420
|
+
handler: async () => {
|
|
1421
|
+
if (this.client) {
|
|
1422
|
+
this.log.debug("Closing PGlite connection...");
|
|
1423
|
+
await this.client.close();
|
|
1424
|
+
this.client = void 0;
|
|
1425
|
+
this.pglite = void 0;
|
|
1426
|
+
this.log.info("PGlite connection closed");
|
|
1427
|
+
}
|
|
1428
|
+
}
|
|
1429
|
+
});
|
|
1430
|
+
async executeMigrations(migrationsFolder) {
|
|
1431
|
+
await migrate$2(this.db, { migrationsFolder });
|
|
1195
1432
|
}
|
|
1196
1433
|
};
|
|
1197
1434
|
|
|
1198
1435
|
//#endregion
|
|
1199
|
-
//#region ../../src/orm/
|
|
1200
|
-
var
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
createClassRepository(entity) {
|
|
1213
|
-
let name = entity.name.charAt(0).toUpperCase() + entity.name.slice(1);
|
|
1214
|
-
if (name.endsWith("s")) name = name.slice(0, -1);
|
|
1215
|
-
name = `${name}Repository`;
|
|
1216
|
-
if (this.registry.has(entity)) return this.registry.get(entity);
|
|
1217
|
-
class GenericRepository extends Repository {
|
|
1218
|
-
constructor() {
|
|
1219
|
-
super(entity);
|
|
1220
|
-
}
|
|
1221
|
-
}
|
|
1222
|
-
Object.defineProperty(GenericRepository, "name", { value: name });
|
|
1223
|
-
this.registry.set(entity, GenericRepository);
|
|
1224
|
-
return GenericRepository;
|
|
1436
|
+
//#region ../../src/orm/errors/DbConflictError.ts
|
|
1437
|
+
var DbConflictError = class extends DbError {
|
|
1438
|
+
name = "DbConflictError";
|
|
1439
|
+
status = 409;
|
|
1440
|
+
};
|
|
1441
|
+
|
|
1442
|
+
//#endregion
|
|
1443
|
+
//#region ../../src/orm/errors/DbEntityNotFoundError.ts
|
|
1444
|
+
var DbEntityNotFoundError = class extends DbError {
|
|
1445
|
+
name = "DbEntityNotFoundError";
|
|
1446
|
+
status = 404;
|
|
1447
|
+
constructor(entityName) {
|
|
1448
|
+
super(`Entity from '${entityName}' was not found`);
|
|
1225
1449
|
}
|
|
1226
1450
|
};
|
|
1227
1451
|
|
|
1228
1452
|
//#endregion
|
|
1229
|
-
//#region ../../src/orm/
|
|
1453
|
+
//#region ../../src/orm/errors/DbVersionMismatchError.ts
|
|
1230
1454
|
/**
|
|
1231
|
-
*
|
|
1455
|
+
* Error thrown when there is a version mismatch.
|
|
1456
|
+
* It's thrown by {@link Repository#save} when the updated entity version does not match the one in the database.
|
|
1457
|
+
* This is used for optimistic concurrency control.
|
|
1232
1458
|
*/
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1459
|
+
var DbVersionMismatchError = class extends DbError {
|
|
1460
|
+
name = "DbVersionMismatchError";
|
|
1461
|
+
constructor(table, id) {
|
|
1462
|
+
super(`Version mismatch for table '${table}' and id '${id}'`);
|
|
1463
|
+
}
|
|
1236
1464
|
};
|
|
1237
1465
|
|
|
1238
1466
|
//#endregion
|
|
1239
|
-
//#region ../../src/orm/
|
|
1467
|
+
//#region ../../src/orm/helpers/pgAttr.ts
|
|
1240
1468
|
/**
|
|
1241
|
-
*
|
|
1469
|
+
* Decorates a typebox schema with a Postgres attribute.
|
|
1470
|
+
*
|
|
1471
|
+
* > It's just a fancy way to add Symbols to a field.
|
|
1472
|
+
*
|
|
1473
|
+
* @example
|
|
1474
|
+
* ```ts
|
|
1475
|
+
* import { t } from "alepha";
|
|
1476
|
+
* import { PG_UPDATED_AT } from "../constants/PG_SYMBOLS";
|
|
1477
|
+
*
|
|
1478
|
+
* export const updatedAtSchema = pgAttr(
|
|
1479
|
+
* t.datetime(), PG_UPDATED_AT,
|
|
1480
|
+
* );
|
|
1481
|
+
* ```
|
|
1242
1482
|
*/
|
|
1243
|
-
const
|
|
1244
|
-
|
|
1483
|
+
const pgAttr = (type, attr, value) => {
|
|
1484
|
+
Object.assign(type, { [attr]: value ?? {} });
|
|
1485
|
+
return type;
|
|
1245
1486
|
};
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
return this.provider.execute(sql$1`SELECT last_value FROM ${sql$1.raw(this.provider.schema)}."${sql$1.raw(this.name)}"`).then((rows) => Number(rows[0]?.last_value));
|
|
1259
|
-
}
|
|
1260
|
-
$provider() {
|
|
1261
|
-
return this.options.provider ?? this.alepha.inject(DatabaseProvider);
|
|
1487
|
+
/**
|
|
1488
|
+
* Retrieves the fields of a schema that have a specific attribute.
|
|
1489
|
+
*/
|
|
1490
|
+
const getAttrFields = (schema$1, name) => {
|
|
1491
|
+
const fields = [];
|
|
1492
|
+
for (const key of Object.keys(schema$1.properties)) {
|
|
1493
|
+
const value = schema$1.properties[key];
|
|
1494
|
+
if (name in value) fields.push({
|
|
1495
|
+
type: value,
|
|
1496
|
+
key,
|
|
1497
|
+
data: value[name]
|
|
1498
|
+
});
|
|
1262
1499
|
}
|
|
1500
|
+
return fields;
|
|
1263
1501
|
};
|
|
1264
|
-
$sequence[KIND] = SequencePrimitive;
|
|
1265
1502
|
|
|
1266
1503
|
//#endregion
|
|
1267
|
-
//#region ../../src/orm/
|
|
1268
|
-
var
|
|
1269
|
-
log = $logger();
|
|
1270
|
-
alepha = $inject(Alepha);
|
|
1271
|
-
/**
|
|
1272
|
-
* Synchronize database with current schema definitions.
|
|
1273
|
-
*
|
|
1274
|
-
* In development mode, it will generate and execute migrations based on the current state.
|
|
1275
|
-
* In testing mode, it will generate migrations from scratch without applying them.
|
|
1276
|
-
*
|
|
1277
|
-
* Does nothing in production mode, you must handle migrations manually.
|
|
1278
|
-
*/
|
|
1279
|
-
async synchronize(provider) {
|
|
1280
|
-
if (this.alepha.isProduction()) {
|
|
1281
|
-
this.log.warn("Synchronization skipped in production mode.");
|
|
1282
|
-
return;
|
|
1283
|
-
}
|
|
1284
|
-
if (provider.schema !== "public") await this.createSchemaIfNotExists(provider, provider.schema);
|
|
1285
|
-
const now = Date.now();
|
|
1286
|
-
if (this.alepha.isTest()) {
|
|
1287
|
-
const { statements } = await this.generateMigration(provider);
|
|
1288
|
-
await this.executeStatements(statements, provider);
|
|
1289
|
-
} else {
|
|
1290
|
-
const entry = await this.loadDevMigrations(provider);
|
|
1291
|
-
const { statements, snapshot } = await this.generateMigration(provider, entry?.snapshot ? JSON.parse(entry.snapshot) : void 0);
|
|
1292
|
-
await this.executeStatements(statements, provider, true);
|
|
1293
|
-
await this.saveDevMigrations(provider, snapshot, entry);
|
|
1294
|
-
}
|
|
1295
|
-
this.log.info(`Db '${provider.name}' synchronization OK [${Date.now() - now}ms]`);
|
|
1296
|
-
}
|
|
1297
|
-
/**
|
|
1298
|
-
* Mostly used for testing purposes. You can generate SQL migration statements without executing them.
|
|
1299
|
-
*/
|
|
1300
|
-
async generateMigration(provider, prevSnapshot) {
|
|
1301
|
-
const kit = this.importDrizzleKit();
|
|
1302
|
-
const models = this.getModels(provider);
|
|
1303
|
-
if (Object.keys(models).length > 0) {
|
|
1304
|
-
if (provider.dialect === "sqlite") {
|
|
1305
|
-
const prev$1 = prevSnapshot ?? await kit.generateSQLiteDrizzleJson({});
|
|
1306
|
-
const curr$1 = await kit.generateSQLiteDrizzleJson(models);
|
|
1307
|
-
return {
|
|
1308
|
-
models,
|
|
1309
|
-
statements: await kit.generateSQLiteMigration(prev$1, curr$1),
|
|
1310
|
-
snapshot: curr$1
|
|
1311
|
-
};
|
|
1312
|
-
}
|
|
1313
|
-
const prev = prevSnapshot ?? await kit.generateDrizzleJson({});
|
|
1314
|
-
const curr = await kit.generateDrizzleJson(models);
|
|
1315
|
-
return {
|
|
1316
|
-
models,
|
|
1317
|
-
statements: await kit.generateMigration(prev, curr),
|
|
1318
|
-
snapshot: curr
|
|
1319
|
-
};
|
|
1320
|
-
}
|
|
1321
|
-
return {
|
|
1322
|
-
models,
|
|
1323
|
-
statements: [],
|
|
1324
|
-
snapshot: {}
|
|
1325
|
-
};
|
|
1326
|
-
}
|
|
1327
|
-
/**
|
|
1328
|
-
* Load all tables, enums, sequences, etc. from the provider's repositories.
|
|
1329
|
-
*/
|
|
1330
|
-
getModels(provider) {
|
|
1331
|
-
const models = {};
|
|
1332
|
-
for (const [key, value] of provider.tables.entries()) {
|
|
1333
|
-
if (models[key]) throw new AlephaError(`Model name conflict: '${key}' is already defined.`);
|
|
1334
|
-
models[key] = value;
|
|
1335
|
-
}
|
|
1336
|
-
for (const [key, value] of provider.enums.entries()) {
|
|
1337
|
-
if (models[key]) throw new AlephaError(`Model name conflict: '${key}' is already defined.`);
|
|
1338
|
-
models[key] = value;
|
|
1339
|
-
}
|
|
1340
|
-
for (const [key, value] of provider.sequences.entries()) {
|
|
1341
|
-
if (models[key]) throw new AlephaError(`Model name conflict: '${key}' is already defined.`);
|
|
1342
|
-
models[key] = value;
|
|
1343
|
-
}
|
|
1344
|
-
return models;
|
|
1345
|
-
}
|
|
1504
|
+
//#region ../../src/orm/services/PgRelationManager.ts
|
|
1505
|
+
var PgRelationManager = class {
|
|
1346
1506
|
/**
|
|
1347
|
-
*
|
|
1348
|
-
*/
|
|
1349
|
-
|
|
1350
|
-
const
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
await provider.execute(sql$1`CREATE SCHEMA IF NOT EXISTS "drizzle";`);
|
|
1365
|
-
await provider.execute(sql$1`
|
|
1366
|
-
CREATE TABLE IF NOT EXISTS "drizzle"."__drizzle_dev_migrations" (
|
|
1367
|
-
"id" SERIAL PRIMARY KEY,
|
|
1368
|
-
"name" TEXT NOT NULL,
|
|
1369
|
-
"created_at" TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
1370
|
-
"snapshot" TEXT NOT NULL
|
|
1371
|
-
);
|
|
1372
|
-
`);
|
|
1373
|
-
const rows = await provider.run(sql$1`SELECT * FROM "drizzle"."__drizzle_dev_migrations" WHERE "name" = ${name} LIMIT 1`, devMigrationsSchema);
|
|
1374
|
-
if (rows.length === 0) {
|
|
1375
|
-
this.log.trace(`No existing migration snapshot for '${name}'`);
|
|
1376
|
-
return;
|
|
1377
|
-
}
|
|
1378
|
-
return this.alepha.codec.decode(devMigrationsSchema, rows[0]);
|
|
1379
|
-
}
|
|
1380
|
-
async saveDevMigrations(provider, curr, devMigrations) {
|
|
1381
|
-
if (provider.url.includes(":memory:")) {
|
|
1382
|
-
this.log.trace(`In-memory database detected for '${provider.constructor.name}', skipping migration snapshot save.`);
|
|
1383
|
-
return;
|
|
1384
|
-
}
|
|
1385
|
-
const name = `${this.alepha.env.APP_NAME ?? "APP"}-${provider.constructor.name}`.toLowerCase();
|
|
1386
|
-
if (provider.dialect === "sqlite") {
|
|
1387
|
-
const filePath = `node_modules/.alepha/sqlite-${name}.json`;
|
|
1388
|
-
await mkdir("node_modules/.alepha", { recursive: true }).catch(() => null);
|
|
1389
|
-
await writeFile(filePath, JSON.stringify({
|
|
1390
|
-
id: devMigrations?.id ?? 1,
|
|
1391
|
-
name,
|
|
1392
|
-
created_at: /* @__PURE__ */ new Date(),
|
|
1393
|
-
snapshot: JSON.stringify(curr)
|
|
1394
|
-
}, null, 2));
|
|
1395
|
-
this.log.debug(`Saved migration snapshot to '${filePath}'`);
|
|
1396
|
-
return;
|
|
1397
|
-
}
|
|
1398
|
-
if (!devMigrations) await provider.execute(sql$1`INSERT INTO "drizzle"."__drizzle_dev_migrations" ("name", "snapshot") VALUES (${name}, ${JSON.stringify(curr)})`);
|
|
1399
|
-
else {
|
|
1400
|
-
const newSnapshot = JSON.stringify(curr);
|
|
1401
|
-
if (devMigrations.snapshot !== newSnapshot) await provider.execute(sql$1`UPDATE "drizzle"."__drizzle_dev_migrations" SET "snapshot" = ${newSnapshot} WHERE "id" = ${devMigrations.id}`);
|
|
1507
|
+
* Recursively build joins for the query builder based on the relations map
|
|
1508
|
+
*/
|
|
1509
|
+
buildJoins(provider, builder, joins, withRelations, table, parentKey) {
|
|
1510
|
+
for (const [key, join] of Object.entries(withRelations)) {
|
|
1511
|
+
const from = provider.table(join.join);
|
|
1512
|
+
const on = isSQLWrapper$1(join.on) ? join.on : sql$1`${table[join.on[0]]} = ${from[join.on[1].name]}`;
|
|
1513
|
+
if (join.type === "right") builder.rightJoin(from, on);
|
|
1514
|
+
else if (join.type === "inner") builder.innerJoin(from, on);
|
|
1515
|
+
else builder.leftJoin(from, on);
|
|
1516
|
+
joins.push({
|
|
1517
|
+
key,
|
|
1518
|
+
table: getTableName(from),
|
|
1519
|
+
schema: join.join.schema,
|
|
1520
|
+
col: (name) => from[name],
|
|
1521
|
+
parent: parentKey
|
|
1522
|
+
});
|
|
1523
|
+
if (join.with) this.buildJoins(provider, builder, joins, join.with, from, parentKey ? `${parentKey}.${key}` : key);
|
|
1402
1524
|
}
|
|
1403
1525
|
}
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
this.log.warn(errorMessage, { context: [error] });
|
|
1415
|
-
} else throw error;
|
|
1526
|
+
/**
|
|
1527
|
+
* Map a row with its joined relations based on the joins definition
|
|
1528
|
+
*/
|
|
1529
|
+
mapRowWithJoins(record, row, schema$1, joins, parentKey) {
|
|
1530
|
+
for (const join of joins) if (join.parent === parentKey) {
|
|
1531
|
+
const joinedData = row[join.table];
|
|
1532
|
+
if (this.isAllNull(joinedData)) record[join.key] = void 0;
|
|
1533
|
+
else {
|
|
1534
|
+
record[join.key] = joinedData;
|
|
1535
|
+
this.mapRowWithJoins(record[join.key], row, schema$1, joins, parentKey ? `${parentKey}.${join.key}` : join.key);
|
|
1416
1536
|
}
|
|
1417
1537
|
}
|
|
1418
|
-
|
|
1538
|
+
return record;
|
|
1419
1539
|
}
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
this.log.debug(`Ensuring schema '${schemaName}' exists`);
|
|
1428
|
-
await provider.execute(sql$1`CREATE SCHEMA IF NOT EXISTS ${sqlSchema}`);
|
|
1540
|
+
/**
|
|
1541
|
+
* Check if all values in an object are null (indicates a left join with no match)
|
|
1542
|
+
*/
|
|
1543
|
+
isAllNull(obj) {
|
|
1544
|
+
if (obj === null || obj === void 0) return true;
|
|
1545
|
+
if (typeof obj !== "object") return false;
|
|
1546
|
+
return Object.values(obj).every((val) => val === null);
|
|
1429
1547
|
}
|
|
1430
1548
|
/**
|
|
1431
|
-
*
|
|
1432
|
-
* If not available, fallback to the local kit import.
|
|
1549
|
+
* Build a schema that includes all join properties recursively
|
|
1433
1550
|
*/
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1551
|
+
buildSchemaWithJoins(baseSchema, joins, parentPath) {
|
|
1552
|
+
const schema$1 = Value.Clone(baseSchema);
|
|
1553
|
+
const joinsAtThisLevel = joins.filter((j) => j.parent === parentPath);
|
|
1554
|
+
for (const join of joinsAtThisLevel) {
|
|
1555
|
+
const joinPath = parentPath ? `${parentPath}.${join.key}` : join.key;
|
|
1556
|
+
const childJoins = joins.filter((j) => j.parent === joinPath);
|
|
1557
|
+
let joinSchema = join.schema;
|
|
1558
|
+
if (childJoins.length > 0) joinSchema = this.buildSchemaWithJoins(join.schema, joins, joinPath);
|
|
1559
|
+
schema$1.properties[join.key] = t.optional(joinSchema);
|
|
1439
1560
|
}
|
|
1561
|
+
return schema$1;
|
|
1440
1562
|
}
|
|
1441
1563
|
};
|
|
1442
|
-
const devMigrationsSchema = t.object({
|
|
1443
|
-
id: t.number(),
|
|
1444
|
-
name: t.text(),
|
|
1445
|
-
snapshot: t.string(),
|
|
1446
|
-
created_at: t.string()
|
|
1447
|
-
});
|
|
1448
1564
|
|
|
1449
1565
|
//#endregion
|
|
1450
|
-
//#region ../../src/orm/services/
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
* into drizzle models (tables, enums, sequences, etc...).
|
|
1454
|
-
*/
|
|
1455
|
-
var ModelBuilder = class {
|
|
1456
|
-
/**
|
|
1457
|
-
* Convert camelCase to snake_case for column names.
|
|
1458
|
-
*/
|
|
1459
|
-
toColumnName(str) {
|
|
1460
|
-
return str[0].toLowerCase() + str.slice(1).replace(/[A-Z]/g, (letter) => `_${letter.toLowerCase()}`);
|
|
1461
|
-
}
|
|
1566
|
+
//#region ../../src/orm/services/QueryManager.ts
|
|
1567
|
+
var QueryManager = class {
|
|
1568
|
+
alepha = $inject(Alepha);
|
|
1462
1569
|
/**
|
|
1463
|
-
*
|
|
1464
|
-
* This includes indexes, foreign keys, constraints, and custom config.
|
|
1465
|
-
*
|
|
1466
|
-
* @param entity - The entity primitive
|
|
1467
|
-
* @param builders - Database-specific builder functions
|
|
1468
|
-
* @param tableResolver - Function to resolve entity references to table columns
|
|
1469
|
-
* @param customConfigHandler - Optional handler for custom config
|
|
1570
|
+
* Convert a query object to a SQL query.
|
|
1470
1571
|
*/
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
if (
|
|
1482
|
-
const
|
|
1483
|
-
const
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1572
|
+
toSQL(query, options) {
|
|
1573
|
+
const { schema: schema$1, col, joins } = options;
|
|
1574
|
+
const conditions = [];
|
|
1575
|
+
if (isSQLWrapper(query)) conditions.push(query);
|
|
1576
|
+
else {
|
|
1577
|
+
const keys = Object.keys(query);
|
|
1578
|
+
for (const key of keys) {
|
|
1579
|
+
const operator = query[key];
|
|
1580
|
+
if (typeof query[key] === "object" && query[key] != null && !Array.isArray(query[key]) && joins?.length) {
|
|
1581
|
+
const matchingJoins = joins.filter((j) => j.key === key);
|
|
1582
|
+
if (matchingJoins.length > 0) {
|
|
1583
|
+
const join = matchingJoins[0];
|
|
1584
|
+
const joinPath = join.parent ? `${join.parent}.${key}` : key;
|
|
1585
|
+
const recursiveJoins = joins.filter((j) => {
|
|
1586
|
+
if (!j.parent) return false;
|
|
1587
|
+
return j.parent === joinPath || j.parent.startsWith(`${joinPath}.`);
|
|
1588
|
+
}).map((j) => {
|
|
1589
|
+
const newParent = j.parent === joinPath ? void 0 : j.parent.substring(joinPath.length + 1);
|
|
1590
|
+
return {
|
|
1591
|
+
...j,
|
|
1592
|
+
parent: newParent
|
|
1593
|
+
};
|
|
1594
|
+
});
|
|
1595
|
+
const sql$2 = this.toSQL(query[key], {
|
|
1596
|
+
schema: join.schema,
|
|
1597
|
+
col: join.col,
|
|
1598
|
+
joins: recursiveJoins.length > 0 ? recursiveJoins : void 0,
|
|
1599
|
+
dialect: options.dialect
|
|
1600
|
+
});
|
|
1601
|
+
if (sql$2) conditions.push(sql$2);
|
|
1602
|
+
continue;
|
|
1492
1603
|
}
|
|
1493
1604
|
}
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1605
|
+
if (Array.isArray(operator)) {
|
|
1606
|
+
const operations = operator.map((it) => {
|
|
1607
|
+
if (isSQLWrapper(it)) return it;
|
|
1608
|
+
return this.toSQL(it, {
|
|
1609
|
+
schema: schema$1,
|
|
1610
|
+
col,
|
|
1611
|
+
joins,
|
|
1612
|
+
dialect: options.dialect
|
|
1613
|
+
});
|
|
1614
|
+
}).filter((it) => it != null);
|
|
1615
|
+
if (key === "and") return and(...operations);
|
|
1616
|
+
if (key === "or") return or(...operations);
|
|
1617
|
+
}
|
|
1618
|
+
if (key === "not") {
|
|
1619
|
+
const where = this.toSQL(operator, {
|
|
1620
|
+
schema: schema$1,
|
|
1621
|
+
col,
|
|
1622
|
+
joins,
|
|
1623
|
+
dialect: options.dialect
|
|
1509
1624
|
});
|
|
1510
|
-
|
|
1511
|
-
name: fkName,
|
|
1512
|
-
columns: cols,
|
|
1513
|
-
foreignColumns
|
|
1514
|
-
}));
|
|
1625
|
+
if (where) return not(where);
|
|
1515
1626
|
}
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
if (cols.length === constraintDef.columns.length) {
|
|
1521
|
-
if (constraintDef.unique) {
|
|
1522
|
-
const constraintName = constraintDef.name || `${entity.name}_${columnNames.join("_")}_unique`;
|
|
1523
|
-
configs.push(builders.unique(constraintName).on(...cols));
|
|
1524
|
-
}
|
|
1525
|
-
if (constraintDef.check) {
|
|
1526
|
-
const constraintName = constraintDef.name || `${entity.name}_${columnNames.join("_")}_check`;
|
|
1527
|
-
configs.push(builders.check(constraintName, constraintDef.check));
|
|
1528
|
-
}
|
|
1627
|
+
if (operator) {
|
|
1628
|
+
const column = col(key);
|
|
1629
|
+
const sql$2 = this.mapOperatorToSql(operator, column, schema$1, key, options.dialect);
|
|
1630
|
+
if (sql$2) conditions.push(sql$2);
|
|
1529
1631
|
}
|
|
1530
1632
|
}
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
if (Array.isArray(customConfigs)) configs.push(...customConfigs);
|
|
1535
|
-
}
|
|
1536
|
-
return configs;
|
|
1537
|
-
};
|
|
1538
|
-
}
|
|
1539
|
-
};
|
|
1540
|
-
|
|
1541
|
-
//#endregion
|
|
1542
|
-
//#region ../../src/orm/services/SqliteModelBuilder.ts
|
|
1543
|
-
var SqliteModelBuilder = class extends ModelBuilder {
|
|
1544
|
-
buildTable(entity, options) {
|
|
1545
|
-
const tableName = entity.name;
|
|
1546
|
-
if (options.tables.has(tableName)) return;
|
|
1547
|
-
const table = sqliteTable(tableName, this.schemaToSqliteColumns(tableName, entity.schema, options.enums, options.tables), this.getTableConfig(entity, options.tables));
|
|
1548
|
-
options.tables.set(tableName, table);
|
|
1633
|
+
}
|
|
1634
|
+
if (conditions.length === 1) return conditions[0];
|
|
1635
|
+
return and(...conditions);
|
|
1549
1636
|
}
|
|
1550
|
-
|
|
1551
|
-
|
|
1637
|
+
/**
|
|
1638
|
+
* Check if an object has any filter operator properties.
|
|
1639
|
+
*/
|
|
1640
|
+
hasFilterOperatorProperties(obj) {
|
|
1641
|
+
if (!obj || typeof obj !== "object") return false;
|
|
1642
|
+
return [
|
|
1643
|
+
"eq",
|
|
1644
|
+
"ne",
|
|
1645
|
+
"gt",
|
|
1646
|
+
"gte",
|
|
1647
|
+
"lt",
|
|
1648
|
+
"lte",
|
|
1649
|
+
"inArray",
|
|
1650
|
+
"notInArray",
|
|
1651
|
+
"isNull",
|
|
1652
|
+
"isNotNull",
|
|
1653
|
+
"like",
|
|
1654
|
+
"notLike",
|
|
1655
|
+
"ilike",
|
|
1656
|
+
"notIlike",
|
|
1657
|
+
"contains",
|
|
1658
|
+
"startsWith",
|
|
1659
|
+
"endsWith",
|
|
1660
|
+
"between",
|
|
1661
|
+
"notBetween",
|
|
1662
|
+
"arrayContains",
|
|
1663
|
+
"arrayContained",
|
|
1664
|
+
"arrayOverlaps"
|
|
1665
|
+
].some((key) => key in obj);
|
|
1552
1666
|
}
|
|
1553
1667
|
/**
|
|
1554
|
-
*
|
|
1668
|
+
* Map a filter operator to a SQL query.
|
|
1555
1669
|
*/
|
|
1556
|
-
|
|
1557
|
-
const
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1670
|
+
mapOperatorToSql(operator, column, columnSchema, columnName, dialect = "postgresql") {
|
|
1671
|
+
const encodeValue = (value) => {
|
|
1672
|
+
if (value == null) return value;
|
|
1673
|
+
if (columnSchema && columnName) try {
|
|
1674
|
+
const fieldSchema = columnSchema.properties[columnName];
|
|
1675
|
+
if (fieldSchema) return this.alepha.codec.encode(fieldSchema, value, { encoder: "drizzle" });
|
|
1676
|
+
} catch (error) {}
|
|
1677
|
+
return value;
|
|
1563
1678
|
};
|
|
1564
|
-
const
|
|
1565
|
-
return
|
|
1679
|
+
const encodeArray = (values) => {
|
|
1680
|
+
return values.map((v) => encodeValue(v));
|
|
1566
1681
|
};
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
if (
|
|
1577
|
-
|
|
1578
|
-
const config = value[PG_REF];
|
|
1579
|
-
col = col.references(() => {
|
|
1580
|
-
const ref = config.ref();
|
|
1581
|
-
const table = tables.get(ref.entity.name);
|
|
1582
|
-
if (!table) throw new AlephaError(`Referenced table ${ref.entity.name} not found for ${tableName}.${key}`);
|
|
1583
|
-
const target = table[ref.name];
|
|
1584
|
-
if (!target) throw new AlephaError(`Referenced column ${ref.name} not found in table ${ref.entity.name} for ${tableName}.${key}`);
|
|
1585
|
-
return target;
|
|
1586
|
-
}, config.actions);
|
|
1587
|
-
}
|
|
1588
|
-
if (schema$1.required?.includes(key)) col = col.notNull();
|
|
1589
|
-
return {
|
|
1590
|
-
...columns,
|
|
1591
|
-
[key]: col
|
|
1592
|
-
};
|
|
1593
|
-
}, {});
|
|
1594
|
-
};
|
|
1595
|
-
mapFieldToSqliteColumn = (tableName, fieldName, value, enums) => {
|
|
1596
|
-
const key = this.toColumnName(fieldName);
|
|
1597
|
-
if ("anyOf" in value && Array.isArray(value.anyOf) && value.anyOf.length === 2 && value.anyOf.some((it) => t.schema.isNull(it))) value = value.anyOf.find((it) => !t.schema.isNull(it));
|
|
1598
|
-
if (t.schema.isInteger(value)) {
|
|
1599
|
-
if (PG_SERIAL in value || PG_IDENTITY in value) return pg$2.integer(key, { mode: "number" }).primaryKey({ autoIncrement: true });
|
|
1600
|
-
return pg$2.integer(key);
|
|
1601
|
-
}
|
|
1602
|
-
if (t.schema.isBigInt(value)) {
|
|
1603
|
-
if (PG_PRIMARY_KEY in value || PG_IDENTITY in value) return pg$2.integer(key, { mode: "number" }).primaryKey({ autoIncrement: true });
|
|
1604
|
-
return pg$2.integer(key, { mode: "number" });
|
|
1605
|
-
}
|
|
1606
|
-
if (t.schema.isNumber(value)) {
|
|
1607
|
-
if (PG_IDENTITY in value) return pg$2.integer(key, { mode: "number" }).primaryKey({ autoIncrement: true });
|
|
1608
|
-
return pg$2.numeric(key);
|
|
1682
|
+
if (typeof operator !== "object" || operator == null || !this.hasFilterOperatorProperties(operator)) return eq(column, encodeValue(operator));
|
|
1683
|
+
const conditions = [];
|
|
1684
|
+
if (operator?.eq != null) conditions.push(eq(column, encodeValue(operator.eq)));
|
|
1685
|
+
if (operator?.ne != null) conditions.push(ne(column, encodeValue(operator.ne)));
|
|
1686
|
+
if (operator?.gt != null) conditions.push(gt(column, encodeValue(operator.gt)));
|
|
1687
|
+
if (operator?.gte != null) conditions.push(gte(column, encodeValue(operator.gte)));
|
|
1688
|
+
if (operator?.lt != null) conditions.push(lt(column, encodeValue(operator.lt)));
|
|
1689
|
+
if (operator?.lte != null) conditions.push(lte(column, encodeValue(operator.lte)));
|
|
1690
|
+
if (operator?.inArray != null) {
|
|
1691
|
+
if (!Array.isArray(operator.inArray) || operator.inArray.length === 0) throw new AlephaError("inArray operator requires at least one value");
|
|
1692
|
+
conditions.push(inArray(column, encodeArray(operator.inArray)));
|
|
1609
1693
|
}
|
|
1610
|
-
if (
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
if (t.schema.isRecord(value)) return this.sqliteJson(key, value);
|
|
1614
|
-
if (t.schema.isAny(value)) return this.sqliteJson(key, value);
|
|
1615
|
-
if (t.schema.isArray(value)) {
|
|
1616
|
-
if (t.schema.isObject(value.items)) return this.sqliteJson(key, value);
|
|
1617
|
-
if (t.schema.isRecord(value.items)) return this.sqliteJson(key, value);
|
|
1618
|
-
if (t.schema.isAny(value.items)) return this.sqliteJson(key, value);
|
|
1619
|
-
if (t.schema.isString(value.items)) return this.sqliteJson(key, value);
|
|
1620
|
-
if (t.schema.isInteger(value.items)) return this.sqliteJson(key, value);
|
|
1621
|
-
if (t.schema.isNumber(value.items)) return this.sqliteJson(key, value);
|
|
1622
|
-
if (t.schema.isBoolean(value.items)) return this.sqliteJson(key, value);
|
|
1694
|
+
if (operator?.notInArray != null) {
|
|
1695
|
+
if (!Array.isArray(operator.notInArray) || operator.notInArray.length === 0) throw new AlephaError("notInArray operator requires at least one value");
|
|
1696
|
+
conditions.push(notInArray(column, encodeArray(operator.notInArray)));
|
|
1623
1697
|
}
|
|
1624
|
-
if (
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
if (
|
|
1629
|
-
|
|
1630
|
-
|
|
1698
|
+
if (operator?.isNull != null) conditions.push(isNull(column));
|
|
1699
|
+
if (operator?.isNotNull != null) conditions.push(isNotNull(column));
|
|
1700
|
+
if (operator?.like != null) conditions.push(like(column, encodeValue(operator.like)));
|
|
1701
|
+
if (operator?.notLike != null) conditions.push(notLike(column, encodeValue(operator.notLike)));
|
|
1702
|
+
if (operator?.ilike != null) conditions.push(ilike(column, encodeValue(operator.ilike)));
|
|
1703
|
+
if (operator?.notIlike != null) conditions.push(notIlike(column, encodeValue(operator.notIlike)));
|
|
1704
|
+
if (operator?.contains != null) {
|
|
1705
|
+
const escapedValue = String(operator.contains).replace(/\\/g, "\\\\").replace(/%/g, "\\%").replace(/_/g, "\\_");
|
|
1706
|
+
if (dialect === "sqlite") conditions.push(sql$1`LOWER(${column}) LIKE LOWER(${encodeValue(`%${escapedValue}%`)})`);
|
|
1707
|
+
else conditions.push(ilike(column, encodeValue(`%${escapedValue}%`)));
|
|
1631
1708
|
}
|
|
1632
|
-
if (
|
|
1633
|
-
|
|
1634
|
-
if (
|
|
1635
|
-
|
|
1636
|
-
return this.sqliteDateTime(key, {});
|
|
1709
|
+
if (operator?.startsWith != null) {
|
|
1710
|
+
const escapedValue = String(operator.startsWith).replace(/\\/g, "\\\\").replace(/%/g, "\\%").replace(/_/g, "\\_");
|
|
1711
|
+
if (dialect === "sqlite") conditions.push(sql$1`LOWER(${column}) LIKE LOWER(${encodeValue(`${escapedValue}%`)})`);
|
|
1712
|
+
else conditions.push(ilike(column, encodeValue(`${escapedValue}%`)));
|
|
1637
1713
|
}
|
|
1638
|
-
if (
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
dataType: () => "text",
|
|
1643
|
-
toDriver: (value) => JSON.stringify(value),
|
|
1644
|
-
fromDriver: (value) => {
|
|
1645
|
-
return value && typeof value === "string" ? JSON.parse(value) : value;
|
|
1714
|
+
if (operator?.endsWith != null) {
|
|
1715
|
+
const escapedValue = String(operator.endsWith).replace(/\\/g, "\\\\").replace(/%/g, "\\%").replace(/_/g, "\\_");
|
|
1716
|
+
if (dialect === "sqlite") conditions.push(sql$1`LOWER(${column}) LIKE LOWER(${encodeValue(`%${escapedValue}`)})`);
|
|
1717
|
+
else conditions.push(ilike(column, encodeValue(`%${escapedValue}`)));
|
|
1646
1718
|
}
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
toDriver: (value) => new Date(value).getTime(),
|
|
1651
|
-
fromDriver: (value) => {
|
|
1652
|
-
return new Date(value).toISOString();
|
|
1719
|
+
if (operator?.between != null) {
|
|
1720
|
+
if (!Array.isArray(operator.between) || operator.between.length !== 2) throw new Error("between operator requires exactly 2 values [min, max]");
|
|
1721
|
+
conditions.push(between(column, encodeValue(operator.between[0]), encodeValue(operator.between[1])));
|
|
1653
1722
|
}
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
toDriver: (value) => value ? 1 : 0,
|
|
1658
|
-
fromDriver: (value) => value === 1
|
|
1659
|
-
});
|
|
1660
|
-
sqliteDate = pg$2.customType({
|
|
1661
|
-
dataType: () => "integer",
|
|
1662
|
-
toDriver: (value) => new Date(value).getTime(),
|
|
1663
|
-
fromDriver: (value) => {
|
|
1664
|
-
return new Date(value).toISOString().split("T")[0];
|
|
1723
|
+
if (operator?.notBetween != null) {
|
|
1724
|
+
if (!Array.isArray(operator.notBetween) || operator.notBetween.length !== 2) throw new Error("notBetween operator requires exactly 2 values [min, max]");
|
|
1725
|
+
conditions.push(notBetween(column, encodeValue(operator.notBetween[0]), encodeValue(operator.notBetween[1])));
|
|
1665
1726
|
}
|
|
1666
|
-
|
|
1727
|
+
if (operator?.arrayContains != null) conditions.push(arrayContains(column, encodeValue(operator.arrayContains)));
|
|
1728
|
+
if (operator?.arrayContained != null) conditions.push(arrayContained(column, encodeValue(operator.arrayContained)));
|
|
1729
|
+
if (operator?.arrayOverlaps != null) conditions.push(arrayOverlaps(column, encodeValue(operator.arrayOverlaps)));
|
|
1730
|
+
if (conditions.length === 0) return;
|
|
1731
|
+
if (conditions.length === 1) return conditions[0];
|
|
1732
|
+
return and(...conditions);
|
|
1733
|
+
}
|
|
1734
|
+
/**
|
|
1735
|
+
* Parse pagination sort string to orderBy format.
|
|
1736
|
+
* Format: "firstName,-lastName" -> [{ column: "firstName", direction: "asc" }, { column: "lastName", direction: "desc" }]
|
|
1737
|
+
* - Columns separated by comma
|
|
1738
|
+
* - Prefix with '-' for DESC direction
|
|
1739
|
+
*
|
|
1740
|
+
* @param sort Pagination sort string
|
|
1741
|
+
* @returns OrderBy array or single object
|
|
1742
|
+
*/
|
|
1743
|
+
parsePaginationSort(sort) {
|
|
1744
|
+
const orderByClauses = sort.split(",").map((field) => field.trim()).map((field) => {
|
|
1745
|
+
if (field.startsWith("-")) return {
|
|
1746
|
+
column: field.substring(1),
|
|
1747
|
+
direction: "desc"
|
|
1748
|
+
};
|
|
1749
|
+
return {
|
|
1750
|
+
column: field,
|
|
1751
|
+
direction: "asc"
|
|
1752
|
+
};
|
|
1753
|
+
});
|
|
1754
|
+
return orderByClauses.length === 1 ? orderByClauses[0] : orderByClauses;
|
|
1755
|
+
}
|
|
1756
|
+
/**
|
|
1757
|
+
* Normalize orderBy parameter to array format.
|
|
1758
|
+
* Supports 3 modes:
|
|
1759
|
+
* 1. String: "name" -> [{ column: "name", direction: "asc" }]
|
|
1760
|
+
* 2. Object: { column: "name", direction: "desc" } -> [{ column: "name", direction: "desc" }]
|
|
1761
|
+
* 3. Array: [{ column: "name" }, { column: "age", direction: "desc" }] -> normalized array
|
|
1762
|
+
*
|
|
1763
|
+
* @param orderBy The orderBy parameter
|
|
1764
|
+
* @returns Normalized array of order by clauses
|
|
1765
|
+
*/
|
|
1766
|
+
normalizeOrderBy(orderBy) {
|
|
1767
|
+
if (typeof orderBy === "string") return [{
|
|
1768
|
+
column: orderBy,
|
|
1769
|
+
direction: "asc"
|
|
1770
|
+
}];
|
|
1771
|
+
if (!Array.isArray(orderBy) && typeof orderBy === "object") return [{
|
|
1772
|
+
column: orderBy.column,
|
|
1773
|
+
direction: orderBy.direction ?? "asc"
|
|
1774
|
+
}];
|
|
1775
|
+
if (Array.isArray(orderBy)) return orderBy.map((item) => ({
|
|
1776
|
+
column: item.column,
|
|
1777
|
+
direction: item.direction ?? "asc"
|
|
1778
|
+
}));
|
|
1779
|
+
return [];
|
|
1780
|
+
}
|
|
1781
|
+
/**
|
|
1782
|
+
* Create a pagination object.
|
|
1783
|
+
*
|
|
1784
|
+
* @deprecated Use `createPagination` from alepha instead.
|
|
1785
|
+
* This method now delegates to the framework-level helper.
|
|
1786
|
+
*
|
|
1787
|
+
* @param entities The entities to paginate.
|
|
1788
|
+
* @param limit The limit of the pagination.
|
|
1789
|
+
* @param offset The offset of the pagination.
|
|
1790
|
+
* @param sort Optional sort metadata to include in response.
|
|
1791
|
+
*/
|
|
1792
|
+
createPagination(entities, limit = 10, offset = 0, sort) {
|
|
1793
|
+
return createPagination(entities, limit, offset, sort);
|
|
1794
|
+
}
|
|
1667
1795
|
};
|
|
1668
1796
|
|
|
1669
1797
|
//#endregion
|
|
1670
|
-
//#region ../../src/orm/
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
* This provider requires a D1 binding to be set via `cloudflareD1Options` before starting.
|
|
1675
|
-
* The binding is typically obtained from the Cloudflare Workers environment.
|
|
1676
|
-
*
|
|
1677
|
-
* @example
|
|
1678
|
-
* ```ts
|
|
1679
|
-
* // In your Cloudflare Worker
|
|
1680
|
-
* alepha.set(cloudflareD1Options, { binding: env.DB });
|
|
1681
|
-
* ```
|
|
1682
|
-
*/
|
|
1683
|
-
var CloudflareD1Provider = class extends DatabaseProvider {
|
|
1684
|
-
kit = $inject(DrizzleKitProvider);
|
|
1798
|
+
//#region ../../src/orm/services/Repository.ts
|
|
1799
|
+
var Repository = class {
|
|
1800
|
+
entity;
|
|
1801
|
+
provider;
|
|
1685
1802
|
log = $logger();
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
|
|
1803
|
+
relationManager = $inject(PgRelationManager);
|
|
1804
|
+
queryManager = $inject(QueryManager);
|
|
1805
|
+
dateTimeProvider = $inject(DateTimeProvider);
|
|
1806
|
+
alepha = $inject(Alepha);
|
|
1807
|
+
constructor(entity, provider = DatabaseProvider) {
|
|
1808
|
+
this.entity = entity;
|
|
1809
|
+
this.provider = this.alepha.inject(provider);
|
|
1810
|
+
this.provider.registerEntity(entity);
|
|
1811
|
+
}
|
|
1812
|
+
/**
|
|
1813
|
+
* Represents the primary key of the table.
|
|
1814
|
+
* - Key is the name of the primary key column.
|
|
1815
|
+
* - Type is the type (TypeBox) of the primary key column.
|
|
1816
|
+
*
|
|
1817
|
+
* ID is mandatory. If the table does not have a primary key, it will throw an error.
|
|
1818
|
+
*/
|
|
1819
|
+
get id() {
|
|
1820
|
+
return this.getPrimaryKey(this.entity.schema);
|
|
1692
1821
|
}
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
|
|
1822
|
+
/**
|
|
1823
|
+
* Get Drizzle table object.
|
|
1824
|
+
*/
|
|
1825
|
+
get table() {
|
|
1826
|
+
return this.provider.table(this.entity);
|
|
1827
|
+
}
|
|
1828
|
+
/**
|
|
1829
|
+
* Get SQL table name. (from Drizzle table object)
|
|
1830
|
+
*/
|
|
1831
|
+
get tableName() {
|
|
1832
|
+
return this.entity.name;
|
|
1696
1833
|
}
|
|
1834
|
+
/**
|
|
1835
|
+
* Getter for the database connection from the database provider.
|
|
1836
|
+
*/
|
|
1697
1837
|
get db() {
|
|
1698
|
-
|
|
1699
|
-
return this.drizzleDb;
|
|
1838
|
+
return this.provider.db;
|
|
1700
1839
|
}
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
|
|
1840
|
+
/**
|
|
1841
|
+
* Execute a SQL query.
|
|
1842
|
+
*
|
|
1843
|
+
* This method allows executing raw SQL queries against the database.
|
|
1844
|
+
* This is by far the easiest way to run custom queries that are not covered by the repository's built-in methods!
|
|
1845
|
+
*
|
|
1846
|
+
* You must use the `sql` tagged template function from Drizzle ORM to create the query. https://orm.drizzle.team/docs/sql
|
|
1847
|
+
*
|
|
1848
|
+
* @example
|
|
1849
|
+
* ```ts
|
|
1850
|
+
* class App {
|
|
1851
|
+
* repository = $repository({ ... });
|
|
1852
|
+
* async getAdults() {
|
|
1853
|
+
* const users = repository.table; // Drizzle table object
|
|
1854
|
+
* await repository.query(sql`SELECT * FROM ${users} WHERE ${users.age} > ${18}`);
|
|
1855
|
+
* // or better
|
|
1856
|
+
* await repository.query((users) => sql`SELECT * FROM ${users} WHERE ${users.age} > ${18}`);
|
|
1857
|
+
* }
|
|
1858
|
+
* }
|
|
1859
|
+
* ```
|
|
1860
|
+
*/
|
|
1861
|
+
async query(query, schema$1) {
|
|
1862
|
+
const raw = typeof query === "function" ? query(this.table, this.db) : query;
|
|
1863
|
+
if (typeof raw === "string" && raw.includes("[object Object]")) throw new AlephaError("Invalid SQL query. Did you forget to call the 'sql' function?");
|
|
1864
|
+
return (await this.provider.execute(raw)).map((it) => {
|
|
1865
|
+
return this.clean(this.mapRawFieldsToEntity(it), schema$1 ?? this.entity.schema);
|
|
1866
|
+
});
|
|
1704
1867
|
}
|
|
1705
|
-
|
|
1706
|
-
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
|
|
1716
|
-
await this.migrate();
|
|
1717
|
-
this.log.info("Using Cloudflare D1 database");
|
|
1868
|
+
/**
|
|
1869
|
+
* Map raw database fields to entity fields. (handles column name differences)
|
|
1870
|
+
*/
|
|
1871
|
+
mapRawFieldsToEntity(row) {
|
|
1872
|
+
const entity = {};
|
|
1873
|
+
for (const key of Object.keys(row)) {
|
|
1874
|
+
entity[key] = row[key];
|
|
1875
|
+
for (const colKey of Object.keys(this.table)) if (this.table[colKey].name === key) {
|
|
1876
|
+
entity[colKey] = row[key];
|
|
1877
|
+
break;
|
|
1878
|
+
}
|
|
1718
1879
|
}
|
|
1719
|
-
|
|
1720
|
-
async executeMigrations(migrationsFolder) {
|
|
1721
|
-
const { migrate: migrate$3 } = await import("drizzle-orm/d1/migrator");
|
|
1722
|
-
await migrate$3(this.db, { migrationsFolder });
|
|
1880
|
+
return entity;
|
|
1723
1881
|
}
|
|
1724
1882
|
/**
|
|
1725
|
-
*
|
|
1726
|
-
* D1 requires proper migrations to be applied.
|
|
1883
|
+
* Get a Drizzle column from the table by his name.
|
|
1727
1884
|
*/
|
|
1728
|
-
|
|
1729
|
-
|
|
1885
|
+
col(name) {
|
|
1886
|
+
const column = this.table[name];
|
|
1887
|
+
if (!column) throw new AlephaError(`Invalid access. Column ${String(name)} not found in table ${this.tableName}`);
|
|
1888
|
+
return column;
|
|
1730
1889
|
}
|
|
1731
1890
|
/**
|
|
1732
|
-
*
|
|
1733
|
-
* D1 doesn't support schema synchronization.
|
|
1891
|
+
* Run a transaction.
|
|
1734
1892
|
*/
|
|
1735
|
-
async
|
|
1736
|
-
|
|
1893
|
+
async transaction(transaction, config) {
|
|
1894
|
+
if (this.provider.driver === "pglite") {
|
|
1895
|
+
this.log.warn("Transactions are not supported with pglite driver");
|
|
1896
|
+
return await transaction(null);
|
|
1897
|
+
}
|
|
1898
|
+
this.log.debug(`Starting transaction on table ${this.tableName}`);
|
|
1899
|
+
return await this.db.transaction(transaction, config);
|
|
1900
|
+
}
|
|
1901
|
+
/**
|
|
1902
|
+
* Start a SELECT query on the table.
|
|
1903
|
+
*/
|
|
1904
|
+
rawSelect(opts = {}) {
|
|
1905
|
+
return (opts.tx ?? this.db).select().from(this.table);
|
|
1906
|
+
}
|
|
1907
|
+
/**
|
|
1908
|
+
* Start a SELECT DISTINCT query on the table.
|
|
1909
|
+
*/
|
|
1910
|
+
rawSelectDistinct(opts = {}, columns = []) {
|
|
1911
|
+
const db$1 = opts.tx ?? this.db;
|
|
1912
|
+
const table = this.table;
|
|
1913
|
+
const fields = {};
|
|
1914
|
+
for (const column of columns) if (typeof column === "string") fields[column] = this.col(column);
|
|
1915
|
+
return db$1.selectDistinct(fields).from(table);
|
|
1916
|
+
}
|
|
1917
|
+
/**
|
|
1918
|
+
* Start an INSERT query on the table.
|
|
1919
|
+
*/
|
|
1920
|
+
rawInsert(opts = {}) {
|
|
1921
|
+
return (opts.tx ?? this.db).insert(this.table);
|
|
1922
|
+
}
|
|
1923
|
+
/**
|
|
1924
|
+
* Start an UPDATE query on the table.
|
|
1925
|
+
*/
|
|
1926
|
+
rawUpdate(opts = {}) {
|
|
1927
|
+
return (opts.tx ?? this.db).update(this.table);
|
|
1928
|
+
}
|
|
1929
|
+
/**
|
|
1930
|
+
* Start a DELETE query on the table.
|
|
1931
|
+
*/
|
|
1932
|
+
rawDelete(opts = {}) {
|
|
1933
|
+
return (opts.tx ?? this.db).delete(this.table);
|
|
1934
|
+
}
|
|
1935
|
+
/**
|
|
1936
|
+
* Create a Drizzle `select` query based on a JSON query object.
|
|
1937
|
+
*
|
|
1938
|
+
* > This method is the base for `find`, `findOne`, `findById`, and `paginate`.
|
|
1939
|
+
*/
|
|
1940
|
+
async findMany(query = {}, opts = {}) {
|
|
1941
|
+
await this.alepha.events.emit("repository:read:before", {
|
|
1942
|
+
tableName: this.tableName,
|
|
1943
|
+
query
|
|
1944
|
+
});
|
|
1945
|
+
const columns = query.columns ?? query.distinct;
|
|
1946
|
+
const builder = query.distinct ? this.rawSelectDistinct(opts, query.distinct) : this.rawSelect(opts);
|
|
1947
|
+
const joins = [];
|
|
1948
|
+
if (query.with) this.relationManager.buildJoins(this.provider, builder, joins, query.with, this.table);
|
|
1949
|
+
const where = this.withDeletedAt(query.where ?? {}, opts);
|
|
1950
|
+
builder.where(() => this.toSQL(where, joins));
|
|
1951
|
+
if (query.offset) {
|
|
1952
|
+
builder.offset(query.offset);
|
|
1953
|
+
if (this.provider.dialect === "sqlite" && !query.limit) query.limit = 1e3;
|
|
1954
|
+
}
|
|
1955
|
+
if (query.limit) builder.limit(query.limit);
|
|
1956
|
+
if (query.orderBy) {
|
|
1957
|
+
const orderByClauses = this.queryManager.normalizeOrderBy(query.orderBy);
|
|
1958
|
+
builder.orderBy(...orderByClauses.map((clause) => clause.direction === "desc" ? desc(this.col(clause.column)) : asc(this.col(clause.column))));
|
|
1959
|
+
}
|
|
1960
|
+
if (query.groupBy) builder.groupBy(...query.groupBy.map((key) => this.col(key)));
|
|
1961
|
+
if (opts.for) {
|
|
1962
|
+
if (typeof opts.for === "string") builder.for(opts.for);
|
|
1963
|
+
else if (opts.for) builder.for(opts.for.strength, opts.for.config);
|
|
1964
|
+
}
|
|
1737
1965
|
try {
|
|
1738
|
-
await
|
|
1739
|
-
|
|
1740
|
-
|
|
1966
|
+
let rows = await builder.execute();
|
|
1967
|
+
let schema$1 = this.entity.schema;
|
|
1968
|
+
if (columns) schema$1 = t.pick(schema$1, columns);
|
|
1969
|
+
if (joins.length) rows = rows.map((row) => {
|
|
1970
|
+
const rowSchema = {
|
|
1971
|
+
...schema$1,
|
|
1972
|
+
properties: { ...schema$1.properties }
|
|
1973
|
+
};
|
|
1974
|
+
return this.relationManager.mapRowWithJoins(row[this.tableName], row, rowSchema, joins);
|
|
1975
|
+
});
|
|
1976
|
+
rows = rows.map((row) => {
|
|
1977
|
+
if (joins.length) {
|
|
1978
|
+
const joinedSchema = this.relationManager.buildSchemaWithJoins(schema$1, joins);
|
|
1979
|
+
return this.cleanWithJoins(row, joinedSchema, joins);
|
|
1980
|
+
}
|
|
1981
|
+
return this.clean(row, schema$1);
|
|
1982
|
+
});
|
|
1983
|
+
await this.alepha.events.emit("repository:read:after", {
|
|
1984
|
+
tableName: this.tableName,
|
|
1985
|
+
query,
|
|
1986
|
+
entities: rows
|
|
1987
|
+
});
|
|
1988
|
+
return rows;
|
|
1989
|
+
} catch (error) {
|
|
1990
|
+
throw new DbError("Query select has failed", error);
|
|
1741
1991
|
}
|
|
1742
1992
|
}
|
|
1743
|
-
|
|
1744
|
-
|
|
1745
|
-
|
|
1746
|
-
|
|
1747
|
-
|
|
1748
|
-
|
|
1749
|
-
|
|
1750
|
-
|
|
1993
|
+
/**
|
|
1994
|
+
* Find a single entity.
|
|
1995
|
+
*/
|
|
1996
|
+
async findOne(query, opts = {}) {
|
|
1997
|
+
const [entity] = await this.findMany({
|
|
1998
|
+
limit: 1,
|
|
1999
|
+
...query
|
|
2000
|
+
}, opts);
|
|
2001
|
+
if (!entity) throw new DbEntityNotFoundError(this.tableName);
|
|
2002
|
+
return entity;
|
|
1751
2003
|
}
|
|
1752
|
-
|
|
1753
|
-
|
|
1754
|
-
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
*
|
|
1758
|
-
*/
|
|
1759
|
-
|
|
1760
|
-
|
|
1761
|
-
|
|
1762
|
-
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
|
|
1766
|
-
|
|
1767
|
-
|
|
1768
|
-
|
|
1769
|
-
table: pgTable
|
|
2004
|
+
/**
|
|
2005
|
+
* Find entities with pagination.
|
|
2006
|
+
*
|
|
2007
|
+
* It uses the same parameters as `find()`, but adds pagination metadata to the response.
|
|
2008
|
+
*
|
|
2009
|
+
* > Pagination CAN also do a count query to get the total number of elements.
|
|
2010
|
+
*/
|
|
2011
|
+
async paginate(pagination = {}, query = {}, opts = {}) {
|
|
2012
|
+
const limit = query.limit ?? pagination.size ?? 10;
|
|
2013
|
+
const page = pagination.page ?? 0;
|
|
2014
|
+
const offset = query.offset ?? page * limit;
|
|
2015
|
+
let orderBy = query.orderBy;
|
|
2016
|
+
if (!query.orderBy && pagination.sort) orderBy = this.queryManager.parsePaginationSort(pagination.sort);
|
|
2017
|
+
const now = Date.now();
|
|
2018
|
+
const timers = {
|
|
2019
|
+
query: now,
|
|
2020
|
+
count: now
|
|
1770
2021
|
};
|
|
1771
|
-
|
|
1772
|
-
|
|
2022
|
+
const tasks = [];
|
|
2023
|
+
tasks.push(this.findMany({
|
|
2024
|
+
offset,
|
|
2025
|
+
limit: limit + 1,
|
|
2026
|
+
orderBy,
|
|
2027
|
+
...query
|
|
2028
|
+
}, opts).then((it) => {
|
|
2029
|
+
timers.query = Date.now() - timers.query;
|
|
2030
|
+
return it;
|
|
2031
|
+
}));
|
|
2032
|
+
if (opts.count) {
|
|
2033
|
+
const where = isSQLWrapper(query.where) ? query.where : query.where ? this.toSQL(query.where) : void 0;
|
|
2034
|
+
tasks.push(this.db.$count(this.table, where).then((it) => {
|
|
2035
|
+
timers.count = Date.now() - timers.count;
|
|
2036
|
+
return it;
|
|
2037
|
+
}));
|
|
2038
|
+
}
|
|
2039
|
+
const [entities, countResult] = await Promise.all(tasks);
|
|
2040
|
+
let sortMetadata;
|
|
2041
|
+
if (orderBy) sortMetadata = this.queryManager.normalizeOrderBy(orderBy);
|
|
2042
|
+
const response = this.queryManager.createPagination(entities, limit, offset, sortMetadata);
|
|
2043
|
+
response.page.totalElements = countResult;
|
|
2044
|
+
if (countResult != null) response.page.totalPages = Math.ceil(countResult / limit);
|
|
2045
|
+
return response;
|
|
1773
2046
|
}
|
|
1774
|
-
|
|
1775
|
-
|
|
1776
|
-
|
|
1777
|
-
|
|
1778
|
-
|
|
1779
|
-
|
|
1780
|
-
|
|
1781
|
-
|
|
2047
|
+
/**
|
|
2048
|
+
* Find an entity by ID.
|
|
2049
|
+
*
|
|
2050
|
+
* This is a convenience method for `findOne` with a where clause on the primary key.
|
|
2051
|
+
* If you need more complex queries, use `findOne` instead.
|
|
2052
|
+
*/
|
|
2053
|
+
async findById(id, opts = {}) {
|
|
2054
|
+
return await this.findOne({ where: this.getWhereId(id) }, opts);
|
|
1782
2055
|
}
|
|
1783
|
-
|
|
1784
|
-
|
|
1785
|
-
|
|
1786
|
-
|
|
1787
|
-
|
|
2056
|
+
/**
|
|
2057
|
+
* Helper to create a type-safe query object.
|
|
2058
|
+
*/
|
|
2059
|
+
createQuery() {
|
|
2060
|
+
return {};
|
|
1788
2061
|
}
|
|
1789
2062
|
/**
|
|
1790
|
-
*
|
|
2063
|
+
* Helper to create a type-safe where clause.
|
|
1791
2064
|
*/
|
|
1792
|
-
|
|
1793
|
-
|
|
1794
|
-
index,
|
|
1795
|
-
uniqueIndex,
|
|
1796
|
-
unique,
|
|
1797
|
-
check,
|
|
1798
|
-
foreignKey
|
|
1799
|
-
};
|
|
1800
|
-
const tableResolver = (entityName) => {
|
|
1801
|
-
return tables.get(entityName);
|
|
1802
|
-
};
|
|
1803
|
-
return this.buildTableConfig(entity, pgBuilders, tableResolver);
|
|
2065
|
+
createQueryWhere() {
|
|
2066
|
+
return {};
|
|
1804
2067
|
}
|
|
1805
|
-
|
|
1806
|
-
|
|
1807
|
-
|
|
1808
|
-
|
|
1809
|
-
|
|
1810
|
-
|
|
1811
|
-
|
|
1812
|
-
|
|
1813
|
-
|
|
1814
|
-
|
|
1815
|
-
|
|
1816
|
-
|
|
1817
|
-
|
|
1818
|
-
|
|
1819
|
-
|
|
1820
|
-
|
|
1821
|
-
|
|
1822
|
-
|
|
1823
|
-
|
|
1824
|
-
|
|
1825
|
-
|
|
1826
|
-
|
|
1827
|
-
};
|
|
1828
|
-
mapFieldToColumn = (tableName, fieldName, value, nsp, enums) => {
|
|
1829
|
-
const key = this.toColumnName(fieldName);
|
|
1830
|
-
if ("anyOf" in value && Array.isArray(value.anyOf) && value.anyOf.length === 2 && value.anyOf.some((it) => t.schema.isNull(it))) value = value.anyOf.find((it) => !t.schema.isNull(it));
|
|
1831
|
-
if (t.schema.isInteger(value)) {
|
|
1832
|
-
if (PG_SERIAL in value) return pg$1.serial(key);
|
|
1833
|
-
if (PG_IDENTITY in value) {
|
|
1834
|
-
const options = value[PG_IDENTITY];
|
|
1835
|
-
if (options.mode === "byDefault") return pg$1.integer().generatedByDefaultAsIdentity(options);
|
|
1836
|
-
return pg$1.integer().generatedAlwaysAsIdentity(options);
|
|
1837
|
-
}
|
|
1838
|
-
return pg$1.integer(key);
|
|
1839
|
-
}
|
|
1840
|
-
if (t.schema.isBigInt(value)) {
|
|
1841
|
-
if (PG_IDENTITY in value) {
|
|
1842
|
-
const options = value[PG_IDENTITY];
|
|
1843
|
-
if (options.mode === "byDefault") return pg$1.bigint({ mode: "bigint" }).generatedByDefaultAsIdentity(options);
|
|
1844
|
-
return pg$1.bigint({ mode: "bigint" }).generatedAlwaysAsIdentity(options);
|
|
1845
|
-
}
|
|
2068
|
+
/**
|
|
2069
|
+
* Create an entity.
|
|
2070
|
+
*
|
|
2071
|
+
* @param data The entity to create.
|
|
2072
|
+
* @param opts The options for creating the entity.
|
|
2073
|
+
* @returns The ID of the created entity.
|
|
2074
|
+
*/
|
|
2075
|
+
async create(data, opts = {}) {
|
|
2076
|
+
await this.alepha.events.emit("repository:create:before", {
|
|
2077
|
+
tableName: this.tableName,
|
|
2078
|
+
data
|
|
2079
|
+
});
|
|
2080
|
+
try {
|
|
2081
|
+
const entity = await this.rawInsert(opts).values(this.cast(data ?? {}, true)).returning(this.table).then(([it]) => this.clean(it, this.entity.schema));
|
|
2082
|
+
await this.alepha.events.emit("repository:create:after", {
|
|
2083
|
+
tableName: this.tableName,
|
|
2084
|
+
data,
|
|
2085
|
+
entity
|
|
2086
|
+
});
|
|
2087
|
+
return entity;
|
|
2088
|
+
} catch (error) {
|
|
2089
|
+
throw this.handleError(error, "Insert query has failed");
|
|
1846
2090
|
}
|
|
1847
|
-
|
|
1848
|
-
|
|
1849
|
-
|
|
1850
|
-
|
|
1851
|
-
|
|
2091
|
+
}
|
|
2092
|
+
/**
|
|
2093
|
+
* Create many entities.
|
|
2094
|
+
*
|
|
2095
|
+
* Inserts are batched in chunks of 1000 to avoid hitting database limits.
|
|
2096
|
+
*
|
|
2097
|
+
* @param values The entities to create.
|
|
2098
|
+
* @param opts The statement options.
|
|
2099
|
+
* @returns The created entities.
|
|
2100
|
+
*/
|
|
2101
|
+
async createMany(values, opts = {}) {
|
|
2102
|
+
if (values.length === 0) return [];
|
|
2103
|
+
await this.alepha.events.emit("repository:create:before", {
|
|
2104
|
+
tableName: this.tableName,
|
|
2105
|
+
data: values
|
|
2106
|
+
});
|
|
2107
|
+
const batchSize = opts.batchSize ?? 1e3;
|
|
2108
|
+
const allEntities = [];
|
|
2109
|
+
try {
|
|
2110
|
+
for (let i = 0; i < values.length; i += batchSize) {
|
|
2111
|
+
const batch = values.slice(i, i + batchSize);
|
|
2112
|
+
const entities = await this.rawInsert(opts).values(batch.map((data) => this.cast(data, true))).returning(this.table).then((rows) => rows.map((it) => this.clean(it, this.entity.schema)));
|
|
2113
|
+
allEntities.push(...entities);
|
|
1852
2114
|
}
|
|
1853
|
-
|
|
1854
|
-
|
|
1855
|
-
|
|
1856
|
-
|
|
1857
|
-
|
|
1858
|
-
|
|
1859
|
-
|
|
1860
|
-
|
|
1861
|
-
if (t.schema.isArray(value)) {
|
|
1862
|
-
if (t.schema.isObject(value.items)) return schema(key, value);
|
|
1863
|
-
if (t.schema.isRecord(value.items)) return schema(key, value);
|
|
1864
|
-
if (t.schema.isString(value.items)) return pg$1.text(key).array();
|
|
1865
|
-
if (t.schema.isInteger(value.items)) return pg$1.integer(key).array();
|
|
1866
|
-
if (t.schema.isNumber(value.items)) return pg$1.numeric(key).array();
|
|
1867
|
-
if (t.schema.isBoolean(value.items)) return pg$1.boolean(key).array();
|
|
1868
|
-
if (isTypeEnum(value.items)) return pg$1.text(key).array();
|
|
2115
|
+
await this.alepha.events.emit("repository:create:after", {
|
|
2116
|
+
tableName: this.tableName,
|
|
2117
|
+
data: values,
|
|
2118
|
+
entity: allEntities
|
|
2119
|
+
});
|
|
2120
|
+
return allEntities;
|
|
2121
|
+
} catch (error) {
|
|
2122
|
+
throw this.handleError(error, "Insert query has failed");
|
|
1869
2123
|
}
|
|
1870
|
-
|
|
1871
|
-
|
|
1872
|
-
|
|
1873
|
-
|
|
1874
|
-
|
|
1875
|
-
|
|
1876
|
-
|
|
1877
|
-
|
|
1878
|
-
|
|
1879
|
-
|
|
1880
|
-
|
|
1881
|
-
|
|
1882
|
-
|
|
2124
|
+
}
|
|
2125
|
+
/**
|
|
2126
|
+
* Find an entity and update it.
|
|
2127
|
+
*/
|
|
2128
|
+
async updateOne(where, data, opts = {}) {
|
|
2129
|
+
await this.alepha.events.emit("repository:update:before", {
|
|
2130
|
+
tableName: this.tableName,
|
|
2131
|
+
where,
|
|
2132
|
+
data
|
|
2133
|
+
});
|
|
2134
|
+
let row = data;
|
|
2135
|
+
const updatedAtField = getAttrFields(this.entity.schema, PG_UPDATED_AT)?.[0];
|
|
2136
|
+
if (updatedAtField) row[updatedAtField.key] = this.dateTimeProvider.of(opts.now).toISOString();
|
|
2137
|
+
where = this.withDeletedAt(where, opts);
|
|
2138
|
+
row = this.cast(row, false);
|
|
2139
|
+
delete row[this.id.key];
|
|
2140
|
+
const response = await this.rawUpdate(opts).set(row).where(this.toSQL(where)).returning(this.table).catch((error) => {
|
|
2141
|
+
throw this.handleError(error, "Update query has failed");
|
|
2142
|
+
});
|
|
2143
|
+
if (!response[0]) throw new DbEntityNotFoundError(this.tableName);
|
|
2144
|
+
try {
|
|
2145
|
+
const entity = this.clean(response[0], this.entity.schema);
|
|
2146
|
+
await this.alepha.events.emit("repository:update:after", {
|
|
2147
|
+
tableName: this.tableName,
|
|
2148
|
+
where,
|
|
2149
|
+
data,
|
|
2150
|
+
entities: [entity]
|
|
2151
|
+
});
|
|
2152
|
+
return entity;
|
|
2153
|
+
} catch (error) {
|
|
2154
|
+
throw this.handleError(error, "Update query has failed");
|
|
1883
2155
|
}
|
|
1884
|
-
|
|
1885
|
-
};
|
|
2156
|
+
}
|
|
1886
2157
|
/**
|
|
1887
|
-
*
|
|
2158
|
+
* Save a given entity.
|
|
1888
2159
|
*
|
|
1889
|
-
* @
|
|
1890
|
-
*
|
|
2160
|
+
* @example
|
|
2161
|
+
* ```ts
|
|
2162
|
+
* const entity = await repository.findById(1);
|
|
2163
|
+
* entity.name = "New Name"; // update a field
|
|
2164
|
+
* delete entity.description; // delete a field
|
|
2165
|
+
* await repository.save(entity);
|
|
2166
|
+
* ```
|
|
2167
|
+
*
|
|
2168
|
+
* Difference with `updateById/updateOne`:
|
|
2169
|
+
*
|
|
2170
|
+
* - requires the entity to be fetched first (whole object is expected)
|
|
2171
|
+
* - check pg.version() if present -> optimistic locking
|
|
2172
|
+
* - validate entity against schema
|
|
2173
|
+
* - undefined values will be set to null, not ignored!
|
|
2174
|
+
*
|
|
2175
|
+
* @see {@link DbVersionMismatchError}
|
|
1891
2176
|
*/
|
|
1892
|
-
|
|
1893
|
-
|
|
1894
|
-
|
|
1895
|
-
|
|
1896
|
-
|
|
1897
|
-
|
|
1898
|
-
|
|
1899
|
-
|
|
1900
|
-
|
|
1901
|
-
|
|
1902
|
-
|
|
1903
|
-
|
|
1904
|
-
|
|
1905
|
-
|
|
1906
|
-
|
|
1907
|
-
|
|
1908
|
-
|
|
1909
|
-
|
|
1910
|
-
|
|
1911
|
-
|
|
2177
|
+
async save(entity, opts = {}) {
|
|
2178
|
+
const row = entity;
|
|
2179
|
+
const id = row[this.id.key];
|
|
2180
|
+
if (id == null) throw new AlephaError("Cannot save entity without ID - missing primary key in value");
|
|
2181
|
+
for (const key of Object.keys(this.entity.schema.properties)) if (row[key] === void 0) row[key] = null;
|
|
2182
|
+
let where = this.createQueryWhere();
|
|
2183
|
+
where.id = { eq: id };
|
|
2184
|
+
const versionField = getAttrFields(this.entity.schema, PG_VERSION)?.[0];
|
|
2185
|
+
if (versionField && typeof row[versionField.key] === "number") {
|
|
2186
|
+
where = { and: [where, { [versionField.key]: { eq: row[versionField.key] } }] };
|
|
2187
|
+
row[versionField.key] += 1;
|
|
2188
|
+
}
|
|
2189
|
+
try {
|
|
2190
|
+
const newValue = await this.updateOne(where, row, opts);
|
|
2191
|
+
for (const key of Object.keys(this.entity.schema.properties)) row[key] = void 0;
|
|
2192
|
+
Object.assign(row, newValue);
|
|
2193
|
+
} catch (error) {
|
|
2194
|
+
if (error instanceof DbEntityNotFoundError && versionField) try {
|
|
2195
|
+
await this.findById(id);
|
|
2196
|
+
throw new DbVersionMismatchError(this.tableName, id);
|
|
2197
|
+
} catch (lookupError) {
|
|
2198
|
+
if (lookupError instanceof DbEntityNotFoundError) throw error;
|
|
2199
|
+
if (lookupError instanceof DbVersionMismatchError) throw lookupError;
|
|
2200
|
+
throw lookupError;
|
|
1912
2201
|
}
|
|
1913
|
-
|
|
2202
|
+
throw error;
|
|
1914
2203
|
}
|
|
1915
|
-
return pg$1.text(key);
|
|
1916
|
-
};
|
|
1917
|
-
};
|
|
1918
|
-
|
|
1919
|
-
//#endregion
|
|
1920
|
-
//#region ../../src/orm/providers/drivers/NodePostgresProvider.ts
|
|
1921
|
-
const envSchema$2 = t.object({
|
|
1922
|
-
DATABASE_URL: t.optional(t.text()),
|
|
1923
|
-
POSTGRES_SCHEMA: t.optional(t.text())
|
|
1924
|
-
});
|
|
1925
|
-
var NodePostgresProvider = class NodePostgresProvider extends DatabaseProvider {
|
|
1926
|
-
static SSL_MODES = [
|
|
1927
|
-
"require",
|
|
1928
|
-
"allow",
|
|
1929
|
-
"prefer",
|
|
1930
|
-
"verify-full"
|
|
1931
|
-
];
|
|
1932
|
-
log = $logger();
|
|
1933
|
-
env = $env(envSchema$2);
|
|
1934
|
-
kit = $inject(DrizzleKitProvider);
|
|
1935
|
-
builder = $inject(PostgresModelBuilder);
|
|
1936
|
-
client;
|
|
1937
|
-
pg;
|
|
1938
|
-
dialect = "postgresql";
|
|
1939
|
-
get name() {
|
|
1940
|
-
return "postgres";
|
|
1941
2204
|
}
|
|
1942
2205
|
/**
|
|
1943
|
-
*
|
|
2206
|
+
* Find an entity by ID and update it.
|
|
1944
2207
|
*/
|
|
1945
|
-
|
|
1946
|
-
|
|
1947
|
-
if (!this.env.DATABASE_URL) throw new AlephaError("DATABASE_URL is not defined in the environment");
|
|
1948
|
-
return this.env.DATABASE_URL;
|
|
2208
|
+
async updateById(id, data, opts = {}) {
|
|
2209
|
+
return await this.updateOne(this.getWhereId(id), data, opts);
|
|
1949
2210
|
}
|
|
1950
2211
|
/**
|
|
1951
|
-
*
|
|
2212
|
+
* Find many entities and update all of them.
|
|
1952
2213
|
*/
|
|
1953
|
-
|
|
2214
|
+
async updateMany(where, data, opts = {}) {
|
|
2215
|
+
await this.alepha.events.emit("repository:update:before", {
|
|
2216
|
+
tableName: this.tableName,
|
|
2217
|
+
where,
|
|
2218
|
+
data
|
|
2219
|
+
});
|
|
2220
|
+
const updatedAtField = getAttrFields(this.entity.schema, PG_UPDATED_AT)?.[0];
|
|
2221
|
+
if (updatedAtField) data[updatedAtField.key] = this.dateTimeProvider.of(opts.now).toISOString();
|
|
2222
|
+
where = this.withDeletedAt(where, opts);
|
|
2223
|
+
data = this.cast(data, false);
|
|
1954
2224
|
try {
|
|
1955
|
-
|
|
2225
|
+
const entities = await this.rawUpdate(opts).set(data).where(this.toSQL(where)).returning();
|
|
2226
|
+
await this.alepha.events.emit("repository:update:after", {
|
|
2227
|
+
tableName: this.tableName,
|
|
2228
|
+
where,
|
|
2229
|
+
data,
|
|
2230
|
+
entities
|
|
2231
|
+
});
|
|
2232
|
+
return entities.map((it) => it[this.id.key]);
|
|
1956
2233
|
} catch (error) {
|
|
1957
|
-
throw
|
|
2234
|
+
throw this.handleError(error, "Update query has failed");
|
|
1958
2235
|
}
|
|
1959
2236
|
}
|
|
1960
2237
|
/**
|
|
1961
|
-
*
|
|
2238
|
+
* Find many and delete all of them.
|
|
2239
|
+
* @returns Array of deleted entity IDs
|
|
1962
2240
|
*/
|
|
1963
|
-
|
|
1964
|
-
|
|
1965
|
-
if (
|
|
1966
|
-
|
|
2241
|
+
async deleteMany(where = {}, opts = {}) {
|
|
2242
|
+
const deletedAt = this.deletedAt();
|
|
2243
|
+
if (deletedAt && !opts.force) return await this.updateMany(where, { [deletedAt.key]: opts.now ?? this.dateTimeProvider.nowISOString() }, opts);
|
|
2244
|
+
await this.alepha.events.emit("repository:delete:before", {
|
|
2245
|
+
tableName: this.tableName,
|
|
2246
|
+
where
|
|
2247
|
+
});
|
|
2248
|
+
try {
|
|
2249
|
+
const ids = (await this.rawDelete(opts).where(this.toSQL(where)).returning({ id: this.table[this.id.key] })).map((row) => row.id);
|
|
2250
|
+
await this.alepha.events.emit("repository:delete:after", {
|
|
2251
|
+
tableName: this.tableName,
|
|
2252
|
+
where,
|
|
2253
|
+
ids
|
|
2254
|
+
});
|
|
2255
|
+
return ids;
|
|
2256
|
+
} catch (error) {
|
|
2257
|
+
throw new DbError("Delete query has failed", error);
|
|
2258
|
+
}
|
|
1967
2259
|
}
|
|
1968
2260
|
/**
|
|
1969
|
-
*
|
|
2261
|
+
* Delete all entities.
|
|
2262
|
+
* @returns Array of deleted entity IDs
|
|
1970
2263
|
*/
|
|
1971
|
-
|
|
1972
|
-
|
|
1973
|
-
return this.pg;
|
|
1974
|
-
}
|
|
1975
|
-
async executeMigrations(migrationsFolder) {
|
|
1976
|
-
await migrate(this.db, { migrationsFolder });
|
|
2264
|
+
clear(opts = {}) {
|
|
2265
|
+
return this.deleteMany({}, opts);
|
|
1977
2266
|
}
|
|
1978
|
-
|
|
1979
|
-
|
|
1980
|
-
|
|
1981
|
-
|
|
1982
|
-
|
|
1983
|
-
|
|
1984
|
-
|
|
1985
|
-
|
|
1986
|
-
|
|
1987
|
-
|
|
1988
|
-
|
|
1989
|
-
|
|
1990
|
-
|
|
1991
|
-
handler: async () => {
|
|
1992
|
-
if (this.alepha.isTest() && this.schemaForTesting && this.schemaForTesting.startsWith("test_")) {
|
|
1993
|
-
if (!/^test_[a-z0-9_]+$/i.test(this.schemaForTesting)) throw new AlephaError(`Invalid test schema name: ${this.schemaForTesting}. Must match pattern: test_[a-z0-9_]+`);
|
|
1994
|
-
this.log.warn(`Deleting test schema '${this.schemaForTesting}' ...`);
|
|
1995
|
-
await this.execute(sql$1`DROP SCHEMA IF EXISTS ${sql$1.raw(this.schemaForTesting)} CASCADE`);
|
|
1996
|
-
this.log.info(`Test schema '${this.schemaForTesting}' deleted`);
|
|
1997
|
-
}
|
|
1998
|
-
await this.close();
|
|
2267
|
+
/**
|
|
2268
|
+
* Delete the given entity.
|
|
2269
|
+
*
|
|
2270
|
+
* You must fetch the entity first in order to delete it.
|
|
2271
|
+
* @returns Array containing the deleted entity ID
|
|
2272
|
+
*/
|
|
2273
|
+
async destroy(entity, opts = {}) {
|
|
2274
|
+
const id = entity[this.id.key];
|
|
2275
|
+
if (id == null) throw new AlephaError("Cannot destroy entity without ID");
|
|
2276
|
+
const deletedAt = this.deletedAt();
|
|
2277
|
+
if (deletedAt && !opts.force) {
|
|
2278
|
+
opts.now ??= this.dateTimeProvider.nowISOString();
|
|
2279
|
+
entity[deletedAt.key] = opts.now;
|
|
1999
2280
|
}
|
|
2000
|
-
|
|
2001
|
-
async connect() {
|
|
2002
|
-
this.log.debug("Connect ..");
|
|
2003
|
-
const client = postgres(this.getClientOptions());
|
|
2004
|
-
await client`SELECT 1`;
|
|
2005
|
-
this.client = client;
|
|
2006
|
-
this.pg = drizzle$1(client, { logger: { logQuery: (query, params) => {
|
|
2007
|
-
this.log.trace(query, { params });
|
|
2008
|
-
} } });
|
|
2009
|
-
this.log.info("Connection OK");
|
|
2281
|
+
return await this.deleteById(id, opts);
|
|
2010
2282
|
}
|
|
2011
|
-
|
|
2012
|
-
|
|
2013
|
-
|
|
2014
|
-
|
|
2015
|
-
|
|
2016
|
-
|
|
2017
|
-
this.log.info("Connection closed");
|
|
2018
|
-
}
|
|
2283
|
+
/**
|
|
2284
|
+
* Find an entity and delete it.
|
|
2285
|
+
* @returns Array of deleted entity IDs (should contain at most one ID)
|
|
2286
|
+
*/
|
|
2287
|
+
async deleteOne(where = {}, opts = {}) {
|
|
2288
|
+
return await this.deleteMany(where, opts);
|
|
2019
2289
|
}
|
|
2020
|
-
migrateLock = $lock({ handler: async () => {
|
|
2021
|
-
await this.migrate();
|
|
2022
|
-
} });
|
|
2023
2290
|
/**
|
|
2024
|
-
*
|
|
2291
|
+
* Find an entity by ID and delete it.
|
|
2292
|
+
* @returns Array containing the deleted entity ID
|
|
2293
|
+
* @throws DbEntityNotFoundError if the entity is not found
|
|
2025
2294
|
*/
|
|
2026
|
-
|
|
2027
|
-
const
|
|
2028
|
-
|
|
2029
|
-
|
|
2030
|
-
user: decodeURIComponent(url.username),
|
|
2031
|
-
database: decodeURIComponent(url.pathname.replace("/", "")),
|
|
2032
|
-
password: decodeURIComponent(url.password),
|
|
2033
|
-
port: Number(url.port || 5432),
|
|
2034
|
-
ssl: this.ssl(url),
|
|
2035
|
-
onnotice: () => {}
|
|
2036
|
-
};
|
|
2295
|
+
async deleteById(id, opts = {}) {
|
|
2296
|
+
const result = await this.deleteMany(this.getWhereId(id), opts);
|
|
2297
|
+
if (result.length === 0) throw new DbEntityNotFoundError(`Entity with ID ${id} not found in ${this.tableName}`);
|
|
2298
|
+
return result;
|
|
2037
2299
|
}
|
|
2038
|
-
|
|
2039
|
-
|
|
2040
|
-
|
|
2300
|
+
/**
|
|
2301
|
+
* Count entities.
|
|
2302
|
+
*/
|
|
2303
|
+
async count(where = {}, opts = {}) {
|
|
2304
|
+
where = this.withDeletedAt(where, opts);
|
|
2305
|
+
return (opts.tx ?? this.db).$count(this.table, this.toSQL(where));
|
|
2041
2306
|
}
|
|
2042
|
-
|
|
2043
|
-
|
|
2044
|
-
|
|
2045
|
-
|
|
2046
|
-
|
|
2047
|
-
/**
|
|
2048
|
-
* Configuration options for the Node.js SQLite database provider.
|
|
2049
|
-
*/
|
|
2050
|
-
const nodeSqliteOptions = $atom({
|
|
2051
|
-
name: "alepha.postgres.node-sqlite.options",
|
|
2052
|
-
schema: t.object({ path: t.optional(t.string({ description: "Filepath or :memory:. If empty, provider will use DATABASE_URL from env." })) }),
|
|
2053
|
-
default: {}
|
|
2054
|
-
});
|
|
2055
|
-
/**
|
|
2056
|
-
* Add a fake support for SQLite in Node.js based on Postgres interfaces.
|
|
2057
|
-
*
|
|
2058
|
-
* This is NOT a real SQLite provider, it's a workaround to use SQLite with Drizzle ORM.
|
|
2059
|
-
* This is NOT recommended for production use.
|
|
2060
|
-
*/
|
|
2061
|
-
var NodeSqliteProvider = class extends DatabaseProvider {
|
|
2062
|
-
kit = $inject(DrizzleKitProvider);
|
|
2063
|
-
log = $logger();
|
|
2064
|
-
env = $env(envSchema$1);
|
|
2065
|
-
builder = $inject(SqliteModelBuilder);
|
|
2066
|
-
options = $use(nodeSqliteOptions);
|
|
2067
|
-
sqlite;
|
|
2068
|
-
get name() {
|
|
2069
|
-
return "sqlite";
|
|
2307
|
+
conflictMessagePattern = "duplicate key value violates unique constraint";
|
|
2308
|
+
handleError(error, message) {
|
|
2309
|
+
if (!(error instanceof Error)) return new DbError(message);
|
|
2310
|
+
if (error.cause?.message.includes(this.conflictMessagePattern) || error.message.includes(this.conflictMessagePattern)) return new DbConflictError(message, error);
|
|
2311
|
+
return new DbError(message, error);
|
|
2070
2312
|
}
|
|
2071
|
-
|
|
2072
|
-
|
|
2073
|
-
const
|
|
2074
|
-
if (
|
|
2075
|
-
|
|
2076
|
-
|
|
2313
|
+
withDeletedAt(where, opts = {}) {
|
|
2314
|
+
if (opts.force) return where;
|
|
2315
|
+
const deletedAt = this.deletedAt();
|
|
2316
|
+
if (!deletedAt) return where;
|
|
2317
|
+
return { and: [where, { [deletedAt.key]: { isNull: true } }] };
|
|
2318
|
+
}
|
|
2319
|
+
deletedAt() {
|
|
2320
|
+
const deletedAtFields = getAttrFields(this.entity.schema, PG_DELETED_AT);
|
|
2321
|
+
if (deletedAtFields.length > 0) return deletedAtFields[0];
|
|
2322
|
+
}
|
|
2323
|
+
/**
|
|
2324
|
+
* Convert something to valid Pg Insert Value.
|
|
2325
|
+
*/
|
|
2326
|
+
cast(data, insert) {
|
|
2327
|
+
const schema$1 = insert ? this.entity.insertSchema : t.partial(this.entity.updateSchema);
|
|
2328
|
+
return this.alepha.codec.encode(schema$1, data);
|
|
2329
|
+
}
|
|
2330
|
+
/**
|
|
2331
|
+
* Transform a row from the database into a clean entity.
|
|
2332
|
+
*/
|
|
2333
|
+
clean(row, schema$1) {
|
|
2334
|
+
for (const key of Object.keys(schema$1.properties)) {
|
|
2335
|
+
const value = schema$1.properties[key];
|
|
2336
|
+
if (typeof row[key] === "string") {
|
|
2337
|
+
if (t.schema.isDateTime(value)) row[key] = this.dateTimeProvider.of(row[key]).toISOString();
|
|
2338
|
+
else if (t.schema.isDate(value)) row[key] = this.dateTimeProvider.of(`${row[key]}T00:00:00Z`).toISOString().split("T")[0];
|
|
2339
|
+
}
|
|
2340
|
+
if (typeof row[key] === "bigint" && t.schema.isBigInt(value)) row[key] = row[key].toString();
|
|
2077
2341
|
}
|
|
2078
|
-
|
|
2079
|
-
else return "node_modules/.alepha/sqlite.db";
|
|
2342
|
+
return this.alepha.codec.decode(schema$1, row);
|
|
2080
2343
|
}
|
|
2081
|
-
|
|
2082
|
-
|
|
2083
|
-
|
|
2084
|
-
|
|
2085
|
-
|
|
2086
|
-
|
|
2087
|
-
|
|
2344
|
+
/**
|
|
2345
|
+
* Clean a row with joins recursively
|
|
2346
|
+
*/
|
|
2347
|
+
cleanWithJoins(row, schema$1, joins, parentPath) {
|
|
2348
|
+
const joinsAtThisLevel = joins.filter((j) => j.parent === parentPath);
|
|
2349
|
+
const cleanRow = { ...row };
|
|
2350
|
+
const joinedData = {};
|
|
2351
|
+
for (const join of joinsAtThisLevel) {
|
|
2352
|
+
joinedData[join.key] = cleanRow[join.key];
|
|
2353
|
+
delete cleanRow[join.key];
|
|
2088
2354
|
}
|
|
2089
|
-
|
|
2090
|
-
|
|
2091
|
-
|
|
2355
|
+
const entity = this.clean(cleanRow, schema$1);
|
|
2356
|
+
for (const join of joinsAtThisLevel) {
|
|
2357
|
+
const joinedValue = joinedData[join.key];
|
|
2358
|
+
if (joinedValue != null) {
|
|
2359
|
+
const joinPath = parentPath ? `${parentPath}.${join.key}` : join.key;
|
|
2360
|
+
if (joins.filter((j) => j.parent === joinPath).length > 0) entity[join.key] = this.cleanWithJoins(joinedValue, join.schema, joins, joinPath);
|
|
2361
|
+
else entity[join.key] = this.clean(joinedValue, join.schema);
|
|
2362
|
+
} else entity[join.key] = void 0;
|
|
2092
2363
|
}
|
|
2093
|
-
return
|
|
2364
|
+
return entity;
|
|
2094
2365
|
}
|
|
2095
|
-
|
|
2096
|
-
|
|
2097
|
-
|
|
2098
|
-
|
|
2099
|
-
|
|
2100
|
-
|
|
2101
|
-
|
|
2102
|
-
|
|
2103
|
-
|
|
2104
|
-
|
|
2105
|
-
|
|
2106
|
-
|
|
2107
|
-
|
|
2108
|
-
|
|
2109
|
-
|
|
2110
|
-
|
|
2111
|
-
|
|
2112
|
-
|
|
2113
|
-
|
|
2114
|
-
|
|
2115
|
-
|
|
2116
|
-
|
|
2117
|
-
|
|
2118
|
-
|
|
2119
|
-
|
|
2120
|
-
|
|
2121
|
-
|
|
2122
|
-
|
|
2123
|
-
|
|
2124
|
-
|
|
2125
|
-
|
|
2126
|
-
this.
|
|
2127
|
-
|
|
2128
|
-
}
|
|
2366
|
+
/**
|
|
2367
|
+
* Convert a where clause to SQL.
|
|
2368
|
+
*/
|
|
2369
|
+
toSQL(where, joins) {
|
|
2370
|
+
return this.queryManager.toSQL(where, {
|
|
2371
|
+
schema: this.entity.schema,
|
|
2372
|
+
col: (name) => {
|
|
2373
|
+
return this.col(name);
|
|
2374
|
+
},
|
|
2375
|
+
joins,
|
|
2376
|
+
dialect: this.provider.dialect
|
|
2377
|
+
});
|
|
2378
|
+
}
|
|
2379
|
+
/**
|
|
2380
|
+
* Get the where clause for an ID.
|
|
2381
|
+
*
|
|
2382
|
+
* @param id The ID to get the where clause for.
|
|
2383
|
+
* @returns The where clause for the ID.
|
|
2384
|
+
*/
|
|
2385
|
+
getWhereId(id) {
|
|
2386
|
+
return { [this.id.key]: { eq: t.schema.isString(this.id.type) ? String(id) : Number(id) } };
|
|
2387
|
+
}
|
|
2388
|
+
/**
|
|
2389
|
+
* Find a primary key in the schema.
|
|
2390
|
+
*/
|
|
2391
|
+
getPrimaryKey(schema$1) {
|
|
2392
|
+
const primaryKeys = getAttrFields(schema$1, PG_PRIMARY_KEY);
|
|
2393
|
+
if (primaryKeys.length === 0) throw new AlephaError("Primary key not found in schema");
|
|
2394
|
+
if (primaryKeys.length > 1) throw new AlephaError(`Multiple primary keys (${primaryKeys.length}) are not supported`);
|
|
2395
|
+
return {
|
|
2396
|
+
key: primaryKeys[0].key,
|
|
2397
|
+
col: this.col(primaryKeys[0].key),
|
|
2398
|
+
type: primaryKeys[0].type
|
|
2399
|
+
};
|
|
2129
2400
|
}
|
|
2130
2401
|
};
|
|
2131
2402
|
|
|
2132
2403
|
//#endregion
|
|
2133
|
-
//#region ../../src/orm/providers/
|
|
2134
|
-
|
|
2135
|
-
|
|
2136
|
-
|
|
2137
|
-
|
|
2138
|
-
|
|
2139
|
-
|
|
2140
|
-
|
|
2141
|
-
env = $env(envSchema);
|
|
2142
|
-
log = $logger();
|
|
2143
|
-
kit = $inject(DrizzleKitProvider);
|
|
2144
|
-
builder = $inject(PostgresModelBuilder);
|
|
2145
|
-
client;
|
|
2146
|
-
pglite;
|
|
2147
|
-
get name() {
|
|
2148
|
-
return "pglite";
|
|
2149
|
-
}
|
|
2150
|
-
dialect = "postgresql";
|
|
2151
|
-
get url() {
|
|
2152
|
-
let path = this.env.DATABASE_URL;
|
|
2153
|
-
if (!path) if (this.alepha.isTest()) path = ":memory:";
|
|
2154
|
-
else path = "node_modules/.alepha/pglite";
|
|
2155
|
-
else if (path.includes(":memory:")) path = ":memory:";
|
|
2156
|
-
else if (path.startsWith("file://")) path = path.replace("file://", "");
|
|
2157
|
-
return path;
|
|
2158
|
-
}
|
|
2159
|
-
get db() {
|
|
2160
|
-
if (!this.pglite) throw new AlephaError("Database not initialized");
|
|
2161
|
-
return this.pglite;
|
|
2404
|
+
//#region ../../src/orm/providers/RepositoryProvider.ts
|
|
2405
|
+
var RepositoryProvider = class {
|
|
2406
|
+
alepha = $inject(Alepha);
|
|
2407
|
+
registry = /* @__PURE__ */ new Map();
|
|
2408
|
+
getRepositories(provider) {
|
|
2409
|
+
const repositories = this.alepha.services(Repository);
|
|
2410
|
+
if (provider) return repositories.filter((it) => it.provider === provider);
|
|
2411
|
+
return repositories;
|
|
2162
2412
|
}
|
|
2163
|
-
|
|
2164
|
-
const
|
|
2165
|
-
return
|
|
2413
|
+
getRepository(entity) {
|
|
2414
|
+
const RepositoryClass = this.createClassRepository(entity);
|
|
2415
|
+
return this.alepha.inject(RepositoryClass);
|
|
2166
2416
|
}
|
|
2167
|
-
|
|
2168
|
-
|
|
2169
|
-
|
|
2170
|
-
|
|
2171
|
-
|
|
2172
|
-
|
|
2173
|
-
|
|
2174
|
-
|
|
2175
|
-
if (path !== ":memory:") {
|
|
2176
|
-
await mkdir(path, { recursive: true }).catch(() => null);
|
|
2177
|
-
this.client = new module.PGlite(path);
|
|
2178
|
-
} else this.client = new module.PGlite();
|
|
2179
|
-
this.pglite = drizzle$3({ client: this.client });
|
|
2180
|
-
await this.migrate();
|
|
2181
|
-
this.log.info(`Using PGlite database at ${path}`);
|
|
2182
|
-
}
|
|
2183
|
-
});
|
|
2184
|
-
onStop = $hook({
|
|
2185
|
-
on: "stop",
|
|
2186
|
-
handler: async () => {
|
|
2187
|
-
if (this.client) {
|
|
2188
|
-
this.log.debug("Closing PGlite connection...");
|
|
2189
|
-
await this.client.close();
|
|
2190
|
-
this.client = void 0;
|
|
2191
|
-
this.pglite = void 0;
|
|
2192
|
-
this.log.info("PGlite connection closed");
|
|
2417
|
+
createClassRepository(entity) {
|
|
2418
|
+
let name = entity.name.charAt(0).toUpperCase() + entity.name.slice(1);
|
|
2419
|
+
if (name.endsWith("s")) name = name.slice(0, -1);
|
|
2420
|
+
name = `${name}Repository`;
|
|
2421
|
+
if (this.registry.has(entity)) return this.registry.get(entity);
|
|
2422
|
+
class GenericRepository extends Repository {
|
|
2423
|
+
constructor() {
|
|
2424
|
+
super(entity);
|
|
2193
2425
|
}
|
|
2194
2426
|
}
|
|
2195
|
-
|
|
2196
|
-
|
|
2197
|
-
|
|
2427
|
+
Object.defineProperty(GenericRepository, "name", { value: name });
|
|
2428
|
+
this.registry.set(entity, GenericRepository);
|
|
2429
|
+
return GenericRepository;
|
|
2198
2430
|
}
|
|
2199
2431
|
};
|
|
2200
2432
|
|
|
@@ -2507,32 +2739,6 @@ function buildQueryString(where) {
|
|
|
2507
2739
|
return parts.join("&");
|
|
2508
2740
|
}
|
|
2509
2741
|
|
|
2510
|
-
//#endregion
|
|
2511
|
-
//#region ../../src/orm/primitives/$transaction.ts
|
|
2512
|
-
/**
|
|
2513
|
-
* Creates a transaction primitive for database operations requiring atomicity and consistency.
|
|
2514
|
-
*
|
|
2515
|
-
* This primitive provides a convenient way to wrap database operations in PostgreSQL
|
|
2516
|
-
* transactions, ensuring ACID properties and automatic retry logic for version conflicts.
|
|
2517
|
-
* It integrates seamlessly with the repository pattern and provides built-in handling
|
|
2518
|
-
* for optimistic locking scenarios with automatic retry on version mismatches.
|
|
2519
|
-
*
|
|
2520
|
-
* **Important Notes**:
|
|
2521
|
-
* - All operations within the transaction handler are atomic
|
|
2522
|
-
* - Automatic retry on `PgVersionMismatchError` for optimistic locking
|
|
2523
|
-
* - Pass `{ tx }` option to all repository operations within the transaction
|
|
2524
|
-
* - Transactions are automatically rolled back on any unhandled error
|
|
2525
|
-
* - Use appropriate isolation levels based on your consistency requirements
|
|
2526
|
-
*/
|
|
2527
|
-
const $transaction = (opts) => {
|
|
2528
|
-
const { alepha } = $context();
|
|
2529
|
-
const provider = alepha.inject(DatabaseProvider);
|
|
2530
|
-
return $retry({
|
|
2531
|
-
when: (err) => err instanceof DbVersionMismatchError,
|
|
2532
|
-
handler: (...args) => provider.db.transaction(async (tx) => opts.handler(tx, ...args), opts.config)
|
|
2533
|
-
});
|
|
2534
|
-
};
|
|
2535
|
-
|
|
2536
2742
|
//#endregion
|
|
2537
2743
|
//#region ../../src/orm/providers/DatabaseTypeProvider.ts
|
|
2538
2744
|
var DatabaseTypeProvider = class {
|
|
@@ -2654,11 +2860,101 @@ const pg = db;
|
|
|
2654
2860
|
*/
|
|
2655
2861
|
const legacyIdSchema = pgAttr(pgAttr(pgAttr(t.integer(), PG_PRIMARY_KEY), PG_SERIAL), PG_DEFAULT);
|
|
2656
2862
|
|
|
2863
|
+
//#endregion
|
|
2864
|
+
//#region ../../src/orm/primitives/$repository.ts
|
|
2865
|
+
/**
|
|
2866
|
+
* Get the repository for the given entity.
|
|
2867
|
+
*/
|
|
2868
|
+
const $repository = (entity) => {
|
|
2869
|
+
const { alepha } = $context();
|
|
2870
|
+
return $inject(alepha.inject(RepositoryProvider).createClassRepository(entity));
|
|
2871
|
+
};
|
|
2872
|
+
|
|
2873
|
+
//#endregion
|
|
2874
|
+
//#region ../../src/orm/primitives/$transaction.ts
|
|
2875
|
+
/**
|
|
2876
|
+
* Creates a transaction primitive for database operations requiring atomicity and consistency.
|
|
2877
|
+
*
|
|
2878
|
+
* This primitive provides a convenient way to wrap database operations in PostgreSQL
|
|
2879
|
+
* transactions, ensuring ACID properties and automatic retry logic for version conflicts.
|
|
2880
|
+
* It integrates seamlessly with the repository pattern and provides built-in handling
|
|
2881
|
+
* for optimistic locking scenarios with automatic retry on version mismatches.
|
|
2882
|
+
*
|
|
2883
|
+
* **Important Notes**:
|
|
2884
|
+
* - All operations within the transaction handler are atomic
|
|
2885
|
+
* - Automatic retry on `PgVersionMismatchError` for optimistic locking
|
|
2886
|
+
* - Pass `{ tx }` option to all repository operations within the transaction
|
|
2887
|
+
* - Transactions are automatically rolled back on any unhandled error
|
|
2888
|
+
* - Use appropriate isolation levels based on your consistency requirements
|
|
2889
|
+
*/
|
|
2890
|
+
const $transaction = (opts) => {
|
|
2891
|
+
const { alepha } = $context();
|
|
2892
|
+
const provider = alepha.inject(DatabaseProvider);
|
|
2893
|
+
return $retry({
|
|
2894
|
+
when: (err) => err instanceof DbVersionMismatchError,
|
|
2895
|
+
handler: (...args) => provider.db.transaction(async (tx) => opts.handler(tx, ...args), opts.config)
|
|
2896
|
+
});
|
|
2897
|
+
};
|
|
2898
|
+
|
|
2657
2899
|
//#endregion
|
|
2658
2900
|
//#region ../../src/orm/index.ts
|
|
2901
|
+
var orm_exports = /* @__PURE__ */ __exportAll({
|
|
2902
|
+
$entity: () => $entity,
|
|
2903
|
+
$repository: () => $repository,
|
|
2904
|
+
$sequence: () => $sequence,
|
|
2905
|
+
$transaction: () => $transaction,
|
|
2906
|
+
AlephaPostgres: () => AlephaPostgres,
|
|
2907
|
+
BunPostgresProvider: () => BunPostgresProvider,
|
|
2908
|
+
BunSqliteProvider: () => BunSqliteProvider,
|
|
2909
|
+
CloudflareD1Provider: () => CloudflareD1Provider,
|
|
2910
|
+
DatabaseProvider: () => DatabaseProvider,
|
|
2911
|
+
DatabaseTypeProvider: () => DatabaseTypeProvider,
|
|
2912
|
+
DbConflictError: () => DbConflictError,
|
|
2913
|
+
DbEntityNotFoundError: () => DbEntityNotFoundError,
|
|
2914
|
+
DbError: () => DbError,
|
|
2915
|
+
DbMigrationError: () => DbMigrationError,
|
|
2916
|
+
DbVersionMismatchError: () => DbVersionMismatchError,
|
|
2917
|
+
DrizzleKitProvider: () => DrizzleKitProvider,
|
|
2918
|
+
EntityPrimitive: () => EntityPrimitive,
|
|
2919
|
+
NodePostgresProvider: () => NodePostgresProvider,
|
|
2920
|
+
NodeSqliteProvider: () => NodeSqliteProvider,
|
|
2921
|
+
PG_CREATED_AT: () => PG_CREATED_AT,
|
|
2922
|
+
PG_DEFAULT: () => PG_DEFAULT,
|
|
2923
|
+
PG_DELETED_AT: () => PG_DELETED_AT,
|
|
2924
|
+
PG_ENUM: () => PG_ENUM,
|
|
2925
|
+
PG_IDENTITY: () => PG_IDENTITY,
|
|
2926
|
+
PG_PRIMARY_KEY: () => PG_PRIMARY_KEY,
|
|
2927
|
+
PG_REF: () => PG_REF,
|
|
2928
|
+
PG_SERIAL: () => PG_SERIAL,
|
|
2929
|
+
PG_UPDATED_AT: () => PG_UPDATED_AT,
|
|
2930
|
+
PG_VERSION: () => PG_VERSION,
|
|
2931
|
+
Repository: () => Repository,
|
|
2932
|
+
RepositoryProvider: () => RepositoryProvider,
|
|
2933
|
+
SequencePrimitive: () => SequencePrimitive,
|
|
2934
|
+
buildQueryString: () => buildQueryString,
|
|
2935
|
+
bunSqliteOptions: () => bunSqliteOptions,
|
|
2936
|
+
db: () => db,
|
|
2937
|
+
drizzle: () => drizzle,
|
|
2938
|
+
getAttrFields: () => getAttrFields,
|
|
2939
|
+
insertSchema: () => insertSchema,
|
|
2940
|
+
legacyIdSchema: () => legacyIdSchema,
|
|
2941
|
+
nodeSqliteOptions: () => nodeSqliteOptions,
|
|
2942
|
+
pageQuerySchema: () => pageQuerySchema,
|
|
2943
|
+
pageSchema: () => pageSchema,
|
|
2944
|
+
parseQueryString: () => parseQueryString,
|
|
2945
|
+
pg: () => pg,
|
|
2946
|
+
pgAttr: () => pgAttr,
|
|
2947
|
+
schema: () => schema,
|
|
2948
|
+
sql: () => sql,
|
|
2949
|
+
updateSchema: () => updateSchema
|
|
2950
|
+
});
|
|
2659
2951
|
/**
|
|
2660
2952
|
* Postgres client based on Drizzle ORM, Alepha type-safe friendly.
|
|
2661
2953
|
*
|
|
2954
|
+
* Automatically selects the appropriate provider based on runtime:
|
|
2955
|
+
* - Bun: Uses `BunPostgresProvider` or `BunSqliteProvider`
|
|
2956
|
+
* - Node.js: Uses `NodePostgresProvider` or `NodeSqliteProvider`
|
|
2957
|
+
*
|
|
2662
2958
|
* ```ts
|
|
2663
2959
|
* import { t } from "alepha";
|
|
2664
2960
|
* import { $entity, $repository, db } from "alepha/postgres";
|
|
@@ -2697,6 +2993,10 @@ const legacyIdSchema = pgAttr(pgAttr(pgAttr(t.integer(), PG_PRIMARY_KEY), PG_SER
|
|
|
2697
2993
|
* @see {@link $sequence}
|
|
2698
2994
|
* @see {@link $repository}
|
|
2699
2995
|
* @see {@link $transaction}
|
|
2996
|
+
* @see {@link NodePostgresProvider} - Node.js Postgres implementation
|
|
2997
|
+
* @see {@link NodeSqliteProvider} - Node.js SQLite implementation
|
|
2998
|
+
* @see {@link BunPostgresProvider} - Bun Postgres implementation
|
|
2999
|
+
* @see {@link BunSqliteProvider} - Bun SQLite implementation
|
|
2700
3000
|
* @module alepha.postgres
|
|
2701
3001
|
*/
|
|
2702
3002
|
const AlephaPostgres = $module({
|
|
@@ -2706,8 +3006,10 @@ const AlephaPostgres = $module({
|
|
|
2706
3006
|
AlephaDateTime,
|
|
2707
3007
|
DatabaseProvider,
|
|
2708
3008
|
NodePostgresProvider,
|
|
2709
|
-
PglitePostgresProvider,
|
|
2710
3009
|
NodeSqliteProvider,
|
|
3010
|
+
BunPostgresProvider,
|
|
3011
|
+
BunSqliteProvider,
|
|
3012
|
+
PglitePostgresProvider,
|
|
2711
3013
|
CloudflareD1Provider,
|
|
2712
3014
|
SqliteModelBuilder,
|
|
2713
3015
|
PostgresModelBuilder,
|
|
@@ -2727,6 +3029,7 @@ const AlephaPostgres = $module({
|
|
|
2727
3029
|
const isSqlite = url?.startsWith("sqlite:");
|
|
2728
3030
|
const isMemory = url?.includes(":memory:");
|
|
2729
3031
|
const isFile = !!url && !isPostgres && !isMemory;
|
|
3032
|
+
const isBun = alepha.isBun();
|
|
2730
3033
|
if (url?.startsWith("cloudflare-d1:")) {
|
|
2731
3034
|
alepha.with({
|
|
2732
3035
|
optional: true,
|
|
@@ -2747,18 +3050,18 @@ const AlephaPostgres = $module({
|
|
|
2747
3050
|
alepha.with({
|
|
2748
3051
|
optional: true,
|
|
2749
3052
|
provide: DatabaseProvider,
|
|
2750
|
-
use: NodePostgresProvider
|
|
3053
|
+
use: isBun ? BunPostgresProvider : NodePostgresProvider
|
|
2751
3054
|
});
|
|
2752
3055
|
return;
|
|
2753
3056
|
}
|
|
2754
3057
|
alepha.with({
|
|
2755
3058
|
optional: true,
|
|
2756
3059
|
provide: DatabaseProvider,
|
|
2757
|
-
use: NodeSqliteProvider
|
|
3060
|
+
use: isBun ? BunSqliteProvider : NodeSqliteProvider
|
|
2758
3061
|
});
|
|
2759
3062
|
}
|
|
2760
3063
|
});
|
|
2761
3064
|
|
|
2762
3065
|
//#endregion
|
|
2763
|
-
export { $entity, $repository, $sequence, $transaction, AlephaPostgres, CloudflareD1Provider, DatabaseProvider, DatabaseTypeProvider, DbConflictError, DbEntityNotFoundError, DbError, DbMigrationError, DbVersionMismatchError, DrizzleKitProvider, EntityPrimitive, NodePostgresProvider, NodeSqliteProvider, PG_CREATED_AT, PG_DEFAULT, PG_DELETED_AT, PG_ENUM, PG_IDENTITY, PG_PRIMARY_KEY, PG_REF, PG_SERIAL, PG_UPDATED_AT, PG_VERSION, Repository, RepositoryProvider, SequencePrimitive, buildQueryString, db, drizzle, getAttrFields, insertSchema, legacyIdSchema, nodeSqliteOptions, pageQuerySchema, pageSchema, parseQueryString, pg, pgAttr, schema, sql, updateSchema };
|
|
3066
|
+
export { $entity, $repository, $sequence, $transaction, AlephaPostgres, BunPostgresProvider, BunSqliteProvider, CloudflareD1Provider, DatabaseProvider, DatabaseTypeProvider, DbConflictError, DbEntityNotFoundError, DbError, DbMigrationError, DbVersionMismatchError, DrizzleKitProvider, EntityPrimitive, NodePostgresProvider, NodeSqliteProvider, PG_CREATED_AT, PG_DEFAULT, PG_DELETED_AT, PG_ENUM, PG_IDENTITY, PG_PRIMARY_KEY, PG_REF, PG_SERIAL, PG_UPDATED_AT, PG_VERSION, Repository, RepositoryProvider, SequencePrimitive, buildQueryString, bunSqliteOptions, db, drizzle, getAttrFields, insertSchema, legacyIdSchema, nodeSqliteOptions, pageQuerySchema, pageSchema, parseQueryString, pg, pgAttr, schema, sql, updateSchema };
|
|
2764
3067
|
//# sourceMappingURL=index.js.map
|