nextly 0.0.1 → 0.0.2-alpha.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.
Files changed (268) hide show
  1. package/LICENSE +22 -0
  2. package/README.md +122 -0
  3. package/dist/_dts-chunks/collections-handler.d-DjgO74Wt.d.ts +20540 -0
  4. package/dist/_dts-chunks/config.d-DNwsDnjs.d.ts +2589 -0
  5. package/dist/_dts-chunks/define-component.d-BUgTHmt3.d.ts +1149 -0
  6. package/dist/_dts-chunks/image-processor.d-OO1PmMrv.d.ts +335 -0
  7. package/dist/_dts-chunks/index.d-axCAzZ7m.d.ts +17842 -0
  8. package/dist/_dts-chunks/media.d-DjDOZo4B.d.ts +117 -0
  9. package/dist/_dts-chunks/on-error.d-CHIKWNxd.d.ts +38 -0
  10. package/dist/_dts-chunks/storage.d-BUhQ2we_.d.ts +404 -0
  11. package/dist/actions/index.d.ts +239 -0
  12. package/dist/actions/index.mjs +281 -0
  13. package/dist/api/auth-state.d.ts +5 -0
  14. package/dist/api/auth-state.mjs +131 -0
  15. package/dist/api/collections-schema-detail.d.ts +56 -0
  16. package/dist/api/collections-schema-detail.mjs +244 -0
  17. package/dist/api/collections-schema-export.d.ts +56 -0
  18. package/dist/api/collections-schema-export.mjs +129 -0
  19. package/dist/api/collections-schema.d.ts +59 -0
  20. package/dist/api/collections-schema.mjs +207 -0
  21. package/dist/api/components-detail.d.ts +50 -0
  22. package/dist/api/components-detail.mjs +132 -0
  23. package/dist/api/components.d.ts +69 -0
  24. package/dist/api/components.mjs +144 -0
  25. package/dist/api/email-providers-default.d.ts +40 -0
  26. package/dist/api/email-providers-default.mjs +75 -0
  27. package/dist/api/email-providers-detail.d.ts +81 -0
  28. package/dist/api/email-providers-detail.mjs +109 -0
  29. package/dist/api/email-providers-test.d.ts +43 -0
  30. package/dist/api/email-providers-test.mjs +114 -0
  31. package/dist/api/email-providers.d.ts +69 -0
  32. package/dist/api/email-providers.mjs +110 -0
  33. package/dist/api/email-send-template.d.ts +41 -0
  34. package/dist/api/email-send-template.mjs +58 -0
  35. package/dist/api/email-send.d.ts +42 -0
  36. package/dist/api/email-send.mjs +58 -0
  37. package/dist/api/email-templates-detail.d.ts +74 -0
  38. package/dist/api/email-templates-detail.mjs +112 -0
  39. package/dist/api/email-templates-layout.d.ts +55 -0
  40. package/dist/api/email-templates-layout.mjs +92 -0
  41. package/dist/api/email-templates-preview.d.ts +48 -0
  42. package/dist/api/email-templates-preview.mjs +93 -0
  43. package/dist/api/email-templates.d.ts +61 -0
  44. package/dist/api/email-templates.mjs +118 -0
  45. package/dist/api/health.d.ts +68 -0
  46. package/dist/api/health.mjs +67 -0
  47. package/dist/api/index.d.ts +54 -0
  48. package/dist/api/index.mjs +16 -0
  49. package/dist/api/media-bulk.d.ts +74 -0
  50. package/dist/api/media-bulk.mjs +196 -0
  51. package/dist/api/media-folders.d.ts +112 -0
  52. package/dist/api/media-folders.mjs +187 -0
  53. package/dist/api/media-handlers.d.ts +102 -0
  54. package/dist/api/media-handlers.mjs +437 -0
  55. package/dist/api/media.d.ts +117 -0
  56. package/dist/api/media.mjs +242 -0
  57. package/dist/api/singles-detail.d.ts +87 -0
  58. package/dist/api/singles-detail.mjs +170 -0
  59. package/dist/api/singles-schema-detail.d.ts +54 -0
  60. package/dist/api/singles-schema-detail.mjs +182 -0
  61. package/dist/api/singles.d.ts +34 -0
  62. package/dist/api/singles.mjs +94 -0
  63. package/dist/api/storage-upload-url.d.ts +48 -0
  64. package/dist/api/storage-upload-url.mjs +202 -0
  65. package/dist/api/uploads.d.ts +109 -0
  66. package/dist/api/uploads.mjs +359 -0
  67. package/dist/auth/index.d.ts +425 -0
  68. package/dist/auth/index.mjs +199 -0
  69. package/dist/boot-apply-PQSYLDIN.mjs +7 -0
  70. package/dist/chunk-2OALJTK6.mjs +489 -0
  71. package/dist/chunk-2Q2SX2CS.mjs +365 -0
  72. package/dist/chunk-2TFX4ND3.mjs +13 -0
  73. package/dist/chunk-2TWPDSYD.mjs +87 -0
  74. package/dist/chunk-2W3DVD7S.mjs +647 -0
  75. package/dist/chunk-2ZFKXPQM.mjs +88 -0
  76. package/dist/chunk-3FA7FKAV.mjs +832 -0
  77. package/dist/chunk-3NZ2KMBL.mjs +58 -0
  78. package/dist/chunk-4MJLT6PZ.mjs +0 -0
  79. package/dist/chunk-56WO4WX7.mjs +0 -0
  80. package/dist/chunk-5APFUGAD.mjs +89 -0
  81. package/dist/chunk-5HMZ644B.mjs +108 -0
  82. package/dist/chunk-67GXH6PR.mjs +32 -0
  83. package/dist/chunk-6JNEPWRW.mjs +14368 -0
  84. package/dist/chunk-6NFHQIJD.mjs +45 -0
  85. package/dist/chunk-7P6ASYW6.mjs +9 -0
  86. package/dist/chunk-A3WPLSDT.mjs +1364 -0
  87. package/dist/chunk-AGJ6F2T3.mjs +144 -0
  88. package/dist/chunk-AK6Z23OX.mjs +1464 -0
  89. package/dist/chunk-APKKRD2G.mjs +102 -0
  90. package/dist/chunk-B2GV2BWH.mjs +73 -0
  91. package/dist/chunk-D5HQBNUB.mjs +74 -0
  92. package/dist/chunk-DNNG377Z.mjs +204 -0
  93. package/dist/chunk-DP3G27G5.mjs +135 -0
  94. package/dist/chunk-DV6WVX2Q.mjs +0 -0
  95. package/dist/chunk-DXGGXIUZ.mjs +57 -0
  96. package/dist/chunk-EGXBZCGC.mjs +943 -0
  97. package/dist/chunk-ERCNLX3V.mjs +176 -0
  98. package/dist/chunk-FQULBZ53.mjs +850 -0
  99. package/dist/chunk-G2AA4QLC.mjs +262 -0
  100. package/dist/chunk-GDBJ5JCU.mjs +488 -0
  101. package/dist/chunk-GJNSJU4S.mjs +19 -0
  102. package/dist/chunk-GZ6DCQKC.mjs +69 -0
  103. package/dist/chunk-H26B4FYG.mjs +167 -0
  104. package/dist/chunk-I4JMR3UR.mjs +21 -0
  105. package/dist/chunk-INV7QKLG.mjs +508 -0
  106. package/dist/chunk-IUDOC7N7.mjs +46 -0
  107. package/dist/chunk-IZWPRDC3.mjs +206 -0
  108. package/dist/chunk-KIMNCZGV.mjs +15 -0
  109. package/dist/chunk-L6HW2DA7.mjs +15 -0
  110. package/dist/chunk-LAZXX4HR.mjs +100 -0
  111. package/dist/chunk-LDKCUMHK.mjs +95 -0
  112. package/dist/chunk-LRXMECUA.mjs +0 -0
  113. package/dist/chunk-M52VMPGA.mjs +119 -0
  114. package/dist/chunk-MGUWEEI6.mjs +160 -0
  115. package/dist/chunk-NRUWQ5Z7.mjs +419 -0
  116. package/dist/chunk-NSEFNNU4.mjs +25360 -0
  117. package/dist/chunk-NTHVDFGO.mjs +138 -0
  118. package/dist/chunk-O3QHXMOX.mjs +3166 -0
  119. package/dist/chunk-P7NH2OSC.mjs +2605 -0
  120. package/dist/chunk-PKMABBB5.mjs +184 -0
  121. package/dist/chunk-PWS6XGJK.mjs +76 -0
  122. package/dist/chunk-R6JJQHFC.mjs +20 -0
  123. package/dist/chunk-RJLLGGPG.mjs +0 -0
  124. package/dist/chunk-SBACDPNX.mjs +689 -0
  125. package/dist/chunk-TO5AFLVQ.mjs +124 -0
  126. package/dist/chunk-TS7GHTG2.mjs +5436 -0
  127. package/dist/chunk-UJ2IMJ4W.mjs +133 -0
  128. package/dist/chunk-UOP63Q54.mjs +102 -0
  129. package/dist/chunk-UUOFWCM6.mjs +78 -0
  130. package/dist/chunk-V4EQTOA4.mjs +893 -0
  131. package/dist/chunk-VJ66NCL4.mjs +193 -0
  132. package/dist/chunk-VQJQHVEV.mjs +29 -0
  133. package/dist/chunk-VTJADRO3.mjs +141 -0
  134. package/dist/chunk-VWF3JO32.mjs +0 -0
  135. package/dist/chunk-W4MGXIRR.mjs +27 -0
  136. package/dist/chunk-W5KKPZT5.mjs +1204 -0
  137. package/dist/chunk-WD34YQ6T.mjs +381 -0
  138. package/dist/chunk-WZBYMYVW.mjs +14 -0
  139. package/dist/chunk-X23WKS3Z.mjs +50 -0
  140. package/dist/chunk-X7TXCYYN.mjs +6496 -0
  141. package/dist/chunk-XGI4EMS3.mjs +140 -0
  142. package/dist/chunk-XZKLBMN6.mjs +1153 -0
  143. package/dist/chunk-YB7INWPY.mjs +0 -0
  144. package/dist/chunk-YV4Y7SDL.mjs +83 -0
  145. package/dist/chunk-YZNBLFIW.mjs +1688 -0
  146. package/dist/chunk-YZZCTONM.mjs +263 -0
  147. package/dist/chunk-ZE6A3FYH.mjs +289 -0
  148. package/dist/cli/nextly.mjs +68 -0
  149. package/dist/cli/utils/index.d.ts +449 -0
  150. package/dist/cli/utils/index.mjs +49 -0
  151. package/dist/component-schema-service-5577KVW6.mjs +11 -0
  152. package/dist/config-loader-23YEMC3Z.mjs +23 -0
  153. package/dist/config.d.ts +44 -0
  154. package/dist/config.mjs +109 -0
  155. package/dist/container-ORGFGYSZ.mjs +9 -0
  156. package/dist/database/index.d.ts +12 -0
  157. package/dist/database/index.mjs +40 -0
  158. package/dist/database/seeders/index.d.ts +93 -0
  159. package/dist/database/seeders/index.mjs +47 -0
  160. package/dist/db-sync-demote-LJGKLB3S.mjs +117 -0
  161. package/dist/db-sync-promote-B26VSYQF.mjs +113 -0
  162. package/dist/dev-reload-broadcaster-B73IQ53V.mjs +25 -0
  163. package/dist/dist-M2NOU37V.mjs +19 -0
  164. package/dist/drizzle-kit-lazy-D2M2PXR2.mjs +13 -0
  165. package/dist/dynamic-collection-schema-service-IEXTPIZ7.mjs +8 -0
  166. package/dist/errors/index.d.ts +159 -0
  167. package/dist/errors/index.mjs +10 -0
  168. package/dist/factory-IWMBKUJM.mjs +15 -0
  169. package/dist/first-run-QIVKWJIF.mjs +63 -0
  170. package/dist/fresh-push-NR67DC3R.mjs +8 -0
  171. package/dist/index.d.ts +4175 -0
  172. package/dist/index.mjs +1336 -0
  173. package/dist/local-plugin-PTET4NAT.mjs +7 -0
  174. package/dist/logger-NU46DXNY.mjs +15 -0
  175. package/dist/logger-YE4TC7ZN.mjs +9 -0
  176. package/dist/migration-journal-EP532Y4L.mjs +139 -0
  177. package/dist/migrations/mysql/0000_eager_sentry.sql +174 -0
  178. package/dist/migrations/mysql/0001_soft_giant_girl.sql +27 -0
  179. package/dist/migrations/mysql/0002_media_table.sql +24 -0
  180. package/dist/migrations/mysql/0003_dynamic_singles.sql +37 -0
  181. package/dist/migrations/mysql/0004_dynamic_components.sql +35 -0
  182. package/dist/migrations/mysql/0005_user_management_tables.sql +92 -0
  183. package/dist/migrations/mysql/0006_api_keys.sql +36 -0
  184. package/dist/migrations/mysql/0007_general_settings.sql +20 -0
  185. package/dist/migrations/mysql/0008_site_settings_logo_url.sql +9 -0
  186. package/dist/migrations/mysql/0009_activity_log.sql +30 -0
  187. package/dist/migrations/mysql/0010_site_settings_sidebar.sql +13 -0
  188. package/dist/migrations/mysql/0011_missing_tables_and_columns.sql +54 -0
  189. package/dist/migrations/mysql/0012_image_sizes_and_focal_point.sql +30 -0
  190. package/dist/migrations/mysql/0012_media_folders.sql +43 -0
  191. package/dist/migrations/mysql/0013_user_brute_force_protection.sql +31 -0
  192. package/dist/migrations/mysql/0014_email_template_attachments.sql +12 -0
  193. package/dist/migrations/mysql/0015_media_uploaded_by_nullable.sql +15 -0
  194. package/dist/migrations/mysql/20260429_000000_000_initial_journal.sql +22 -0
  195. package/dist/migrations/mysql/20260501_000000_journal_batch.sql +17 -0
  196. package/dist/migrations/mysql/20260501_000001_audit_log.sql +24 -0
  197. package/dist/migrations/mysql/20260504_000000_nextly_meta.sql +21 -0
  198. package/dist/migrations/mysql/meta/0000_snapshot.json +1005 -0
  199. package/dist/migrations/mysql/meta/0001_snapshot.json +1099 -0
  200. package/dist/migrations/mysql/meta/_journal.json +41 -0
  201. package/dist/migrations/postgresql/0000_misty_king_bedlam.sql +169 -0
  202. package/dist/migrations/postgresql/0001_perpetual_captain_marvel.sql +8 -0
  203. package/dist/migrations/postgresql/0002_sad_spectrum.sql +16 -0
  204. package/dist/migrations/postgresql/0003_hesitant_ultron.sql +17 -0
  205. package/dist/migrations/postgresql/0004_media_table.sql +24 -0
  206. package/dist/migrations/postgresql/0005_media_folders.sql +36 -0
  207. package/dist/migrations/postgresql/0006_dynamic_collections_update.sql +50 -0
  208. package/dist/migrations/postgresql/0007_dynamic_singles.sql +38 -0
  209. package/dist/migrations/postgresql/0008_dynamic_components.sql +37 -0
  210. package/dist/migrations/postgresql/0009_user_management_tables.sql +95 -0
  211. package/dist/migrations/postgresql/0010_api_keys.sql +34 -0
  212. package/dist/migrations/postgresql/0011_general_settings.sql +20 -0
  213. package/dist/migrations/postgresql/0012_site_settings_logo_url.sql +9 -0
  214. package/dist/migrations/postgresql/0013_activity_log.sql +29 -0
  215. package/dist/migrations/postgresql/0014_image_sizes_and_focal_point.sql +33 -0
  216. package/dist/migrations/postgresql/0014_site_settings_sidebar.sql +13 -0
  217. package/dist/migrations/postgresql/0015_user_brute_force_protection.sql +29 -0
  218. package/dist/migrations/postgresql/0016_email_template_attachments.sql +12 -0
  219. package/dist/migrations/postgresql/0017_media_uploaded_by_nullable.sql +15 -0
  220. package/dist/migrations/postgresql/20260429_000000_000_initial_journal.sql +24 -0
  221. package/dist/migrations/postgresql/20260501_000000_journal_batch.sql +17 -0
  222. package/dist/migrations/postgresql/20260501_000001_audit_log.sql +24 -0
  223. package/dist/migrations/postgresql/20260504_000000_nextly_meta.sql +22 -0
  224. package/dist/migrations/postgresql/meta/0000_snapshot.json +1286 -0
  225. package/dist/migrations/postgresql/meta/0001_snapshot.json +1407 -0
  226. package/dist/migrations/postgresql/meta/0002_snapshot.json +1552 -0
  227. package/dist/migrations/postgresql/meta/0003_snapshot.json +1695 -0
  228. package/dist/migrations/postgresql/meta/0010_snapshot.json +2345 -0
  229. package/dist/migrations/postgresql/meta/_journal.json +90 -0
  230. package/dist/migrations/sqlite/0000_api_keys.sql +34 -0
  231. package/dist/migrations/sqlite/0001_general_settings.sql +20 -0
  232. package/dist/migrations/sqlite/0002_site_settings_logo_url.sql +9 -0
  233. package/dist/migrations/sqlite/0003_activity_log.sql +29 -0
  234. package/dist/migrations/sqlite/0004_image_sizes_and_focal_point.sql +29 -0
  235. package/dist/migrations/sqlite/0004_site_settings_sidebar.sql +11 -0
  236. package/dist/migrations/sqlite/0005_user_brute_force_protection.sql +29 -0
  237. package/dist/migrations/sqlite/0006_email_template_attachments.sql +12 -0
  238. package/dist/migrations/sqlite/0007_media_uploaded_by_nullable.sql +111 -0
  239. package/dist/migrations/sqlite/20260429_000000_000_initial_journal.sql +24 -0
  240. package/dist/migrations/sqlite/20260501_000000_journal_batch.sql +19 -0
  241. package/dist/migrations/sqlite/20260501_000001_audit_log.sql +24 -0
  242. package/dist/migrations/sqlite/20260504_000000_nextly_meta.sql +21 -0
  243. package/dist/migrations/sqlite/20260505_000000_user_management_tables.sql +77 -0
  244. package/dist/next.d.ts +57 -0
  245. package/dist/next.mjs +55 -0
  246. package/dist/observability/index.d.ts +87 -0
  247. package/dist/observability/index.mjs +57 -0
  248. package/dist/permissions-3DZZQZMI.mjs +39 -0
  249. package/dist/pipeline-YOML7SWF.mjs +29 -0
  250. package/dist/preview-ZZTR3QGS.mjs +9 -0
  251. package/dist/program-PW6UB2ZC.mjs +5934 -0
  252. package/dist/reconcile-single-tables-7ENVXJGB.mjs +7 -0
  253. package/dist/register-SF6E6FVU.mjs +49 -0
  254. package/dist/reload-config-HWQ4G5MM.mjs +23 -0
  255. package/dist/resolve-single-table-name-JSOMUB3R.mjs +7 -0
  256. package/dist/routeHandler-UNMMJIBM.mjs +77 -0
  257. package/dist/runtime-schema-generator-NRA6A6Z6.mjs +8 -0
  258. package/dist/runtime.d.ts +120 -0
  259. package/dist/runtime.mjs +73 -0
  260. package/dist/schema-hash-FMMG6VPJ.mjs +13 -0
  261. package/dist/schema-registry-EQ36FZDP.mjs +7 -0
  262. package/dist/scripts/load-env.mjs +42 -0
  263. package/dist/storage/index.d.ts +566 -0
  264. package/dist/storage/index.mjs +45 -0
  265. package/dist/super-admin-G5ZK5F4T.mjs +39 -0
  266. package/dist/system-table-service-WGSRVEGT.mjs +17 -0
  267. package/dist/users-7KELGRYJ.mjs +38 -0
  268. 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
+ };