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,832 @@
1
+ import {
2
+ calculateSchemaHash,
3
+ schemaHashesMatch
4
+ } from "./chunk-5HMZ644B.mjs";
5
+ import {
6
+ BaseService
7
+ } from "./chunk-2W3DVD7S.mjs";
8
+ import {
9
+ NextlyError,
10
+ toDbError
11
+ } from "./chunk-NRUWQ5Z7.mjs";
12
+
13
+ // src/services/lib/resource-slug-guard.ts
14
+ async function findSlugOwner(adapter, slug) {
15
+ const collection = await adapter.selectOne(
16
+ "dynamic_collections",
17
+ {
18
+ where: { and: [{ column: "slug", op: "=", value: slug }] },
19
+ columns: ["id"]
20
+ }
21
+ );
22
+ if (collection?.id) {
23
+ return {
24
+ resourceType: "collection",
25
+ id: collection.id
26
+ };
27
+ }
28
+ const single = await adapter.selectOne("dynamic_singles", {
29
+ where: { and: [{ column: "slug", op: "=", value: slug }] },
30
+ columns: ["id"]
31
+ });
32
+ if (single?.id) {
33
+ return {
34
+ resourceType: "single",
35
+ id: single.id
36
+ };
37
+ }
38
+ return null;
39
+ }
40
+ async function assertGlobalResourceSlugAvailable(adapter, slug, options) {
41
+ const owner = await findSlugOwner(adapter, slug);
42
+ if (!owner) {
43
+ return;
44
+ }
45
+ const isSameResource = owner.resourceType === options?.currentResourceType && owner.id === options?.currentResourceId;
46
+ if (isSameResource) {
47
+ return;
48
+ }
49
+ throw NextlyError.duplicate({
50
+ logContext: {
51
+ slug,
52
+ conflictResourceType: owner.resourceType,
53
+ conflictResourceId: owner.id
54
+ }
55
+ });
56
+ }
57
+
58
+ // src/shared/base-registry-service.ts
59
+ import crypto from "crypto";
60
+ var BaseRegistryService = class extends BaseService {
61
+ constructor(adapter, logger) {
62
+ super(adapter, logger);
63
+ }
64
+ // ============================================================
65
+ // Shared Query Methods
66
+ // ============================================================
67
+ /**
68
+ * Get a record by slug, returning null if not found.
69
+ */
70
+ async getRecordBySlug(slug) {
71
+ try {
72
+ const result = await this.adapter.selectOne(
73
+ this.registryTableName,
74
+ {
75
+ where: this.whereEq("slug", slug)
76
+ }
77
+ );
78
+ return result ? this.deserializeRecord(result) : null;
79
+ } catch (error) {
80
+ if (NextlyError.is(error)) throw error;
81
+ throw NextlyError.fromDatabaseError(toDbError(this.dialect, error));
82
+ }
83
+ }
84
+ /**
85
+ * Get a record by slug, throwing NOT_FOUND if missing.
86
+ *
87
+ * §13.8: public message is generic; identifying details (slug, resource
88
+ * type) flow through `logContext`, not the wire.
89
+ */
90
+ async getRecordOrThrow(slug) {
91
+ const record = await this.getRecordBySlug(slug);
92
+ if (!record) {
93
+ throw NextlyError.notFound({
94
+ logContext: { entity: this.resourceType, slug }
95
+ });
96
+ }
97
+ return record;
98
+ }
99
+ /**
100
+ * Get all records, optionally filtered by source, migration status, and locked.
101
+ */
102
+ async getAllRecords(options) {
103
+ try {
104
+ const conditions = this.buildFilterConditions(options);
105
+ const results = await this.adapter.select(
106
+ this.registryTableName,
107
+ {
108
+ where: conditions.length > 0 ? { and: conditions } : void 0,
109
+ orderBy: [{ column: "created_at", direction: "asc" }],
110
+ limit: options?.limit,
111
+ offset: options?.offset
112
+ }
113
+ );
114
+ return results.map((record) => this.deserializeRecord(record));
115
+ } catch (error) {
116
+ if (NextlyError.is(error)) throw error;
117
+ throw NextlyError.fromDatabaseError(toDbError(this.dialect, error));
118
+ }
119
+ }
120
+ /**
121
+ * List records with pagination, search, and total count.
122
+ */
123
+ async listRecords(options) {
124
+ try {
125
+ const conditions = this.buildFilterConditions(options);
126
+ if (options?.search) {
127
+ const searchPattern = `%${options.search}%`;
128
+ const searchColumns = this.getSearchColumns();
129
+ conditions.push({
130
+ or: searchColumns.map((column) => ({
131
+ column,
132
+ op: "ILIKE",
133
+ value: searchPattern
134
+ }))
135
+ });
136
+ }
137
+ const whereClause = conditions.length > 0 ? { and: conditions } : void 0;
138
+ const allResults = await this.adapter.select(
139
+ this.registryTableName,
140
+ {
141
+ where: whereClause,
142
+ columns: ["id"]
143
+ }
144
+ );
145
+ const total = allResults.length;
146
+ const results = await this.adapter.select(
147
+ this.registryTableName,
148
+ {
149
+ where: whereClause,
150
+ orderBy: [{ column: "created_at", direction: "asc" }],
151
+ limit: options?.limit,
152
+ offset: options?.offset
153
+ }
154
+ );
155
+ return {
156
+ data: results.map((record) => this.deserializeRecord(record)),
157
+ total
158
+ };
159
+ } catch (error) {
160
+ if (NextlyError.is(error)) throw error;
161
+ throw NextlyError.fromDatabaseError(toDbError(this.dialect, error));
162
+ }
163
+ }
164
+ // ============================================================
165
+ // Shared Locking & Migration
166
+ // ============================================================
167
+ /**
168
+ * Check if a record is locked (code-first resources are locked).
169
+ */
170
+ async checkIsLocked(slug) {
171
+ const record = await this.getRecordBySlug(slug);
172
+ return record?.locked ?? false;
173
+ }
174
+ /**
175
+ * Update migration status for a record.
176
+ */
177
+ async updateRecordMigrationStatus(slug, status, migrationId) {
178
+ this.logger.debug("Updating migration status", { slug, status });
179
+ try {
180
+ const updateData = {
181
+ migration_status: status,
182
+ updated_at: this.formatDateForDb()
183
+ };
184
+ if (migrationId) {
185
+ updateData.last_migration_id = migrationId;
186
+ }
187
+ const results = await this.adapter.update(
188
+ this.registryTableName,
189
+ updateData,
190
+ this.whereEq("slug", slug),
191
+ { returning: "*" }
192
+ );
193
+ if (results.length === 0) {
194
+ throw NextlyError.notFound({
195
+ logContext: { entity: this.resourceType, slug }
196
+ });
197
+ }
198
+ this.logger.info("Migration status updated", { slug, status });
199
+ } catch (error) {
200
+ if (NextlyError.is(error)) throw error;
201
+ throw NextlyError.fromDatabaseError(toDbError(this.dialect, error));
202
+ }
203
+ }
204
+ /**
205
+ * Safely update migration status to 'applied' with table existence verification.
206
+ *
207
+ * CRITICAL: Use this instead of updateRecordMigrationStatus when setting status
208
+ * to 'applied' to prevent the race condition where status is marked as 'applied'
209
+ * but the table doesn't actually exist.
210
+ */
211
+ async updateMigrationStatusWithTableVerification(slug, tableName) {
212
+ this.logger.debug("Updating migration status with verification", {
213
+ slug,
214
+ tableName
215
+ });
216
+ try {
217
+ const tableExists = await this.adapter.tableExists(tableName);
218
+ if (tableExists) {
219
+ await this.updateRecordMigrationStatus(
220
+ slug,
221
+ "applied"
222
+ );
223
+ this.logger.info("Table verified, migration status set to 'applied'", {
224
+ slug,
225
+ tableName
226
+ });
227
+ return { verified: true, status: "applied" };
228
+ } else {
229
+ await this.updateRecordMigrationStatus(
230
+ slug,
231
+ "failed"
232
+ );
233
+ this.logger.error(
234
+ "Table verification failed - migration status set to 'failed'",
235
+ { slug, tableName }
236
+ );
237
+ return { verified: false, status: "failed" };
238
+ }
239
+ } catch (error) {
240
+ if (NextlyError.is(error)) throw error;
241
+ throw NextlyError.fromDatabaseError(toDbError(this.dialect, error));
242
+ }
243
+ }
244
+ /**
245
+ * Get all records with pending migrations (status 'pending' or 'generated').
246
+ */
247
+ async getRecordsWithPendingMigrations() {
248
+ try {
249
+ const results = await this.adapter.select(
250
+ this.registryTableName,
251
+ {
252
+ where: {
253
+ and: [
254
+ {
255
+ column: "migration_status",
256
+ op: "IN",
257
+ value: ["pending", "generated"]
258
+ }
259
+ ]
260
+ },
261
+ orderBy: [{ column: "created_at", direction: "asc" }]
262
+ }
263
+ );
264
+ return results.map((record) => this.deserializeRecord(record));
265
+ } catch (error) {
266
+ if (NextlyError.is(error)) throw error;
267
+ throw NextlyError.fromDatabaseError(toDbError(this.dialect, error));
268
+ }
269
+ }
270
+ // ============================================================
271
+ // Shared Utilities
272
+ // ============================================================
273
+ /**
274
+ * Generate a unique ID using crypto.randomUUID().
275
+ */
276
+ generateId() {
277
+ return crypto.randomUUID();
278
+ }
279
+ /**
280
+ * Compute a simple hash from a string (for auto-generating schema_hash).
281
+ * Uses a fast DJB2-style hash — not cryptographic, just for change detection.
282
+ */
283
+ computeSimpleHash(input) {
284
+ let hash = 5381;
285
+ for (let i = 0; i < input.length; i++) {
286
+ hash = (hash << 5) + hash + input.charCodeAt(i) | 0;
287
+ }
288
+ return (hash >>> 0).toString(16).padStart(8, "0");
289
+ }
290
+ /**
291
+ * Generate a table name from a slug.
292
+ * Converts slug to snake_case, removes invalid characters, and adds the domain prefix.
293
+ */
294
+ generateTableName(slug) {
295
+ const normalized = slug.toLowerCase().replace(/[^a-z0-9]+/g, "_").replace(/^_+|_+$/g, "");
296
+ return `${this.tableNamePrefix}${normalized}`;
297
+ }
298
+ /**
299
+ * Ensure table name has the domain-specific prefix.
300
+ */
301
+ ensureTableNamePrefix(tableName) {
302
+ if (tableName.startsWith(this.tableNamePrefix)) {
303
+ return tableName;
304
+ }
305
+ return `${this.tableNamePrefix}${tableName}`;
306
+ }
307
+ /**
308
+ * Check if admin config has changed between code and database.
309
+ * Uses JSON comparison to detect changes in admin properties.
310
+ */
311
+ adminConfigChanged(codeAdmin, existingAdmin) {
312
+ if (!codeAdmin && !existingAdmin) {
313
+ return false;
314
+ }
315
+ if (!codeAdmin || !existingAdmin) {
316
+ return true;
317
+ }
318
+ return JSON.stringify(codeAdmin) !== JSON.stringify(existingAdmin);
319
+ }
320
+ // ============================================================
321
+ // Private Helpers
322
+ // ============================================================
323
+ /**
324
+ * Build WHERE conditions for source, migrationStatus, and locked filters.
325
+ * Returns a mutable array so callers can add additional conditions (e.g., search).
326
+ */
327
+ buildFilterConditions(options) {
328
+ const conditions = [];
329
+ if (options?.source) {
330
+ conditions.push({
331
+ column: "source",
332
+ op: "=",
333
+ value: options.source
334
+ });
335
+ }
336
+ if (options?.migrationStatus) {
337
+ conditions.push({
338
+ column: "migration_status",
339
+ op: "=",
340
+ value: options.migrationStatus
341
+ });
342
+ }
343
+ if (options?.locked !== void 0) {
344
+ conditions.push({
345
+ column: "locked",
346
+ op: "=",
347
+ value: options.locked
348
+ });
349
+ }
350
+ return conditions;
351
+ }
352
+ };
353
+
354
+ // src/domains/collections/services/collection-registry-service.ts
355
+ function describeError(error) {
356
+ if (NextlyError.is(error)) {
357
+ const parts = [];
358
+ parts.push(`[${error.code}] ${error.message}`);
359
+ if (error.cause instanceof Error && error.cause.message) {
360
+ parts.push(`cause: ${error.cause.message}`);
361
+ } else if (typeof error.cause === "string") {
362
+ parts.push(`cause: ${error.cause}`);
363
+ }
364
+ const ctx = error.logContext;
365
+ if (ctx && typeof ctx === "object" && Object.keys(ctx).length > 0) {
366
+ try {
367
+ parts.push(`context: ${JSON.stringify(ctx)}`);
368
+ } catch {
369
+ }
370
+ }
371
+ return parts.join(" | ");
372
+ }
373
+ if (error instanceof Error) {
374
+ return error.message;
375
+ }
376
+ return String(error);
377
+ }
378
+ var CollectionRegistryService = class extends BaseRegistryService {
379
+ registryTableName = "dynamic_collections";
380
+ resourceType = "Collection";
381
+ tableNamePrefix = "dc_";
382
+ permissionSeedService;
383
+ constructor(adapter, logger) {
384
+ super(adapter, logger);
385
+ }
386
+ getSearchColumns() {
387
+ return ["slug"];
388
+ }
389
+ /** Set the PermissionSeedService for auto-seeding permissions on collection sync. */
390
+ setPermissionSeedService(service) {
391
+ this.permissionSeedService = service;
392
+ }
393
+ async getCollectionBySlug(slug) {
394
+ return this.getRecordBySlug(slug);
395
+ }
396
+ async getCollection(slug) {
397
+ return this.getRecordOrThrow(slug);
398
+ }
399
+ async getAllCollections(options) {
400
+ return this.getAllRecords(options);
401
+ }
402
+ async listCollections(options) {
403
+ return this.listRecords(options);
404
+ }
405
+ async isLocked(slug) {
406
+ return this.checkIsLocked(slug);
407
+ }
408
+ async updateMigrationStatus(slug, status, migrationId) {
409
+ return this.updateRecordMigrationStatus(slug, status, migrationId);
410
+ }
411
+ async updateMigrationStatusWithVerification(slug, tableName) {
412
+ return this.updateMigrationStatusWithTableVerification(slug, tableName);
413
+ }
414
+ async getPendingMigrations() {
415
+ return this.getRecordsWithPendingMigrations();
416
+ }
417
+ async registerCollection(data) {
418
+ this.logger.debug("Registering collection", { slug: data.slug });
419
+ await assertGlobalResourceSlugAvailable(this.adapter, data.slug);
420
+ const existing = await this.getCollectionBySlug(data.slug);
421
+ if (existing) {
422
+ throw NextlyError.duplicate({
423
+ logContext: { reason: "collection-slug-conflict", slug: data.slug }
424
+ });
425
+ }
426
+ const now = this.formatDateForDb();
427
+ const tableName = this.ensureTableNamePrefix(data.tableName);
428
+ const fieldsJson = JSON.stringify(data.fields);
429
+ const schemaHash = data.schemaHash ?? this.computeSimpleHash(fieldsJson);
430
+ const record = {
431
+ id: this.generateId(),
432
+ slug: data.slug,
433
+ labels: JSON.stringify(data.labels),
434
+ table_name: tableName,
435
+ description: data.description,
436
+ fields: fieldsJson,
437
+ timestamps: data.timestamps ?? true ? 1 : 0,
438
+ admin: data.admin ? JSON.stringify(data.admin) : null,
439
+ source: data.source,
440
+ locked: data.locked ?? data.source === "code" ? 1 : 0,
441
+ // Persist Draft/Published flag. Stored as 0/1 to match how
442
+ // `timestamps` and `locked` are written in this code path; the
443
+ // SQLite/postgres/mysql Drizzle column types accept either form.
444
+ status: data.status === true ? 1 : 0,
445
+ config_path: data.configPath,
446
+ schema_hash: schemaHash,
447
+ schema_version: data.schemaVersion ?? 1,
448
+ migration_status: data.migrationStatus ?? "pending",
449
+ last_migration_id: data.lastMigrationId,
450
+ created_by: data.createdBy,
451
+ hooks: data.hooks ? JSON.stringify(data.hooks) : null,
452
+ created_at: now,
453
+ updated_at: now
454
+ };
455
+ try {
456
+ const result = await this.adapter.insert(
457
+ this.registryTableName,
458
+ record,
459
+ { returning: "*" }
460
+ );
461
+ this.logger.info("Collection registered", {
462
+ slug: data.slug,
463
+ source: data.source
464
+ });
465
+ const deserializedRecord = this.deserializeRecord(result);
466
+ if (this.permissionSeedService && data.slug) {
467
+ try {
468
+ const permResult = await this.permissionSeedService.seedCollectionPermissions(
469
+ data.slug
470
+ );
471
+ if (permResult.newPermissionIds?.length > 0) {
472
+ await this.permissionSeedService.assignNewPermissionsToSuperAdmin(
473
+ permResult.newPermissionIds
474
+ );
475
+ }
476
+ this.logger.info("Collection permissions seeded", {
477
+ slug: data.slug,
478
+ created: permResult.created,
479
+ total: permResult.total
480
+ });
481
+ } catch (permError) {
482
+ this.logger.error("Failed to seed collection permissions", {
483
+ slug: data.slug,
484
+ error: String(permError)
485
+ });
486
+ }
487
+ }
488
+ return deserializedRecord;
489
+ } catch (error) {
490
+ if (NextlyError.is(error)) throw error;
491
+ throw NextlyError.fromDatabaseError(toDbError(this.dialect, error));
492
+ }
493
+ }
494
+ async updateCollection(slug, data, options) {
495
+ this.logger.debug("Updating collection", { slug });
496
+ const existing = await this.getCollection(slug);
497
+ const targetSlug = data.slug ?? slug;
498
+ await assertGlobalResourceSlugAvailable(this.adapter, targetSlug, {
499
+ currentResourceType: "collection",
500
+ currentResourceId: existing.id
501
+ });
502
+ if (existing.locked && options?.source !== "code") {
503
+ throw NextlyError.forbidden({
504
+ logContext: {
505
+ reason: "collection-locked",
506
+ slug,
507
+ source: options?.source
508
+ }
509
+ });
510
+ }
511
+ const updateData = {
512
+ updated_at: this.formatDateForDb()
513
+ };
514
+ if (data.labels) {
515
+ updateData.labels = JSON.stringify(data.labels);
516
+ }
517
+ if (data.description !== void 0) {
518
+ updateData.description = data.description;
519
+ }
520
+ if (data.fields) {
521
+ updateData.fields = JSON.stringify(data.fields);
522
+ updateData.schema_version = existing.schemaVersion + 1;
523
+ updateData.migration_status = "pending";
524
+ }
525
+ if (data.timestamps !== void 0) {
526
+ updateData.timestamps = data.timestamps;
527
+ }
528
+ if (data.admin !== void 0) {
529
+ updateData.admin = data.admin ? JSON.stringify(data.admin) : null;
530
+ }
531
+ if (data.schemaHash) {
532
+ updateData.schema_hash = data.schemaHash;
533
+ }
534
+ if (data.locked !== void 0) {
535
+ updateData.locked = data.locked;
536
+ }
537
+ if (data.configPath !== void 0) {
538
+ updateData.config_path = data.configPath;
539
+ }
540
+ if (data.hooks !== void 0) {
541
+ updateData.hooks = data.hooks ? JSON.stringify(data.hooks) : null;
542
+ }
543
+ if (data.status !== void 0) {
544
+ updateData.status = data.status === true ? 1 : 0;
545
+ }
546
+ try {
547
+ const results = await this.adapter.update(
548
+ this.registryTableName,
549
+ updateData,
550
+ this.whereEq("slug", slug),
551
+ { returning: "*" }
552
+ );
553
+ if (results.length === 0) {
554
+ throw NextlyError.notFound({ logContext: { slug } });
555
+ }
556
+ this.logger.info("Collection updated", { slug });
557
+ return this.deserializeRecord(results[0]);
558
+ } catch (error) {
559
+ if (NextlyError.is(error)) {
560
+ throw error;
561
+ }
562
+ throw NextlyError.fromDatabaseError(toDbError(this.dialect, error));
563
+ }
564
+ }
565
+ async deleteCollection(slug) {
566
+ this.logger.debug("Deleting collection", { slug });
567
+ const existing = await this.getCollection(slug);
568
+ if (existing.locked) {
569
+ throw NextlyError.forbidden({
570
+ logContext: { reason: "collection-locked-delete", slug }
571
+ });
572
+ }
573
+ try {
574
+ const count = await this.adapter.delete(
575
+ this.registryTableName,
576
+ this.whereEq("slug", slug)
577
+ );
578
+ if (count === 0) {
579
+ throw NextlyError.notFound({ logContext: { slug } });
580
+ }
581
+ this.logger.info("Collection deleted", { slug });
582
+ } catch (error) {
583
+ if (NextlyError.is(error)) {
584
+ throw error;
585
+ }
586
+ throw NextlyError.fromDatabaseError(toDbError(this.dialect, error));
587
+ }
588
+ }
589
+ async syncCodeFirstCollections(configs) {
590
+ this.logger.info("Syncing code-first collections", {
591
+ count: configs.length
592
+ });
593
+ const result = {
594
+ created: [],
595
+ updated: [],
596
+ unchanged: [],
597
+ errors: []
598
+ };
599
+ for (const config of configs) {
600
+ try {
601
+ const existing = await this.getCollectionBySlug(config.slug);
602
+ const schemaHash = calculateSchemaHash(config.fields);
603
+ if (!existing) {
604
+ await this.registerCollection({
605
+ slug: config.slug,
606
+ labels: config.labels,
607
+ tableName: config.tableName ?? this.generateTableName(config.slug),
608
+ description: config.description,
609
+ fields: config.fields,
610
+ timestamps: config.timestamps ?? true,
611
+ admin: config.admin,
612
+ source: "code",
613
+ locked: true,
614
+ // Forward Draft/Published flag so code-first collections that
615
+ // opt in actually write the column on first sync.
616
+ status: config.status === true,
617
+ configPath: config.configPath,
618
+ schemaHash
619
+ });
620
+ result.created.push(config.slug);
621
+ await this.seedPermissionsForCollection(config.slug);
622
+ } else if (!schemaHashesMatch(schemaHash, existing.schemaHash) || config.status === true !== (existing.status === true)) {
623
+ await this.updateCollection(
624
+ config.slug,
625
+ {
626
+ labels: config.labels,
627
+ description: config.description,
628
+ fields: config.fields,
629
+ timestamps: config.timestamps,
630
+ admin: config.admin,
631
+ configPath: config.configPath,
632
+ schemaHash,
633
+ locked: true,
634
+ status: config.status === true
635
+ },
636
+ { source: "code" }
637
+ );
638
+ result.updated.push(config.slug);
639
+ await this.seedPermissionsForCollection(config.slug);
640
+ } else if (this.adminConfigChanged(config.admin, existing.admin) || this.labelsChanged(config.labels, existing.labels)) {
641
+ await this.updateCollection(
642
+ config.slug,
643
+ {
644
+ labels: config.labels,
645
+ admin: config.admin,
646
+ locked: true
647
+ },
648
+ { source: "code" }
649
+ );
650
+ result.updated.push(config.slug);
651
+ } else {
652
+ await this.seedPermissionsForCollection(config.slug);
653
+ result.unchanged.push(config.slug);
654
+ }
655
+ } catch (error) {
656
+ const message = describeError(error);
657
+ const isDuplicate = NextlyError.is(error) && error.code === "DUPLICATE" || message.toLowerCase().includes("already exists") || message.toLowerCase().includes("duplicate") || message.toLowerCase().includes("unique constraint");
658
+ if (isDuplicate) {
659
+ const refetched = await this.getCollectionBySlug(config.slug).catch(
660
+ () => null
661
+ );
662
+ if (refetched) {
663
+ this.logger.warn(
664
+ `Code-first sync: "${config.slug}" already in DB \u2014 treating as unchanged`,
665
+ { slug: config.slug }
666
+ );
667
+ result.unchanged.push(config.slug);
668
+ } else {
669
+ const disambiguatedTableName = `dc_${config.slug.replace(/-/g, "_")}_cf`;
670
+ try {
671
+ const retrySchemaHash = calculateSchemaHash(config.fields);
672
+ await this.registerCollection({
673
+ slug: config.slug,
674
+ labels: config.labels,
675
+ tableName: disambiguatedTableName,
676
+ description: config.description,
677
+ fields: config.fields,
678
+ timestamps: config.timestamps ?? true,
679
+ admin: config.admin,
680
+ source: "code",
681
+ locked: true,
682
+ configPath: config.configPath,
683
+ schemaHash: retrySchemaHash
684
+ });
685
+ this.logger.warn(
686
+ `Code-first sync: "${config.slug}" had table_name conflict \u2014 registered with table "${disambiguatedTableName}"`,
687
+ { slug: config.slug, tableName: disambiguatedTableName }
688
+ );
689
+ result.created.push(config.slug);
690
+ } catch (retryError) {
691
+ const retryMessage = describeError(retryError);
692
+ result.errors.push({
693
+ slug: config.slug,
694
+ error: `table_name conflict (tried "${disambiguatedTableName}"): ${retryMessage}`
695
+ });
696
+ }
697
+ }
698
+ } else {
699
+ result.errors.push({ slug: config.slug, error: message });
700
+ }
701
+ }
702
+ }
703
+ this.logger.info("Code-first sync completed", {
704
+ created: result.created.length,
705
+ updated: result.updated.length,
706
+ unchanged: result.unchanged.length,
707
+ errors: result.errors.length
708
+ });
709
+ return result;
710
+ }
711
+ async registerCollectionInTransaction(tx, data) {
712
+ await assertGlobalResourceSlugAvailable(this.adapter, data.slug);
713
+ const existing = await tx.selectOne(
714
+ this.registryTableName,
715
+ {
716
+ where: this.whereEq("slug", data.slug)
717
+ }
718
+ );
719
+ if (existing) {
720
+ throw NextlyError.duplicate({
721
+ logContext: {
722
+ reason: "collection-slug-conflict-tx",
723
+ slug: data.slug
724
+ }
725
+ });
726
+ }
727
+ const now = this.formatDateForDb();
728
+ const record = {
729
+ id: this.generateId(),
730
+ slug: data.slug,
731
+ labels: JSON.stringify(data.labels),
732
+ table_name: data.tableName,
733
+ description: data.description,
734
+ fields: JSON.stringify(data.fields),
735
+ timestamps: data.timestamps ?? true ? 1 : 0,
736
+ admin: data.admin ? JSON.stringify(data.admin) : null,
737
+ source: data.source,
738
+ locked: data.locked ?? data.source === "code" ? 1 : 0,
739
+ // Same as registerCollection — persist Draft/Published as 0/1.
740
+ status: data.status === true ? 1 : 0,
741
+ config_path: data.configPath,
742
+ schema_hash: data.schemaHash,
743
+ schema_version: data.schemaVersion ?? 1,
744
+ migration_status: data.migrationStatus ?? "pending",
745
+ last_migration_id: data.lastMigrationId,
746
+ created_by: data.createdBy,
747
+ hooks: data.hooks ? JSON.stringify(data.hooks) : null,
748
+ created_at: now,
749
+ updated_at: now
750
+ };
751
+ const result = await tx.insert(
752
+ this.registryTableName,
753
+ record,
754
+ { returning: "*" }
755
+ );
756
+ return this.deserializeRecord(result);
757
+ }
758
+ async seedPermissionsForCollection(slug) {
759
+ if (!this.permissionSeedService) return;
760
+ try {
761
+ const result = await this.permissionSeedService.seedCollectionPermissions(slug);
762
+ if (result.newPermissionIds.length > 0) {
763
+ await this.permissionSeedService.assignNewPermissionsToSuperAdmin(
764
+ result.newPermissionIds
765
+ );
766
+ }
767
+ if (result.created > 0) {
768
+ this.logger.info(
769
+ `Permissions seeded for collection "${slug}": ${result.created} created, ${result.skipped} already existed`
770
+ );
771
+ }
772
+ } catch (error) {
773
+ this.logger.warn(
774
+ `Failed to seed permissions for collection "${slug}": ${error instanceof Error ? error.message : String(error)}`
775
+ );
776
+ }
777
+ }
778
+ labelsChanged(codeLabels, existingLabels) {
779
+ if (!codeLabels && !existingLabels) return false;
780
+ if (!codeLabels || !existingLabels) return true;
781
+ const existing = typeof existingLabels === "string" ? JSON.parse(existingLabels) : existingLabels;
782
+ return codeLabels.singular !== existing.singular || codeLabels.plural !== existing.plural;
783
+ }
784
+ deserializeRecord(record) {
785
+ const r = record;
786
+ const labels = r.labels || r.labels;
787
+ const fields = r.fields || r.fields;
788
+ const admin = r.admin || r.admin;
789
+ const hooks = r.hooks || r.hooks;
790
+ const tableName = r.table_name || r.tableName;
791
+ const configPath = r.config_path || r.configPath;
792
+ const schemaHash = r.schema_hash || r.schemaHash;
793
+ const schemaVersion = r.schema_version || r.schemaVersion;
794
+ const migrationStatus = r.migration_status || r.migrationStatus;
795
+ const lastMigrationId = r.last_migration_id || r.lastMigrationId;
796
+ const createdBy = r.created_by || r.createdBy;
797
+ const createdAt = r.created_at || r.createdAt;
798
+ const updatedAt = r.updated_at || r.updatedAt;
799
+ return {
800
+ id: r.id,
801
+ slug: r.slug,
802
+ tableName,
803
+ description: r.description,
804
+ labels: typeof labels === "string" ? JSON.parse(labels) : labels,
805
+ fields: typeof fields === "string" ? JSON.parse(fields) : fields,
806
+ timestamps: r.timestamps,
807
+ admin: admin ? typeof admin === "string" ? JSON.parse(admin) : admin : void 0,
808
+ hooks: hooks ? typeof hooks === "string" ? JSON.parse(hooks) : hooks : void 0,
809
+ source: r.source,
810
+ locked: r.locked,
811
+ // Why: read the new status meta-column, defaulting to false for rows
812
+ // written before this column existed (legacy data without status set).
813
+ // SQLite returns 0/1 even with mode:"boolean" in some driver/dialect
814
+ // combinations, so accept both shapes.
815
+ status: r.status === 1 || r.status === true,
816
+ configPath,
817
+ schemaHash,
818
+ schemaVersion,
819
+ migrationStatus,
820
+ lastMigrationId,
821
+ createdBy,
822
+ updatedAt: this.normalizeDbTimestamp(updatedAt),
823
+ createdAt: this.normalizeDbTimestamp(createdAt)
824
+ };
825
+ }
826
+ };
827
+
828
+ export {
829
+ assertGlobalResourceSlugAvailable,
830
+ BaseRegistryService,
831
+ CollectionRegistryService
832
+ };