nextly 0.0.1 → 0.0.2-alpha.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +22 -0
- package/README.md +122 -0
- package/dist/_dts-chunks/collections-handler.d-DjgO74Wt.d.ts +20540 -0
- package/dist/_dts-chunks/config.d-DNwsDnjs.d.ts +2589 -0
- package/dist/_dts-chunks/define-component.d-BUgTHmt3.d.ts +1149 -0
- package/dist/_dts-chunks/image-processor.d-OO1PmMrv.d.ts +335 -0
- package/dist/_dts-chunks/index.d-axCAzZ7m.d.ts +17842 -0
- package/dist/_dts-chunks/media.d-DjDOZo4B.d.ts +117 -0
- package/dist/_dts-chunks/on-error.d-CHIKWNxd.d.ts +38 -0
- package/dist/_dts-chunks/storage.d-BUhQ2we_.d.ts +404 -0
- package/dist/actions/index.d.ts +239 -0
- package/dist/actions/index.mjs +281 -0
- package/dist/api/auth-state.d.ts +5 -0
- package/dist/api/auth-state.mjs +131 -0
- package/dist/api/collections-schema-detail.d.ts +56 -0
- package/dist/api/collections-schema-detail.mjs +244 -0
- package/dist/api/collections-schema-export.d.ts +56 -0
- package/dist/api/collections-schema-export.mjs +129 -0
- package/dist/api/collections-schema.d.ts +59 -0
- package/dist/api/collections-schema.mjs +207 -0
- package/dist/api/components-detail.d.ts +50 -0
- package/dist/api/components-detail.mjs +132 -0
- package/dist/api/components.d.ts +69 -0
- package/dist/api/components.mjs +144 -0
- package/dist/api/email-providers-default.d.ts +40 -0
- package/dist/api/email-providers-default.mjs +75 -0
- package/dist/api/email-providers-detail.d.ts +81 -0
- package/dist/api/email-providers-detail.mjs +109 -0
- package/dist/api/email-providers-test.d.ts +43 -0
- package/dist/api/email-providers-test.mjs +114 -0
- package/dist/api/email-providers.d.ts +69 -0
- package/dist/api/email-providers.mjs +110 -0
- package/dist/api/email-send-template.d.ts +41 -0
- package/dist/api/email-send-template.mjs +58 -0
- package/dist/api/email-send.d.ts +42 -0
- package/dist/api/email-send.mjs +58 -0
- package/dist/api/email-templates-detail.d.ts +74 -0
- package/dist/api/email-templates-detail.mjs +112 -0
- package/dist/api/email-templates-layout.d.ts +55 -0
- package/dist/api/email-templates-layout.mjs +92 -0
- package/dist/api/email-templates-preview.d.ts +48 -0
- package/dist/api/email-templates-preview.mjs +93 -0
- package/dist/api/email-templates.d.ts +61 -0
- package/dist/api/email-templates.mjs +118 -0
- package/dist/api/health.d.ts +68 -0
- package/dist/api/health.mjs +67 -0
- package/dist/api/index.d.ts +54 -0
- package/dist/api/index.mjs +16 -0
- package/dist/api/media-bulk.d.ts +74 -0
- package/dist/api/media-bulk.mjs +196 -0
- package/dist/api/media-folders.d.ts +112 -0
- package/dist/api/media-folders.mjs +187 -0
- package/dist/api/media-handlers.d.ts +102 -0
- package/dist/api/media-handlers.mjs +437 -0
- package/dist/api/media.d.ts +117 -0
- package/dist/api/media.mjs +242 -0
- package/dist/api/singles-detail.d.ts +87 -0
- package/dist/api/singles-detail.mjs +170 -0
- package/dist/api/singles-schema-detail.d.ts +54 -0
- package/dist/api/singles-schema-detail.mjs +182 -0
- package/dist/api/singles.d.ts +34 -0
- package/dist/api/singles.mjs +94 -0
- package/dist/api/storage-upload-url.d.ts +48 -0
- package/dist/api/storage-upload-url.mjs +202 -0
- package/dist/api/uploads.d.ts +109 -0
- package/dist/api/uploads.mjs +359 -0
- package/dist/auth/index.d.ts +425 -0
- package/dist/auth/index.mjs +199 -0
- package/dist/boot-apply-PQSYLDIN.mjs +7 -0
- package/dist/chunk-2OALJTK6.mjs +489 -0
- package/dist/chunk-2Q2SX2CS.mjs +365 -0
- package/dist/chunk-2TFX4ND3.mjs +13 -0
- package/dist/chunk-2TWPDSYD.mjs +87 -0
- package/dist/chunk-2W3DVD7S.mjs +647 -0
- package/dist/chunk-2ZFKXPQM.mjs +88 -0
- package/dist/chunk-3FA7FKAV.mjs +832 -0
- package/dist/chunk-3NZ2KMBL.mjs +58 -0
- package/dist/chunk-4MJLT6PZ.mjs +0 -0
- package/dist/chunk-56WO4WX7.mjs +0 -0
- package/dist/chunk-5APFUGAD.mjs +89 -0
- package/dist/chunk-5HMZ644B.mjs +108 -0
- package/dist/chunk-67GXH6PR.mjs +32 -0
- package/dist/chunk-6JNEPWRW.mjs +14368 -0
- package/dist/chunk-6NFHQIJD.mjs +45 -0
- package/dist/chunk-7P6ASYW6.mjs +9 -0
- package/dist/chunk-A3WPLSDT.mjs +1364 -0
- package/dist/chunk-AGJ6F2T3.mjs +144 -0
- package/dist/chunk-AK6Z23OX.mjs +1464 -0
- package/dist/chunk-APKKRD2G.mjs +102 -0
- package/dist/chunk-B2GV2BWH.mjs +73 -0
- package/dist/chunk-D5HQBNUB.mjs +74 -0
- package/dist/chunk-DNNG377Z.mjs +204 -0
- package/dist/chunk-DP3G27G5.mjs +135 -0
- package/dist/chunk-DV6WVX2Q.mjs +0 -0
- package/dist/chunk-DXGGXIUZ.mjs +57 -0
- package/dist/chunk-EGXBZCGC.mjs +943 -0
- package/dist/chunk-ERCNLX3V.mjs +176 -0
- package/dist/chunk-FQULBZ53.mjs +850 -0
- package/dist/chunk-G2AA4QLC.mjs +262 -0
- package/dist/chunk-GDBJ5JCU.mjs +488 -0
- package/dist/chunk-GJNSJU4S.mjs +19 -0
- package/dist/chunk-GZ6DCQKC.mjs +69 -0
- package/dist/chunk-H26B4FYG.mjs +167 -0
- package/dist/chunk-I4JMR3UR.mjs +21 -0
- package/dist/chunk-INV7QKLG.mjs +508 -0
- package/dist/chunk-IUDOC7N7.mjs +46 -0
- package/dist/chunk-IZWPRDC3.mjs +206 -0
- package/dist/chunk-KIMNCZGV.mjs +15 -0
- package/dist/chunk-L6HW2DA7.mjs +15 -0
- package/dist/chunk-LAZXX4HR.mjs +100 -0
- package/dist/chunk-LDKCUMHK.mjs +95 -0
- package/dist/chunk-LRXMECUA.mjs +0 -0
- package/dist/chunk-M52VMPGA.mjs +119 -0
- package/dist/chunk-MGUWEEI6.mjs +160 -0
- package/dist/chunk-NRUWQ5Z7.mjs +419 -0
- package/dist/chunk-NSEFNNU4.mjs +25360 -0
- package/dist/chunk-NTHVDFGO.mjs +138 -0
- package/dist/chunk-O3QHXMOX.mjs +3166 -0
- package/dist/chunk-P7NH2OSC.mjs +2605 -0
- package/dist/chunk-PKMABBB5.mjs +184 -0
- package/dist/chunk-PWS6XGJK.mjs +76 -0
- package/dist/chunk-R6JJQHFC.mjs +20 -0
- package/dist/chunk-RJLLGGPG.mjs +0 -0
- package/dist/chunk-SBACDPNX.mjs +689 -0
- package/dist/chunk-TO5AFLVQ.mjs +124 -0
- package/dist/chunk-TS7GHTG2.mjs +5436 -0
- package/dist/chunk-UJ2IMJ4W.mjs +133 -0
- package/dist/chunk-UOP63Q54.mjs +102 -0
- package/dist/chunk-UUOFWCM6.mjs +78 -0
- package/dist/chunk-V4EQTOA4.mjs +893 -0
- package/dist/chunk-VJ66NCL4.mjs +193 -0
- package/dist/chunk-VQJQHVEV.mjs +29 -0
- package/dist/chunk-VTJADRO3.mjs +141 -0
- package/dist/chunk-VWF3JO32.mjs +0 -0
- package/dist/chunk-W4MGXIRR.mjs +27 -0
- package/dist/chunk-W5KKPZT5.mjs +1204 -0
- package/dist/chunk-WD34YQ6T.mjs +381 -0
- package/dist/chunk-WZBYMYVW.mjs +14 -0
- package/dist/chunk-X23WKS3Z.mjs +50 -0
- package/dist/chunk-X7TXCYYN.mjs +6496 -0
- package/dist/chunk-XGI4EMS3.mjs +140 -0
- package/dist/chunk-XZKLBMN6.mjs +1153 -0
- package/dist/chunk-YB7INWPY.mjs +0 -0
- package/dist/chunk-YV4Y7SDL.mjs +83 -0
- package/dist/chunk-YZNBLFIW.mjs +1688 -0
- package/dist/chunk-YZZCTONM.mjs +263 -0
- package/dist/chunk-ZE6A3FYH.mjs +289 -0
- package/dist/cli/nextly.mjs +68 -0
- package/dist/cli/utils/index.d.ts +449 -0
- package/dist/cli/utils/index.mjs +49 -0
- package/dist/component-schema-service-5577KVW6.mjs +11 -0
- package/dist/config-loader-23YEMC3Z.mjs +23 -0
- package/dist/config.d.ts +44 -0
- package/dist/config.mjs +109 -0
- package/dist/container-ORGFGYSZ.mjs +9 -0
- package/dist/database/index.d.ts +12 -0
- package/dist/database/index.mjs +40 -0
- package/dist/database/seeders/index.d.ts +93 -0
- package/dist/database/seeders/index.mjs +47 -0
- package/dist/db-sync-demote-LJGKLB3S.mjs +117 -0
- package/dist/db-sync-promote-B26VSYQF.mjs +113 -0
- package/dist/dev-reload-broadcaster-B73IQ53V.mjs +25 -0
- package/dist/dist-M2NOU37V.mjs +19 -0
- package/dist/drizzle-kit-lazy-D2M2PXR2.mjs +13 -0
- package/dist/dynamic-collection-schema-service-IEXTPIZ7.mjs +8 -0
- package/dist/errors/index.d.ts +159 -0
- package/dist/errors/index.mjs +10 -0
- package/dist/factory-IWMBKUJM.mjs +15 -0
- package/dist/first-run-QIVKWJIF.mjs +63 -0
- package/dist/fresh-push-NR67DC3R.mjs +8 -0
- package/dist/index.d.ts +4175 -0
- package/dist/index.mjs +1336 -0
- package/dist/local-plugin-PTET4NAT.mjs +7 -0
- package/dist/logger-NU46DXNY.mjs +15 -0
- package/dist/logger-YE4TC7ZN.mjs +9 -0
- package/dist/migration-journal-EP532Y4L.mjs +139 -0
- package/dist/migrations/mysql/0000_eager_sentry.sql +174 -0
- package/dist/migrations/mysql/0001_soft_giant_girl.sql +27 -0
- package/dist/migrations/mysql/0002_media_table.sql +24 -0
- package/dist/migrations/mysql/0003_dynamic_singles.sql +37 -0
- package/dist/migrations/mysql/0004_dynamic_components.sql +35 -0
- package/dist/migrations/mysql/0005_user_management_tables.sql +92 -0
- package/dist/migrations/mysql/0006_api_keys.sql +36 -0
- package/dist/migrations/mysql/0007_general_settings.sql +20 -0
- package/dist/migrations/mysql/0008_site_settings_logo_url.sql +9 -0
- package/dist/migrations/mysql/0009_activity_log.sql +30 -0
- package/dist/migrations/mysql/0010_site_settings_sidebar.sql +13 -0
- package/dist/migrations/mysql/0011_missing_tables_and_columns.sql +54 -0
- package/dist/migrations/mysql/0012_image_sizes_and_focal_point.sql +30 -0
- package/dist/migrations/mysql/0012_media_folders.sql +43 -0
- package/dist/migrations/mysql/0013_user_brute_force_protection.sql +31 -0
- package/dist/migrations/mysql/0014_email_template_attachments.sql +12 -0
- package/dist/migrations/mysql/0015_media_uploaded_by_nullable.sql +15 -0
- package/dist/migrations/mysql/20260429_000000_000_initial_journal.sql +22 -0
- package/dist/migrations/mysql/20260501_000000_journal_batch.sql +17 -0
- package/dist/migrations/mysql/20260501_000001_audit_log.sql +24 -0
- package/dist/migrations/mysql/20260504_000000_nextly_meta.sql +21 -0
- package/dist/migrations/mysql/meta/0000_snapshot.json +1005 -0
- package/dist/migrations/mysql/meta/0001_snapshot.json +1099 -0
- package/dist/migrations/mysql/meta/_journal.json +41 -0
- package/dist/migrations/postgresql/0000_misty_king_bedlam.sql +169 -0
- package/dist/migrations/postgresql/0001_perpetual_captain_marvel.sql +8 -0
- package/dist/migrations/postgresql/0002_sad_spectrum.sql +16 -0
- package/dist/migrations/postgresql/0003_hesitant_ultron.sql +17 -0
- package/dist/migrations/postgresql/0004_media_table.sql +24 -0
- package/dist/migrations/postgresql/0005_media_folders.sql +36 -0
- package/dist/migrations/postgresql/0006_dynamic_collections_update.sql +50 -0
- package/dist/migrations/postgresql/0007_dynamic_singles.sql +38 -0
- package/dist/migrations/postgresql/0008_dynamic_components.sql +37 -0
- package/dist/migrations/postgresql/0009_user_management_tables.sql +95 -0
- package/dist/migrations/postgresql/0010_api_keys.sql +34 -0
- package/dist/migrations/postgresql/0011_general_settings.sql +20 -0
- package/dist/migrations/postgresql/0012_site_settings_logo_url.sql +9 -0
- package/dist/migrations/postgresql/0013_activity_log.sql +29 -0
- package/dist/migrations/postgresql/0014_image_sizes_and_focal_point.sql +33 -0
- package/dist/migrations/postgresql/0014_site_settings_sidebar.sql +13 -0
- package/dist/migrations/postgresql/0015_user_brute_force_protection.sql +29 -0
- package/dist/migrations/postgresql/0016_email_template_attachments.sql +12 -0
- package/dist/migrations/postgresql/0017_media_uploaded_by_nullable.sql +15 -0
- package/dist/migrations/postgresql/20260429_000000_000_initial_journal.sql +24 -0
- package/dist/migrations/postgresql/20260501_000000_journal_batch.sql +17 -0
- package/dist/migrations/postgresql/20260501_000001_audit_log.sql +24 -0
- package/dist/migrations/postgresql/20260504_000000_nextly_meta.sql +22 -0
- package/dist/migrations/postgresql/meta/0000_snapshot.json +1286 -0
- package/dist/migrations/postgresql/meta/0001_snapshot.json +1407 -0
- package/dist/migrations/postgresql/meta/0002_snapshot.json +1552 -0
- package/dist/migrations/postgresql/meta/0003_snapshot.json +1695 -0
- package/dist/migrations/postgresql/meta/0010_snapshot.json +2345 -0
- package/dist/migrations/postgresql/meta/_journal.json +90 -0
- package/dist/migrations/sqlite/0000_api_keys.sql +34 -0
- package/dist/migrations/sqlite/0001_general_settings.sql +20 -0
- package/dist/migrations/sqlite/0002_site_settings_logo_url.sql +9 -0
- package/dist/migrations/sqlite/0003_activity_log.sql +29 -0
- package/dist/migrations/sqlite/0004_image_sizes_and_focal_point.sql +29 -0
- package/dist/migrations/sqlite/0004_site_settings_sidebar.sql +11 -0
- package/dist/migrations/sqlite/0005_user_brute_force_protection.sql +29 -0
- package/dist/migrations/sqlite/0006_email_template_attachments.sql +12 -0
- package/dist/migrations/sqlite/0007_media_uploaded_by_nullable.sql +111 -0
- package/dist/migrations/sqlite/20260429_000000_000_initial_journal.sql +24 -0
- package/dist/migrations/sqlite/20260501_000000_journal_batch.sql +19 -0
- package/dist/migrations/sqlite/20260501_000001_audit_log.sql +24 -0
- package/dist/migrations/sqlite/20260504_000000_nextly_meta.sql +21 -0
- package/dist/migrations/sqlite/20260505_000000_user_management_tables.sql +77 -0
- package/dist/next.d.ts +57 -0
- package/dist/next.mjs +55 -0
- package/dist/observability/index.d.ts +87 -0
- package/dist/observability/index.mjs +57 -0
- package/dist/permissions-3DZZQZMI.mjs +39 -0
- package/dist/pipeline-YOML7SWF.mjs +29 -0
- package/dist/preview-ZZTR3QGS.mjs +9 -0
- package/dist/program-PW6UB2ZC.mjs +5934 -0
- package/dist/reconcile-single-tables-7ENVXJGB.mjs +7 -0
- package/dist/register-SF6E6FVU.mjs +49 -0
- package/dist/reload-config-HWQ4G5MM.mjs +23 -0
- package/dist/resolve-single-table-name-JSOMUB3R.mjs +7 -0
- package/dist/routeHandler-UNMMJIBM.mjs +77 -0
- package/dist/runtime-schema-generator-NRA6A6Z6.mjs +8 -0
- package/dist/runtime.d.ts +120 -0
- package/dist/runtime.mjs +73 -0
- package/dist/schema-hash-FMMG6VPJ.mjs +13 -0
- package/dist/schema-registry-EQ36FZDP.mjs +7 -0
- package/dist/scripts/load-env.mjs +42 -0
- package/dist/storage/index.d.ts +566 -0
- package/dist/storage/index.mjs +45 -0
- package/dist/super-admin-G5ZK5F4T.mjs +39 -0
- package/dist/system-table-service-WGSRVEGT.mjs +17 -0
- package/dist/users-7KELGRYJ.mjs +38 -0
- package/package.json +308 -9
|
@@ -0,0 +1,647 @@
|
|
|
1
|
+
import {
|
|
2
|
+
getDialectTables
|
|
3
|
+
} from "./chunk-TS7GHTG2.mjs";
|
|
4
|
+
import {
|
|
5
|
+
container
|
|
6
|
+
} from "./chunk-D5HQBNUB.mjs";
|
|
7
|
+
|
|
8
|
+
// src/shared/lib/date-formatting.ts
|
|
9
|
+
var ISO_DATE_TIME_REGEX = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}(?::\d{2}(?:\.\d{1,3})?)?(?:Z|[+-]\d{2}:\d{2})?$/;
|
|
10
|
+
var MYSQL_DATE_TIME_REGEX = /^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}(?:\.\d{1,3})?$/;
|
|
11
|
+
var HAS_EXPLICIT_TIMEZONE_REGEX = /(?:Z|[+-]\d{2}:\d{2})$/i;
|
|
12
|
+
var TIMESTAMP_KEY_REGEX = /(?:^|_)(createdAt|updatedAt|created_at|updated_at|publishedAt|published_at|deletedAt|deleted_at|lastLoginAt|last_login_at|expires|expiresAt|expires_at|date|time|timestamp|at|on)$/i;
|
|
13
|
+
function shouldDebugTimezone() {
|
|
14
|
+
return process.env.NEXTLY_DEBUG_TIMEZONE === "1";
|
|
15
|
+
}
|
|
16
|
+
function debugTimezone(stage, details) {
|
|
17
|
+
if (!shouldDebugTimezone()) return;
|
|
18
|
+
console.debug(`[timezone][${stage}]`, details);
|
|
19
|
+
}
|
|
20
|
+
function isTimestampFieldKey(key) {
|
|
21
|
+
if (!key) return false;
|
|
22
|
+
return TIMESTAMP_KEY_REGEX.test(key);
|
|
23
|
+
}
|
|
24
|
+
function formatIsoWithTimezone(date, timezone) {
|
|
25
|
+
try {
|
|
26
|
+
const formatter = new Intl.DateTimeFormat("en-US", {
|
|
27
|
+
timeZone: timezone,
|
|
28
|
+
year: "numeric",
|
|
29
|
+
month: "2-digit",
|
|
30
|
+
day: "2-digit",
|
|
31
|
+
hour: "2-digit",
|
|
32
|
+
minute: "2-digit",
|
|
33
|
+
second: "2-digit",
|
|
34
|
+
fractionalSecondDigits: 3,
|
|
35
|
+
hourCycle: "h23"
|
|
36
|
+
});
|
|
37
|
+
const parts = formatter.formatToParts(date);
|
|
38
|
+
const map = {};
|
|
39
|
+
parts.forEach((p) => {
|
|
40
|
+
map[p.type] = p.value;
|
|
41
|
+
});
|
|
42
|
+
const year = map.year || date.getFullYear().toString();
|
|
43
|
+
const month = map.month || (date.getMonth() + 1).toString().padStart(2, "0");
|
|
44
|
+
const day = map.day || date.getDate().toString().padStart(2, "0");
|
|
45
|
+
const hour = map.hour || date.getHours().toString().padStart(2, "0");
|
|
46
|
+
const minute = map.minute || date.getMinutes().toString().padStart(2, "0");
|
|
47
|
+
const second = map.second || date.getSeconds().toString().padStart(2, "0");
|
|
48
|
+
const fractionalSecond = map.fractionalSecond || "000";
|
|
49
|
+
const isoNoOffset = `${year}-${month}-${day}T${hour}:${minute}:${second}.${fractionalSecond}`;
|
|
50
|
+
const offsetFormatter = new Intl.DateTimeFormat("en-US", {
|
|
51
|
+
timeZone: timezone,
|
|
52
|
+
timeZoneName: "longOffset"
|
|
53
|
+
});
|
|
54
|
+
const offsetPart = offsetFormatter.formatToParts(date).find((p) => p.type === "timeZoneName")?.value || "GMT";
|
|
55
|
+
let formattedOffset = offsetPart.replace("GMT", "").trim();
|
|
56
|
+
if (!formattedOffset || formattedOffset === "Z") return `${isoNoOffset}Z`;
|
|
57
|
+
if (!formattedOffset.includes(":")) {
|
|
58
|
+
const sign = formattedOffset[0];
|
|
59
|
+
const hours = formattedOffset.substring(1).padStart(2, "0");
|
|
60
|
+
formattedOffset = `${sign}${hours}:00`;
|
|
61
|
+
}
|
|
62
|
+
return `${isoNoOffset}${formattedOffset}`;
|
|
63
|
+
} catch (_error) {
|
|
64
|
+
return date.toISOString();
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
function normalizeTimestampString(value, timezone) {
|
|
68
|
+
if (!ISO_DATE_TIME_REGEX.test(value) && !MYSQL_DATE_TIME_REGEX.test(value)) {
|
|
69
|
+
return value;
|
|
70
|
+
}
|
|
71
|
+
const hasExplicitTimezone = HAS_EXPLICIT_TIMEZONE_REGEX.test(value);
|
|
72
|
+
const normalizedInput = MYSQL_DATE_TIME_REGEX.test(value) ? value.replace(" ", "T") : value;
|
|
73
|
+
const parseInput = hasExplicitTimezone ? normalizedInput : `${normalizedInput}Z`;
|
|
74
|
+
const parsed = new Date(parseInput);
|
|
75
|
+
if (Number.isNaN(parsed.getTime())) {
|
|
76
|
+
debugTimezone("parse-failed", {
|
|
77
|
+
raw: value,
|
|
78
|
+
parseInput,
|
|
79
|
+
reason: "Invalid Date"
|
|
80
|
+
});
|
|
81
|
+
return value;
|
|
82
|
+
}
|
|
83
|
+
if (timezone) {
|
|
84
|
+
const localized = formatIsoWithTimezone(parsed, timezone);
|
|
85
|
+
debugTimezone("localized", {
|
|
86
|
+
raw: value,
|
|
87
|
+
timezone,
|
|
88
|
+
localized
|
|
89
|
+
});
|
|
90
|
+
return localized;
|
|
91
|
+
}
|
|
92
|
+
const normalized = parsed.toISOString();
|
|
93
|
+
debugTimezone("normalized", {
|
|
94
|
+
raw: value,
|
|
95
|
+
parseInput,
|
|
96
|
+
normalized,
|
|
97
|
+
hasExplicitTimezone
|
|
98
|
+
});
|
|
99
|
+
return normalized;
|
|
100
|
+
}
|
|
101
|
+
function normalizeDbTimestamp(value) {
|
|
102
|
+
if (value == null) return null;
|
|
103
|
+
if (value instanceof Date) {
|
|
104
|
+
if (Number.isNaN(value.getTime())) return null;
|
|
105
|
+
const utcDate = new Date(
|
|
106
|
+
Date.UTC(
|
|
107
|
+
value.getFullYear(),
|
|
108
|
+
value.getMonth(),
|
|
109
|
+
value.getDate(),
|
|
110
|
+
value.getHours(),
|
|
111
|
+
value.getMinutes(),
|
|
112
|
+
value.getSeconds(),
|
|
113
|
+
value.getMilliseconds()
|
|
114
|
+
)
|
|
115
|
+
);
|
|
116
|
+
return utcDate.toISOString();
|
|
117
|
+
}
|
|
118
|
+
if (typeof value === "string") {
|
|
119
|
+
const trimmed = value.trim();
|
|
120
|
+
if (/^\d{4}-\d{2}-\d{2}[ T]\d{2}:\d{2}:\d{2}(?:\.\d+)?$/.test(trimmed)) {
|
|
121
|
+
const normalized = trimmed.replace(" ", "T");
|
|
122
|
+
return `${normalized}Z`;
|
|
123
|
+
}
|
|
124
|
+
const d = new Date(trimmed);
|
|
125
|
+
if (!Number.isNaN(d.getTime())) {
|
|
126
|
+
return d.toISOString();
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
return String(value);
|
|
130
|
+
}
|
|
131
|
+
function normalizeTimestampsInPayload(value, timezone, currentKey) {
|
|
132
|
+
if (typeof value === "string") {
|
|
133
|
+
const isKnownDateKey = isTimestampFieldKey(currentKey);
|
|
134
|
+
const looksLikeDateValue = ISO_DATE_TIME_REGEX.test(value) || MYSQL_DATE_TIME_REGEX.test(value);
|
|
135
|
+
if (isKnownDateKey || looksLikeDateValue) {
|
|
136
|
+
return normalizeTimestampString(value, timezone);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
if (Array.isArray(value)) {
|
|
140
|
+
return value.map((item) => normalizeTimestampsInPayload(item, timezone));
|
|
141
|
+
}
|
|
142
|
+
if (value && typeof value === "object") {
|
|
143
|
+
const input = value;
|
|
144
|
+
const output = {};
|
|
145
|
+
for (const [key, nested] of Object.entries(input)) {
|
|
146
|
+
output[key] = normalizeTimestampsInPayload(nested, timezone, key);
|
|
147
|
+
}
|
|
148
|
+
return output;
|
|
149
|
+
}
|
|
150
|
+
return value;
|
|
151
|
+
}
|
|
152
|
+
async function withTimezoneFormatting(response) {
|
|
153
|
+
const contentType = response.headers.get("content-type") || "";
|
|
154
|
+
if (!contentType.toLowerCase().includes("application/json")) {
|
|
155
|
+
return response;
|
|
156
|
+
}
|
|
157
|
+
if (response.status >= 400) {
|
|
158
|
+
return response;
|
|
159
|
+
}
|
|
160
|
+
let parsed;
|
|
161
|
+
try {
|
|
162
|
+
parsed = await response.clone().json();
|
|
163
|
+
} catch {
|
|
164
|
+
return response;
|
|
165
|
+
}
|
|
166
|
+
let timezone = null;
|
|
167
|
+
try {
|
|
168
|
+
if (container.has("generalSettingsService")) {
|
|
169
|
+
const svc = container.get(
|
|
170
|
+
"generalSettingsService"
|
|
171
|
+
);
|
|
172
|
+
timezone = await svc.getTimezone();
|
|
173
|
+
}
|
|
174
|
+
} catch (_err) {
|
|
175
|
+
}
|
|
176
|
+
const activeTimezone = timezone ?? null;
|
|
177
|
+
const transformed = normalizeTimestampsInPayload(parsed, activeTimezone);
|
|
178
|
+
const headers = new Headers(response.headers);
|
|
179
|
+
headers.set("Content-Type", "application/json");
|
|
180
|
+
return new Response(JSON.stringify(transformed), {
|
|
181
|
+
status: response.status,
|
|
182
|
+
statusText: response.statusText,
|
|
183
|
+
headers
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// src/shared/base-service.ts
|
|
188
|
+
import { sql } from "drizzle-orm";
|
|
189
|
+
var sqliteBeginImmediate = sql.raw("BEGIN IMMEDIATE");
|
|
190
|
+
var sqliteCommit = sql.raw("COMMIT");
|
|
191
|
+
var sqliteRollback = sql.raw("ROLLBACK");
|
|
192
|
+
var BaseService = class {
|
|
193
|
+
constructor(adapter, logger) {
|
|
194
|
+
this.adapter = adapter;
|
|
195
|
+
this.logger = logger;
|
|
196
|
+
}
|
|
197
|
+
_db = null;
|
|
198
|
+
_tables = null;
|
|
199
|
+
/**
|
|
200
|
+
* Raw Drizzle instance for relational queries (.query.TABLE.findFirst(), etc.).
|
|
201
|
+
* Prefer this.adapter methods for simple CRUD; use this.db only when you need
|
|
202
|
+
* Drizzle's query builder directly (JOINs, relational queries, aggregations).
|
|
203
|
+
*
|
|
204
|
+
* Schema is passed to getDrizzle() so that the relational query API
|
|
205
|
+
* (db.query.users.findFirst, etc.) has access to table definitions.
|
|
206
|
+
* Cached after first access.
|
|
207
|
+
*/
|
|
208
|
+
get db() {
|
|
209
|
+
if (!this._db) {
|
|
210
|
+
this._db = this.adapter.getDrizzle(
|
|
211
|
+
this.tables
|
|
212
|
+
);
|
|
213
|
+
}
|
|
214
|
+
return this._db;
|
|
215
|
+
}
|
|
216
|
+
/**
|
|
217
|
+
* Dialect-specific table schemas resolved from the current adapter.
|
|
218
|
+
* Cached after first access.
|
|
219
|
+
*/
|
|
220
|
+
get tables() {
|
|
221
|
+
if (!this._tables) {
|
|
222
|
+
this._tables = getDialectTables(
|
|
223
|
+
this.adapter.getCapabilities().dialect
|
|
224
|
+
);
|
|
225
|
+
}
|
|
226
|
+
return this._tables;
|
|
227
|
+
}
|
|
228
|
+
/**
|
|
229
|
+
* Get the current database dialect.
|
|
230
|
+
*
|
|
231
|
+
* Use this property for conditional logic when you need database-specific behavior.
|
|
232
|
+
* However, prefer using the adapter's built-in dialect handling when possible.
|
|
233
|
+
*
|
|
234
|
+
* @returns The database dialect: 'postgresql', 'mysql', or 'sqlite'
|
|
235
|
+
*
|
|
236
|
+
* @example
|
|
237
|
+
* ```typescript
|
|
238
|
+
* // Check dialect for conditional logic
|
|
239
|
+
* if (this.dialect === 'postgresql') {
|
|
240
|
+
* // Use PostgreSQL-specific optimization
|
|
241
|
+
* await this.adapter.execute('SELECT ... FOR UPDATE SKIP LOCKED');
|
|
242
|
+
* } else {
|
|
243
|
+
* // Fallback for other databases
|
|
244
|
+
* await this.adapter.select('users', { where: ... });
|
|
245
|
+
* }
|
|
246
|
+
* ```
|
|
247
|
+
*
|
|
248
|
+
* @example
|
|
249
|
+
* ```typescript
|
|
250
|
+
* // Log dialect for debugging
|
|
251
|
+
* this.logger.info(`Running query on ${this.dialect} database`);
|
|
252
|
+
* ```
|
|
253
|
+
*/
|
|
254
|
+
get dialect() {
|
|
255
|
+
return this.adapter.getCapabilities().dialect;
|
|
256
|
+
}
|
|
257
|
+
/**
|
|
258
|
+
* Execute work within a database transaction using Drizzle ORM's fluent API.
|
|
259
|
+
*
|
|
260
|
+
* Transactions ensure ACID properties (Atomicity, Consistency, Isolation, Durability)
|
|
261
|
+
* across multiple database operations. If the callback throws, all changes are rolled
|
|
262
|
+
* back. If it returns, the transaction commits.
|
|
263
|
+
*
|
|
264
|
+
* The `tx` argument is a Drizzle instance that exposes the fluent query API
|
|
265
|
+
* (`tx.insert(table).values(data)`, `tx.update(table).set(data).where(cond)`, etc.).
|
|
266
|
+
* On PostgreSQL and MySQL this is a dialect-specific Drizzle transaction object
|
|
267
|
+
* (`NodePgTransaction` / `MySql2Transaction`). On SQLite it is the shared `this.db`
|
|
268
|
+
* instance, because better-sqlite3's native transaction API cannot run async callbacks
|
|
269
|
+
* — see the SQLite branch below for why.
|
|
270
|
+
*
|
|
271
|
+
* ## Transaction Behavior by Dialect
|
|
272
|
+
*
|
|
273
|
+
* - **PostgreSQL** — routes through Drizzle's native `db.transaction(fn)`. Supports
|
|
274
|
+
* savepoints, isolation levels, and fully async callbacks. tx is a real
|
|
275
|
+
* `NodePgTransaction`.
|
|
276
|
+
* - **MySQL** — same as PostgreSQL via `MySql2Transaction`.
|
|
277
|
+
* - **SQLite** — better-sqlite3's `db.transaction()` rejects any callback that returns
|
|
278
|
+
* a promise (`TypeError: Transaction function cannot return a promise`). Since
|
|
279
|
+
* every Nextly service method is async, we cannot use Drizzle's native SQLite
|
|
280
|
+
* transaction. Instead we open the transaction manually via `BEGIN IMMEDIATE`
|
|
281
|
+
* on the shared connection, run the callback against `this.db`, and COMMIT or
|
|
282
|
+
* ROLLBACK on success/failure. All Drizzle queries against `this.db` during the
|
|
283
|
+
* callback window execute on the same synchronous connection and therefore
|
|
284
|
+
* participate in the BEGIN/COMMIT boundary.
|
|
285
|
+
*
|
|
286
|
+
* ## Why not the adapter's positional `TransactionContext`
|
|
287
|
+
*
|
|
288
|
+
* The adapter's `TransactionContext` (`tx.insert(table: string, data: object)`)
|
|
289
|
+
* builds raw SQL strings internally. Drizzle's fluent API uses the same
|
|
290
|
+
* parameterized query builder as the rest of the codebase, gives schema-based
|
|
291
|
+
* type safety, and is the pattern (db-adapters refactor) standardized on.
|
|
292
|
+
* The positional adapter context is only kept for legacy collection-service code
|
|
293
|
+
* paths that have not yet been migrated.
|
|
294
|
+
*
|
|
295
|
+
* @param work - Async function executed inside the transaction. Receives a
|
|
296
|
+
* Drizzle instance (transaction on PG/MySQL, shared db on SQLite) as `tx`.
|
|
297
|
+
* @returns Promise resolving to the function's return value.
|
|
298
|
+
*
|
|
299
|
+
* @throws {DatabaseError} If the transaction fails or is rolled back.
|
|
300
|
+
*
|
|
301
|
+
* @example Basic insert + insert atomic
|
|
302
|
+
* ```typescript
|
|
303
|
+
* async createUserWithProfile(userData: NewUser, profileData: NewProfile): Promise<User> {
|
|
304
|
+
* return this.withTransaction(async (tx: any) => {
|
|
305
|
+
* const [user] = await tx.insert(this.tables.users).values(userData).returning();
|
|
306
|
+
* await tx.insert(this.tables.profiles).values({ ...profileData, userId: user.id });
|
|
307
|
+
* return user;
|
|
308
|
+
* });
|
|
309
|
+
* }
|
|
310
|
+
* ```
|
|
311
|
+
*
|
|
312
|
+
* @example Update with rollback on validation failure
|
|
313
|
+
* ```typescript
|
|
314
|
+
* async transferCredits(fromId: string, toId: string, amount: number): Promise<void> {
|
|
315
|
+
* await this.withTransaction(async (tx: any) => {
|
|
316
|
+
* await tx.update(this.tables.users)
|
|
317
|
+
* .set({ credits: sql`${this.tables.users.credits} - ${amount}` })
|
|
318
|
+
* .where(eq(this.tables.users.id, fromId));
|
|
319
|
+
*
|
|
320
|
+
* await tx.update(this.tables.users)
|
|
321
|
+
* .set({ credits: sql`${this.tables.users.credits} + ${amount}` })
|
|
322
|
+
* .where(eq(this.tables.users.id, toId));
|
|
323
|
+
*
|
|
324
|
+
* const [sender] = await tx.select().from(this.tables.users)
|
|
325
|
+
* .where(eq(this.tables.users.id, fromId));
|
|
326
|
+
*
|
|
327
|
+
* if (sender.credits < 0) {
|
|
328
|
+
* // Throwing rolls back BOTH updates atomically.
|
|
329
|
+
* throw new Error('Insufficient credits');
|
|
330
|
+
* }
|
|
331
|
+
* });
|
|
332
|
+
* }
|
|
333
|
+
* ```
|
|
334
|
+
*/
|
|
335
|
+
// The tx argument is dialect-typed (NodePgTransaction / MySql2Transaction /
|
|
336
|
+
// BetterSQLite3Database). Importing all three would bind BaseService to all
|
|
337
|
+
// three driver packages and break tree-shaking, so we use `unknown` here and
|
|
338
|
+
// callers narrow with `(tx: any)` if they need fluent chaining. The fluent
|
|
339
|
+
// query API surface is identical across all three so most callers don't need
|
|
340
|
+
// a cast beyond that.
|
|
341
|
+
async withTransaction(work) {
|
|
342
|
+
if (this.dialect === "sqlite") {
|
|
343
|
+
await this.db.run(sqliteBeginImmediate);
|
|
344
|
+
try {
|
|
345
|
+
const result = await work(this.db);
|
|
346
|
+
await this.db.run(sqliteCommit);
|
|
347
|
+
return result;
|
|
348
|
+
} catch (err) {
|
|
349
|
+
try {
|
|
350
|
+
await this.db.run(sqliteRollback);
|
|
351
|
+
} catch {
|
|
352
|
+
}
|
|
353
|
+
throw err;
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
return this.db.transaction(work);
|
|
357
|
+
}
|
|
358
|
+
/**
|
|
359
|
+
* Build a simple WHERE clause for equality comparison.
|
|
360
|
+
*
|
|
361
|
+
* This is a convenience method for the most common WHERE clause pattern.
|
|
362
|
+
* For more complex queries, use `whereAnd()` or build the clause manually.
|
|
363
|
+
*
|
|
364
|
+
* @param column - Column name to filter
|
|
365
|
+
* @param value - Value to match (string, number, boolean, Date, null, or undefined)
|
|
366
|
+
* @returns WHERE clause object
|
|
367
|
+
*
|
|
368
|
+
* @example Basic equality
|
|
369
|
+
* ```typescript
|
|
370
|
+
* const user = await this.adapter.selectOne<User>('users', {
|
|
371
|
+
* where: this.whereEq('email', 'user@example.com'),
|
|
372
|
+
* });
|
|
373
|
+
* ```
|
|
374
|
+
*
|
|
375
|
+
* @example With null value
|
|
376
|
+
* ```typescript
|
|
377
|
+
* const unverifiedUsers = await this.adapter.select<User>('users', {
|
|
378
|
+
* where: this.whereEq('emailVerifiedAt', null),
|
|
379
|
+
* });
|
|
380
|
+
* ```
|
|
381
|
+
*
|
|
382
|
+
* @example In update operation
|
|
383
|
+
* ```typescript
|
|
384
|
+
* await this.adapter.update<User>(
|
|
385
|
+
* 'users',
|
|
386
|
+
* { status: 'active' },
|
|
387
|
+
* this.whereEq('id', userId),
|
|
388
|
+
* { returning: '*' }
|
|
389
|
+
* );
|
|
390
|
+
* ```
|
|
391
|
+
*
|
|
392
|
+
* @example In delete operation
|
|
393
|
+
* ```typescript
|
|
394
|
+
* await this.adapter.delete('sessions', this.whereEq('userId', userId));
|
|
395
|
+
* ```
|
|
396
|
+
*/
|
|
397
|
+
whereEq(column, value) {
|
|
398
|
+
return {
|
|
399
|
+
and: [{ column, op: "=", value }]
|
|
400
|
+
};
|
|
401
|
+
}
|
|
402
|
+
/**
|
|
403
|
+
* Build a WHERE clause with multiple AND conditions.
|
|
404
|
+
*
|
|
405
|
+
* All conditions must be true for a row to match. This is equivalent to
|
|
406
|
+
* SQL: `WHERE column1 = value1 AND column2 = value2 AND ...`
|
|
407
|
+
*
|
|
408
|
+
* For single equality checks, prefer `whereEq()` for simplicity.
|
|
409
|
+
* For OR conditions, build the clause manually using the WhereClause structure.
|
|
410
|
+
*
|
|
411
|
+
* @param conditions - Object mapping column names to their values
|
|
412
|
+
* @returns WHERE clause object with AND conditions
|
|
413
|
+
*
|
|
414
|
+
* @example Multiple filters
|
|
415
|
+
* ```typescript
|
|
416
|
+
* const activeAdmins = await this.adapter.select<User>('users', {
|
|
417
|
+
* where: this.whereAnd({
|
|
418
|
+
* role: 'admin',
|
|
419
|
+
* status: 'active',
|
|
420
|
+
* emailVerified: true,
|
|
421
|
+
* }),
|
|
422
|
+
* });
|
|
423
|
+
* ```
|
|
424
|
+
*
|
|
425
|
+
* @example With null values
|
|
426
|
+
* ```typescript
|
|
427
|
+
* const pendingUsers = await this.adapter.select<User>('users', {
|
|
428
|
+
* where: this.whereAnd({
|
|
429
|
+
* status: 'pending',
|
|
430
|
+
* emailVerifiedAt: null,
|
|
431
|
+
* }),
|
|
432
|
+
* });
|
|
433
|
+
* ```
|
|
434
|
+
*
|
|
435
|
+
* @example Combined with other options
|
|
436
|
+
* ```typescript
|
|
437
|
+
* const results = await this.adapter.select<Document>('documents', {
|
|
438
|
+
* where: this.whereAnd({
|
|
439
|
+
* collectionSlug: 'posts',
|
|
440
|
+
* status: 'published',
|
|
441
|
+
* }),
|
|
442
|
+
* orderBy: [{ column: 'createdAt', direction: 'desc' }],
|
|
443
|
+
* limit: 10,
|
|
444
|
+
* });
|
|
445
|
+
* ```
|
|
446
|
+
*
|
|
447
|
+
* @example In transaction
|
|
448
|
+
* ```typescript
|
|
449
|
+
* await this.withTransaction(async (tx) => {
|
|
450
|
+
* const drafts = await tx.select<Document>('documents', {
|
|
451
|
+
* where: this.whereAnd({
|
|
452
|
+
* userId: currentUserId,
|
|
453
|
+
* status: 'draft',
|
|
454
|
+
* }),
|
|
455
|
+
* });
|
|
456
|
+
*
|
|
457
|
+
* // Process drafts...
|
|
458
|
+
* });
|
|
459
|
+
* ```
|
|
460
|
+
*
|
|
461
|
+
* @example For complex OR conditions, build manually
|
|
462
|
+
* ```typescript
|
|
463
|
+
* // For: WHERE (role = 'admin' AND status = 'active') OR (role = 'superadmin')
|
|
464
|
+
* const complexWhere: WhereClause = {
|
|
465
|
+
* or: [
|
|
466
|
+
* this.whereAnd({ role: 'admin', status: 'active' }),
|
|
467
|
+
* this.whereEq('role', 'superadmin'),
|
|
468
|
+
* ],
|
|
469
|
+
* };
|
|
470
|
+
* const users = await this.adapter.select<User>('users', { where: complexWhere });
|
|
471
|
+
* ```
|
|
472
|
+
*/
|
|
473
|
+
whereAnd(conditions) {
|
|
474
|
+
return {
|
|
475
|
+
and: Object.entries(conditions).map(([column, value]) => ({
|
|
476
|
+
column,
|
|
477
|
+
op: "=",
|
|
478
|
+
value
|
|
479
|
+
}))
|
|
480
|
+
};
|
|
481
|
+
}
|
|
482
|
+
/**
|
|
483
|
+
* Check if the current database supports a specific feature.
|
|
484
|
+
*
|
|
485
|
+
* Use this method to write database-agnostic code that gracefully handles
|
|
486
|
+
* database-specific features. The adapter will automatically provide fallbacks
|
|
487
|
+
* for unsupported features when possible.
|
|
488
|
+
*
|
|
489
|
+
* ## Database Capabilities
|
|
490
|
+
*
|
|
491
|
+
* | Feature | PostgreSQL | MySQL | SQLite |
|
|
492
|
+
* |----------------------|------------|-------|--------|
|
|
493
|
+
* | supportsJsonb | ✅ | ❌ | ❌ |
|
|
494
|
+
* | supportsJson | ✅ | ✅ | ✅ |
|
|
495
|
+
* | supportsArrays | ✅ | ❌ | ❌ |
|
|
496
|
+
* | supportsIlike | ✅ | ❌ | ❌ |
|
|
497
|
+
* | supportsReturning | ✅ | ❌ | ✅ |
|
|
498
|
+
* | supportsSavepoints | ✅ | ❌ | ✅ |
|
|
499
|
+
* | supportsOnConflict | ✅ | ✅ | ✅ |
|
|
500
|
+
* | supportsFts | ✅ | ⚠️ | ❌ |
|
|
501
|
+
*
|
|
502
|
+
* @param feature - Feature name from DatabaseCapabilities
|
|
503
|
+
* @returns True if the database supports the feature
|
|
504
|
+
*
|
|
505
|
+
* @example Case-insensitive search
|
|
506
|
+
* ```typescript
|
|
507
|
+
* async searchByEmail(email: string): Promise<User[]> {
|
|
508
|
+
* if (this.supportsFeature('supportsIlike')) {
|
|
509
|
+
* // PostgreSQL: Use native ILIKE
|
|
510
|
+
* return this.adapter.select<User>('users', {
|
|
511
|
+
* where: { and: [{ column: 'email', op: 'ILIKE', value: `%${email}%` }] },
|
|
512
|
+
* });
|
|
513
|
+
* } else {
|
|
514
|
+
* // MySQL/SQLite: Adapter handles LOWER() LIKE fallback
|
|
515
|
+
* return this.adapter.select<User>('users', {
|
|
516
|
+
* where: { and: [{ column: 'email', op: 'ILIKE', value: `%${email}%` }] },
|
|
517
|
+
* });
|
|
518
|
+
* // Note: Adapter automatically converts ILIKE to LOWER() LIKE for MySQL/SQLite
|
|
519
|
+
* }
|
|
520
|
+
* }
|
|
521
|
+
* ```
|
|
522
|
+
*
|
|
523
|
+
* @example RETURNING clause support
|
|
524
|
+
* ```typescript
|
|
525
|
+
* async updateAndReturn(id: string, data: Partial<User>): Promise<User> {
|
|
526
|
+
* if (this.supportsFeature('supportsReturning')) {
|
|
527
|
+
* // PostgreSQL/SQLite: Use RETURNING
|
|
528
|
+
* const [updated] = await this.adapter.update<User>(
|
|
529
|
+
* 'users',
|
|
530
|
+
* data,
|
|
531
|
+
* this.whereEq('id', id),
|
|
532
|
+
* { returning: '*' }
|
|
533
|
+
* );
|
|
534
|
+
* return updated;
|
|
535
|
+
* } else {
|
|
536
|
+
* // MySQL: Adapter automatically does UPDATE + SELECT
|
|
537
|
+
* const [updated] = await this.adapter.update<User>(
|
|
538
|
+
* 'users',
|
|
539
|
+
* data,
|
|
540
|
+
* this.whereEq('id', id),
|
|
541
|
+
* { returning: '*' }
|
|
542
|
+
* );
|
|
543
|
+
* return updated;
|
|
544
|
+
* // Note: Adapter handles the two-query pattern automatically
|
|
545
|
+
* }
|
|
546
|
+
* }
|
|
547
|
+
* ```
|
|
548
|
+
*
|
|
549
|
+
* @example Savepoint usage
|
|
550
|
+
* ```typescript
|
|
551
|
+
* async complexUpdate(): Promise<void> {
|
|
552
|
+
* await this.withTransaction(async (tx) => {
|
|
553
|
+
* await tx.insert('audit_log', { action: 'started' });
|
|
554
|
+
*
|
|
555
|
+
* if (this.supportsFeature('supportsSavepoints') && tx.savepoint) {
|
|
556
|
+
* await tx.savepoint('before_update');
|
|
557
|
+
*
|
|
558
|
+
* try {
|
|
559
|
+
* await tx.update('sensitive_data', { value: 'new' }, this.whereEq('id', '1'));
|
|
560
|
+
* } catch (error) {
|
|
561
|
+
* await tx.rollbackToSavepoint!('before_update');
|
|
562
|
+
* this.logger.warn('Update failed, rolled back to savepoint');
|
|
563
|
+
* }
|
|
564
|
+
* } else {
|
|
565
|
+
* // MySQL: No savepoints, handle differently
|
|
566
|
+
* await tx.update('sensitive_data', { value: 'new' }, this.whereEq('id', '1'));
|
|
567
|
+
* }
|
|
568
|
+
* });
|
|
569
|
+
* }
|
|
570
|
+
* ```
|
|
571
|
+
*
|
|
572
|
+
* @example JSON/JSONB storage
|
|
573
|
+
* ```typescript
|
|
574
|
+
* async storeMetadata(id: string, metadata: object): Promise<void> {
|
|
575
|
+
* if (this.supportsFeature('supportsJsonb')) {
|
|
576
|
+
* // PostgreSQL: Use JSONB for better performance
|
|
577
|
+
* this.logger.info('Using JSONB column for metadata');
|
|
578
|
+
* } else if (this.supportsFeature('supportsJson')) {
|
|
579
|
+
* // MySQL/SQLite: Use JSON column
|
|
580
|
+
* this.logger.info('Using JSON column for metadata');
|
|
581
|
+
* }
|
|
582
|
+
*
|
|
583
|
+
* await this.adapter.update(
|
|
584
|
+
* 'documents',
|
|
585
|
+
* { metadata: JSON.stringify(metadata) },
|
|
586
|
+
* this.whereEq('id', id)
|
|
587
|
+
* );
|
|
588
|
+
* }
|
|
589
|
+
* ```
|
|
590
|
+
*
|
|
591
|
+
* @example All capabilities
|
|
592
|
+
* ```typescript
|
|
593
|
+
* logDatabaseCapabilities(): void {
|
|
594
|
+
* const caps = this.adapter.getCapabilities();
|
|
595
|
+
* this.logger.info('Database capabilities', {
|
|
596
|
+
* dialect: caps.dialect,
|
|
597
|
+
* jsonb: caps.supportsJsonb,
|
|
598
|
+
* arrays: caps.supportsArrays,
|
|
599
|
+
* ilike: caps.supportsIlike,
|
|
600
|
+
* returning: caps.supportsReturning,
|
|
601
|
+
* savepoints: caps.supportsSavepoints,
|
|
602
|
+
* fts: caps.supportsFts,
|
|
603
|
+
* });
|
|
604
|
+
* }
|
|
605
|
+
* ```
|
|
606
|
+
*/
|
|
607
|
+
supportsFeature(feature) {
|
|
608
|
+
const capabilities = this.adapter.getCapabilities();
|
|
609
|
+
return !!capabilities[feature];
|
|
610
|
+
}
|
|
611
|
+
/**
|
|
612
|
+
* Format a Date for database insertion.
|
|
613
|
+
*
|
|
614
|
+
* MySQL requires datetime in 'YYYY-MM-DD HH:MM:SS' format, while PostgreSQL
|
|
615
|
+
* and SQLite accept ISO 8601 format ('YYYY-MM-DDTHH:MM:SS.sssZ').
|
|
616
|
+
*
|
|
617
|
+
* @param date - Date to format (defaults to current date/time)
|
|
618
|
+
* @returns Formatted date string appropriate for the current database dialect
|
|
619
|
+
*
|
|
620
|
+
* @example
|
|
621
|
+
* ```typescript
|
|
622
|
+
* const now = this.formatDateForDb();
|
|
623
|
+
* await this.adapter.insert('records', { created_at: now });
|
|
624
|
+
* ```
|
|
625
|
+
*/
|
|
626
|
+
formatDateForDb(date = /* @__PURE__ */ new Date()) {
|
|
627
|
+
return date;
|
|
628
|
+
}
|
|
629
|
+
/**
|
|
630
|
+
* Normalize a value from the database into a standard ISO 8601 UTC string.
|
|
631
|
+
*
|
|
632
|
+
* Crucial for dynamic tables (Singles) where the DB driver might parse
|
|
633
|
+
* naive datetime strings using the server's local timezone.
|
|
634
|
+
*
|
|
635
|
+
* @param value - The value from the database (Date, string, or unknown)
|
|
636
|
+
* @returns Optimized ISO string with explicit UTC 'Z' offset
|
|
637
|
+
*/
|
|
638
|
+
normalizeDbTimestamp(value) {
|
|
639
|
+
return normalizeDbTimestamp(value);
|
|
640
|
+
}
|
|
641
|
+
};
|
|
642
|
+
|
|
643
|
+
export {
|
|
644
|
+
normalizeDbTimestamp,
|
|
645
|
+
withTimezoneFormatting,
|
|
646
|
+
BaseService
|
|
647
|
+
};
|