@sonicjs-cms/core 2.19.0 → 3.0.0-beta.3

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 (224) hide show
  1. package/README.md +4 -3
  2. package/dist/admin-documents-form.template-KN7JF66Q.cjs +19 -0
  3. package/dist/{admin-layout-catalyst.template-UMTIN66R.js.map → admin-documents-form.template-KN7JF66Q.cjs.map} +1 -1
  4. package/dist/admin-documents-form.template-NLSI6Z42.js +6 -0
  5. package/dist/{admin-layout-catalyst.template-HFD37TY5.cjs.map → admin-documents-form.template-NLSI6Z42.js.map} +1 -1
  6. package/dist/admin-layout-catalyst.template-WHJGSWWD.js +7 -0
  7. package/dist/admin-layout-catalyst.template-WHJGSWWD.js.map +1 -0
  8. package/dist/admin-layout-catalyst.template-ZK5HD545.cjs +17 -0
  9. package/dist/admin-layout-catalyst.template-ZK5HD545.cjs.map +1 -0
  10. package/dist/app-Bo0X1OWX.d.ts +1268 -0
  11. package/dist/app-Do66yCcV.d.cts +1268 -0
  12. package/dist/cache-DDARE4QE.js +4 -0
  13. package/dist/cache-DDARE4QE.js.map +1 -0
  14. package/dist/cache-LVYS4BPL.cjs +33 -0
  15. package/dist/cache-LVYS4BPL.cjs.map +1 -0
  16. package/dist/chunk-2CB4KY7I.cjs +771 -0
  17. package/dist/chunk-2CB4KY7I.cjs.map +1 -0
  18. package/dist/chunk-2JTWQZHG.cjs +276 -0
  19. package/dist/chunk-2JTWQZHG.cjs.map +1 -0
  20. package/dist/{chunk-55RDMDOP.js → chunk-3TB6AT6X.js} +148 -55
  21. package/dist/chunk-3TB6AT6X.js.map +1 -0
  22. package/dist/{chunk-E4YFJBM2.cjs → chunk-673S7EBY.cjs} +621 -829
  23. package/dist/chunk-673S7EBY.cjs.map +1 -0
  24. package/dist/{chunk-ON5ZMSU4.js → chunk-6JQOUUOB.js} +3 -3
  25. package/dist/chunk-6JQOUUOB.js.map +1 -0
  26. package/dist/chunk-7LHUVREV.cjs +158 -0
  27. package/dist/chunk-7LHUVREV.cjs.map +1 -0
  28. package/dist/chunk-AI663NBO.js +821 -0
  29. package/dist/chunk-AI663NBO.js.map +1 -0
  30. package/dist/chunk-BDDABDAB.cjs +1149 -0
  31. package/dist/chunk-BDDABDAB.cjs.map +1 -0
  32. package/dist/chunk-BLMTL57B.js +767 -0
  33. package/dist/chunk-BLMTL57B.js.map +1 -0
  34. package/dist/{chunk-ABB34XUS.cjs → chunk-CHCBHIBM.cjs} +667 -19
  35. package/dist/chunk-CHCBHIBM.cjs.map +1 -0
  36. package/dist/chunk-DNQCEKUK.cjs +327 -0
  37. package/dist/chunk-DNQCEKUK.cjs.map +1 -0
  38. package/dist/chunk-EF2NQUIQ.js +323 -0
  39. package/dist/chunk-EF2NQUIQ.js.map +1 -0
  40. package/dist/chunk-GCDZZNIN.js +192 -0
  41. package/dist/chunk-GCDZZNIN.js.map +1 -0
  42. package/dist/{chunk-XWIA3HVX.js → chunk-HDWE5FRJ.js} +6 -1249
  43. package/dist/chunk-HDWE5FRJ.js.map +1 -0
  44. package/dist/chunk-HIKBY7MS.cjs +70 -0
  45. package/dist/chunk-HIKBY7MS.cjs.map +1 -0
  46. package/dist/chunk-IESEVHXL.js +66 -0
  47. package/dist/chunk-IESEVHXL.js.map +1 -0
  48. package/dist/chunk-IVPRUGTY.js +242 -0
  49. package/dist/chunk-IVPRUGTY.js.map +1 -0
  50. package/dist/{chunk-JZVHLLSI.cjs → chunk-IXUHXTHW.cjs} +2 -151
  51. package/dist/chunk-IXUHXTHW.cjs.map +1 -0
  52. package/dist/{chunk-7A4CB7T3.cjs → chunk-J5DCX3F6.cjs} +509 -91
  53. package/dist/chunk-J5DCX3F6.cjs.map +1 -0
  54. package/dist/chunk-J6JTWD2A.cjs +100 -0
  55. package/dist/chunk-J6JTWD2A.cjs.map +1 -0
  56. package/dist/chunk-JEQ7FLOD.cjs +199 -0
  57. package/dist/chunk-JEQ7FLOD.cjs.map +1 -0
  58. package/dist/chunk-K25XHMM3.js +566 -0
  59. package/dist/chunk-K25XHMM3.js.map +1 -0
  60. package/dist/{chunk-R4FOLLFB.cjs → chunk-LO6MEPRW.cjs} +8730 -11520
  61. package/dist/chunk-LO6MEPRW.cjs.map +1 -0
  62. package/dist/chunk-MHP7HYTT.js +273 -0
  63. package/dist/chunk-MHP7HYTT.js.map +1 -0
  64. package/dist/chunk-MP3Q2W76.js +387 -0
  65. package/dist/chunk-MP3Q2W76.js.map +1 -0
  66. package/dist/{chunk-OHYBNCVL.cjs → chunk-MVIZJOO5.cjs} +10 -1256
  67. package/dist/chunk-MVIZJOO5.cjs.map +1 -0
  68. package/dist/{chunk-UYJ6TJHX.cjs → chunk-NAVPFIG5.cjs} +148 -55
  69. package/dist/chunk-NAVPFIG5.cjs.map +1 -0
  70. package/dist/chunk-NUKJ54GA.cjs +245 -0
  71. package/dist/chunk-NUKJ54GA.cjs.map +1 -0
  72. package/dist/{chunk-BU7SFHGP.js → chunk-QZGABF2M.js} +3 -149
  73. package/dist/chunk-QZGABF2M.js.map +1 -0
  74. package/dist/{chunk-JZV22DEV.js → chunk-RJV6UXLZ.js} +611 -817
  75. package/dist/chunk-RJV6UXLZ.js.map +1 -0
  76. package/dist/chunk-RNZFGN4R.js +88 -0
  77. package/dist/chunk-RNZFGN4R.js.map +1 -0
  78. package/dist/{chunk-TFNTM3OA.js → chunk-SL6XS6YT.js} +645 -15
  79. package/dist/chunk-SL6XS6YT.js.map +1 -0
  80. package/dist/{chunk-OCL3HMEG.js → chunk-SO2T3OXR.js} +7004 -9807
  81. package/dist/chunk-SO2T3OXR.js.map +1 -0
  82. package/dist/chunk-VQHAJUZZ.cjs +408 -0
  83. package/dist/chunk-VQHAJUZZ.cjs.map +1 -0
  84. package/dist/{chunk-4NPCDK6B.js → chunk-VZ3NHR5Z.js} +505 -90
  85. package/dist/chunk-VZ3NHR5Z.js.map +1 -0
  86. package/dist/{chunk-4ZSNJDLS.cjs → chunk-WULONYGB.cjs} +9 -9
  87. package/dist/chunk-WULONYGB.cjs.map +1 -0
  88. package/dist/chunk-XFQHK64T.js +154 -0
  89. package/dist/chunk-XFQHK64T.js.map +1 -0
  90. package/dist/chunk-YA3TJ65D.cjs +575 -0
  91. package/dist/chunk-YA3TJ65D.cjs.map +1 -0
  92. package/dist/chunk-YP7GW2G5.cjs +866 -0
  93. package/dist/chunk-YP7GW2G5.cjs.map +1 -0
  94. package/dist/{chunk-QFWHAFEO.js → chunk-ZEZ245PW.js} +148 -858
  95. package/dist/chunk-ZEZ245PW.js.map +1 -0
  96. package/dist/{collection-config-B4PG-AaF.d.cts → collection-config-JgHOpFCG.d.cts} +30 -2
  97. package/dist/{collection-config-B4PG-AaF.d.ts → collection-config-JgHOpFCG.d.ts} +30 -2
  98. package/dist/config-HFXANXCC.js +6 -0
  99. package/dist/config-HFXANXCC.js.map +1 -0
  100. package/dist/config-ON6FNMYX.cjs +19 -0
  101. package/dist/config-ON6FNMYX.cjs.map +1 -0
  102. package/dist/define-plugin-BzNHc1ZI.d.ts +1321 -0
  103. package/dist/define-plugin-IWDKYaVm.d.cts +1321 -0
  104. package/dist/document-projection-TDWRJX3Z.cjs +13 -0
  105. package/dist/document-projection-TDWRJX3Z.cjs.map +1 -0
  106. package/dist/document-projection-YYMC6I4U.js +4 -0
  107. package/dist/document-projection-YYMC6I4U.js.map +1 -0
  108. package/dist/index.cjs +13734 -4328
  109. package/dist/index.cjs.map +1 -1
  110. package/dist/index.d.cts +329 -492
  111. package/dist/index.d.ts +329 -492
  112. package/dist/index.js +13385 -3998
  113. package/dist/index.js.map +1 -1
  114. package/dist/middleware.cjs +36 -32
  115. package/dist/middleware.d.cts +50 -7
  116. package/dist/middleware.d.ts +50 -7
  117. package/dist/middleware.js +7 -3
  118. package/dist/migrations-DK2YFPLR.js +4 -0
  119. package/dist/{migrations-H5IXZNCO.js.map → migrations-DK2YFPLR.js.map} +1 -1
  120. package/dist/migrations-YHNHTJOO.cjs +13 -0
  121. package/dist/{migrations-566IIPS2.cjs.map → migrations-YHNHTJOO.cjs.map} +1 -1
  122. package/dist/{plugin-bootstrap-DfVerYV4.d.cts → plugin-bootstrap-B8ThJU21.d.cts} +4315 -1661
  123. package/dist/{plugin-bootstrap-P_ciLp_C.d.ts → plugin-bootstrap-qu8hJgUt.d.ts} +4315 -1661
  124. package/dist/plugins.cjs +171 -12
  125. package/dist/plugins.d.cts +36 -2
  126. package/dist/plugins.d.ts +36 -2
  127. package/dist/plugins.js +5 -2
  128. package/dist/rbac-O73MFKDA.js +5 -0
  129. package/dist/rbac-O73MFKDA.js.map +1 -0
  130. package/dist/rbac-VONLJJKB.cjs +14 -0
  131. package/dist/rbac-VONLJJKB.cjs.map +1 -0
  132. package/dist/routes.cjs +41 -45
  133. package/dist/routes.d.cts +56 -146
  134. package/dist/routes.d.ts +56 -146
  135. package/dist/routes.js +17 -9
  136. package/dist/services.cjs +39 -72
  137. package/dist/services.d.cts +79 -54
  138. package/dist/services.d.ts +79 -54
  139. package/dist/services.js +6 -3
  140. package/dist/templates.cjs +17 -29
  141. package/dist/templates.d.cts +1 -66
  142. package/dist/templates.d.ts +1 -66
  143. package/dist/templates.js +3 -3
  144. package/dist/types-Dea1eNxU.d.cts +286 -0
  145. package/dist/types-Dea1eNxU.d.ts +286 -0
  146. package/dist/types.d.cts +1 -1
  147. package/dist/types.d.ts +1 -1
  148. package/dist/utils.cjs +18 -17
  149. package/dist/utils.d.cts +1 -1
  150. package/dist/utils.d.ts +1 -1
  151. package/dist/utils.js +2 -1
  152. package/migrations/0001_core.sql +184 -0
  153. package/migrations/0002_documents.sql +163 -0
  154. package/package.json +12 -7
  155. package/dist/admin-layout-catalyst.template-HFD37TY5.cjs +0 -17
  156. package/dist/admin-layout-catalyst.template-UMTIN66R.js +0 -7
  157. package/dist/app-C9esKLmh.d.cts +0 -112
  158. package/dist/app-C9esKLmh.d.ts +0 -112
  159. package/dist/chunk-4NPCDK6B.js.map +0 -1
  160. package/dist/chunk-4ZSNJDLS.cjs.map +0 -1
  161. package/dist/chunk-55RDMDOP.js.map +0 -1
  162. package/dist/chunk-635JAMSE.cjs +0 -653
  163. package/dist/chunk-635JAMSE.cjs.map +0 -1
  164. package/dist/chunk-7A4CB7T3.cjs.map +0 -1
  165. package/dist/chunk-ABB34XUS.cjs.map +0 -1
  166. package/dist/chunk-BU7SFHGP.js.map +0 -1
  167. package/dist/chunk-E4YFJBM2.cjs.map +0 -1
  168. package/dist/chunk-EXNEW5US.js +0 -648
  169. package/dist/chunk-EXNEW5US.js.map +0 -1
  170. package/dist/chunk-JZV22DEV.js.map +0 -1
  171. package/dist/chunk-JZVHLLSI.cjs.map +0 -1
  172. package/dist/chunk-OCL3HMEG.js.map +0 -1
  173. package/dist/chunk-OHYBNCVL.cjs.map +0 -1
  174. package/dist/chunk-ON5ZMSU4.js.map +0 -1
  175. package/dist/chunk-QFWHAFEO.js.map +0 -1
  176. package/dist/chunk-R4FOLLFB.cjs.map +0 -1
  177. package/dist/chunk-RLMUFFUD.cjs +0 -2219
  178. package/dist/chunk-RLMUFFUD.cjs.map +0 -1
  179. package/dist/chunk-TFNTM3OA.js.map +0 -1
  180. package/dist/chunk-UYJ6TJHX.cjs.map +0 -1
  181. package/dist/chunk-WAEQXGCX.cjs +0 -1898
  182. package/dist/chunk-WAEQXGCX.cjs.map +0 -1
  183. package/dist/chunk-XWIA3HVX.js.map +0 -1
  184. package/dist/chunk-ZYAYUIZE.js +0 -2217
  185. package/dist/chunk-ZYAYUIZE.js.map +0 -1
  186. package/dist/migrations-566IIPS2.cjs +0 -13
  187. package/dist/migrations-H5IXZNCO.js +0 -4
  188. package/dist/plugin-manager-BoM3Q7o7.d.cts +0 -328
  189. package/dist/plugin-manager-Efx9RyDX.d.ts +0 -328
  190. package/migrations/001_initial_schema.sql +0 -170
  191. package/migrations/002_faq_plugin.sql +0 -86
  192. package/migrations/003_stage5_enhancements.sql +0 -121
  193. package/migrations/004_stage6_user_management.sql +0 -183
  194. package/migrations/005_stage7_workflow_automation.sql +0 -294
  195. package/migrations/006_plugin_system.sql +0 -155
  196. package/migrations/007_demo_login_plugin.sql +0 -23
  197. package/migrations/008_fix_slug_validation.sql +0 -22
  198. package/migrations/009_system_logging.sql +0 -57
  199. package/migrations/011_config_managed_collections.sql +0 -15
  200. package/migrations/012_testimonials_plugin.sql +0 -80
  201. package/migrations/013_code_examples_plugin.sql +0 -177
  202. package/migrations/014_fix_plugin_registry.sql +0 -88
  203. package/migrations/015_add_remaining_plugins.sql +0 -89
  204. package/migrations/016_remove_duplicate_cache_plugin.sql +0 -17
  205. package/migrations/017_auth_configurable_fields.sql +0 -49
  206. package/migrations/018_settings_table.sql +0 -23
  207. package/migrations/019_remove_blog_posts_collection.sql +0 -15
  208. package/migrations/020_add_email_plugin.sql +0 -22
  209. package/migrations/021_add_magic_link_auth_plugin.sql +0 -42
  210. package/migrations/022_add_tinymce_plugin.sql +0 -25
  211. package/migrations/023_add_easy_mdx_plugin.sql +0 -25
  212. package/migrations/024_add_quill_editor_plugin.sql +0 -25
  213. package/migrations/025_add_easymde_plugin.sql +0 -25
  214. package/migrations/026_add_otp_login.sql +0 -42
  215. package/migrations/027_fix_slug_field_type.sql +0 -18
  216. package/migrations/028_fix_slug_field_type_in_schemas.sql +0 -30
  217. package/migrations/029_add_forms_system.sql +0 -184
  218. package/migrations/030_add_turnstile_to_forms.sql +0 -14
  219. package/migrations/031_ai_search_plugin.sql +0 -45
  220. package/migrations/032_user_profiles.sql +0 -37
  221. package/migrations/033_form_content_integration.sql +0 -19
  222. package/migrations/034_security_audit_plugin.sql +0 -27
  223. package/migrations/035_user_profiles_data_column.sql +0 -16
  224. package/migrations/036_analytics_events.sql +0 -22
@@ -1,2219 +0,0 @@
1
- 'use strict';
2
-
3
- // src/db/migrations-bundle.ts
4
- var bundledMigrations = [
5
- {
6
- id: "001",
7
- name: "Initial Schema",
8
- filename: "001_initial_schema.sql",
9
- description: "Migration 001: Initial Schema",
10
- sql: `-- Initial schema for SonicJS AI
11
- -- Create users table for authentication
12
- CREATE TABLE IF NOT EXISTS users (
13
- id TEXT PRIMARY KEY,
14
- email TEXT NOT NULL UNIQUE,
15
- username TEXT NOT NULL UNIQUE,
16
- first_name TEXT NOT NULL,
17
- last_name TEXT NOT NULL,
18
- password_hash TEXT,
19
- role TEXT NOT NULL DEFAULT 'viewer',
20
- avatar TEXT,
21
- is_active INTEGER NOT NULL DEFAULT 1,
22
- last_login_at INTEGER,
23
- created_at INTEGER NOT NULL,
24
- updated_at INTEGER NOT NULL
25
- );
26
-
27
- -- Create collections table for content schema definitions
28
- CREATE TABLE IF NOT EXISTS collections (
29
- id TEXT PRIMARY KEY,
30
- name TEXT NOT NULL UNIQUE,
31
- display_name TEXT NOT NULL,
32
- description TEXT,
33
- schema TEXT NOT NULL, -- JSON schema definition
34
- is_active INTEGER NOT NULL DEFAULT 1,
35
- created_at INTEGER NOT NULL,
36
- updated_at INTEGER NOT NULL
37
- );
38
-
39
- -- Create content table for actual content data
40
- CREATE TABLE IF NOT EXISTS content (
41
- id TEXT PRIMARY KEY,
42
- collection_id TEXT NOT NULL REFERENCES collections(id),
43
- slug TEXT NOT NULL,
44
- title TEXT NOT NULL,
45
- data TEXT NOT NULL, -- JSON content data
46
- status TEXT NOT NULL DEFAULT 'draft',
47
- published_at INTEGER,
48
- author_id TEXT NOT NULL REFERENCES users(id),
49
- created_at INTEGER NOT NULL,
50
- updated_at INTEGER NOT NULL
51
- );
52
-
53
- -- Create content_versions table for versioning
54
- CREATE TABLE IF NOT EXISTS content_versions (
55
- id TEXT PRIMARY KEY,
56
- content_id TEXT NOT NULL REFERENCES content(id),
57
- version INTEGER NOT NULL,
58
- data TEXT NOT NULL, -- JSON data
59
- author_id TEXT NOT NULL REFERENCES users(id),
60
- created_at INTEGER NOT NULL
61
- );
62
-
63
- -- Create media/files table with comprehensive R2 integration
64
- CREATE TABLE IF NOT EXISTS media (
65
- id TEXT PRIMARY KEY,
66
- filename TEXT NOT NULL,
67
- original_name TEXT NOT NULL,
68
- mime_type TEXT NOT NULL,
69
- size INTEGER NOT NULL,
70
- width INTEGER,
71
- height INTEGER,
72
- folder TEXT NOT NULL DEFAULT 'uploads',
73
- r2_key TEXT NOT NULL, -- R2 storage key
74
- public_url TEXT NOT NULL, -- CDN URL
75
- thumbnail_url TEXT, -- Cloudflare Images URL
76
- alt TEXT,
77
- caption TEXT,
78
- tags TEXT, -- JSON array of tags
79
- uploaded_by TEXT NOT NULL REFERENCES users(id),
80
- uploaded_at INTEGER NOT NULL,
81
- updated_at INTEGER,
82
- published_at INTEGER,
83
- scheduled_at INTEGER,
84
- archived_at INTEGER,
85
- deleted_at INTEGER
86
- );
87
-
88
- -- Create API tokens table for programmatic access
89
- CREATE TABLE IF NOT EXISTS api_tokens (
90
- id TEXT PRIMARY KEY,
91
- name TEXT NOT NULL,
92
- token TEXT NOT NULL UNIQUE,
93
- user_id TEXT NOT NULL REFERENCES users(id),
94
- permissions TEXT NOT NULL, -- JSON array of permissions
95
- expires_at INTEGER,
96
- last_used_at INTEGER,
97
- created_at INTEGER NOT NULL
98
- );
99
-
100
- -- Create workflow history table for content workflow tracking
101
- CREATE TABLE IF NOT EXISTS workflow_history (
102
- id TEXT PRIMARY KEY,
103
- content_id TEXT NOT NULL REFERENCES content(id),
104
- action TEXT NOT NULL,
105
- from_status TEXT NOT NULL,
106
- to_status TEXT NOT NULL,
107
- user_id TEXT NOT NULL REFERENCES users(id),
108
- comment TEXT,
109
- created_at INTEGER NOT NULL
110
- );
111
-
112
- -- Create indexes for better performance
113
- CREATE INDEX IF NOT EXISTS idx_users_email ON users(email);
114
- CREATE INDEX IF NOT EXISTS idx_users_username ON users(username);
115
- CREATE INDEX IF NOT EXISTS idx_users_role ON users(role);
116
-
117
- CREATE INDEX IF NOT EXISTS idx_collections_name ON collections(name);
118
- CREATE INDEX IF NOT EXISTS idx_collections_active ON collections(is_active);
119
-
120
- CREATE INDEX IF NOT EXISTS idx_content_collection ON content(collection_id);
121
- CREATE INDEX IF NOT EXISTS idx_content_author ON content(author_id);
122
- CREATE INDEX IF NOT EXISTS idx_content_status ON content(status);
123
- CREATE INDEX IF NOT EXISTS idx_content_published ON content(published_at);
124
- CREATE INDEX IF NOT EXISTS idx_content_slug ON content(slug);
125
-
126
- CREATE INDEX IF NOT EXISTS idx_content_versions_content ON content_versions(content_id);
127
- CREATE INDEX IF NOT EXISTS idx_content_versions_version ON content_versions(version);
128
-
129
- CREATE INDEX IF NOT EXISTS idx_media_folder ON media(folder);
130
- CREATE INDEX IF NOT EXISTS idx_media_type ON media(mime_type);
131
- CREATE INDEX IF NOT EXISTS idx_media_uploaded_by ON media(uploaded_by);
132
- CREATE INDEX IF NOT EXISTS idx_media_uploaded_at ON media(uploaded_at);
133
- CREATE INDEX IF NOT EXISTS idx_media_deleted ON media(deleted_at);
134
-
135
- CREATE INDEX IF NOT EXISTS idx_api_tokens_user ON api_tokens(user_id);
136
- CREATE INDEX IF NOT EXISTS idx_api_tokens_token ON api_tokens(token);
137
-
138
- CREATE INDEX IF NOT EXISTS idx_workflow_history_content ON workflow_history(content_id);
139
- CREATE INDEX IF NOT EXISTS idx_workflow_history_user ON workflow_history(user_id);
140
-
141
- -- Note: Admin user is created via the seed script with user-provided credentials
142
- -- Run 'npm run seed' after migrations to create the admin user
143
-
144
- -- Insert sample collections
145
- INSERT OR IGNORE INTO collections (
146
- id, name, display_name, description, schema,
147
- is_active, created_at, updated_at
148
- ) VALUES (
149
- 'blog-posts-collection',
150
- 'blog_posts',
151
- 'Blog Posts',
152
- 'Blog post content collection',
153
- '{"type":"object","properties":{"title":{"type":"string","title":"Title","required":true},"content":{"type":"string","title":"Content","format":"richtext"},"excerpt":{"type":"string","title":"Excerpt"},"featured_image":{"type":"string","title":"Featured Image","format":"media"},"tags":{"type":"array","title":"Tags","items":{"type":"string"}},"status":{"type":"string","title":"Status","enum":["draft","published","archived"],"default":"draft"}},"required":["title"]}',
154
- 1,
155
- strftime('%s', 'now') * 1000,
156
- strftime('%s', 'now') * 1000
157
- ),
158
- (
159
- 'pages-collection',
160
- 'pages',
161
- 'Pages',
162
- 'Static page content collection',
163
- '{"type":"object","properties":{"title":{"type":"string","title":"Title","required":true},"content":{"type":"string","title":"Content","format":"richtext"},"slug":{"type":"string","title":"Slug"},"meta_description":{"type":"string","title":"Meta Description"},"featured_image":{"type":"string","title":"Featured Image","format":"media"}},"required":["title"]}',
164
- 1,
165
- strftime('%s', 'now') * 1000,
166
- strftime('%s', 'now') * 1000
167
- ),
168
- (
169
- 'news-collection',
170
- 'news',
171
- 'News',
172
- 'News article content collection',
173
- '{"type":"object","properties":{"title":{"type":"string","title":"Title","required":true},"content":{"type":"string","title":"Content","format":"richtext"},"publish_date":{"type":"string","title":"Publish Date","format":"date"},"author":{"type":"string","title":"Author"},"category":{"type":"string","title":"Category","enum":["technology","business","general"]}},"required":["title"]}',
174
- 1,
175
- strftime('%s', 'now') * 1000,
176
- strftime('%s', 'now') * 1000
177
- );
178
-
179
- -- Note: Sample content can be created via the admin interface after the admin user is seeded`
180
- },
181
- {
182
- id: "002",
183
- name: "Faq Plugin",
184
- filename: "002_faq_plugin.sql",
185
- description: "Migration 002: Faq Plugin",
186
- sql: `-- FAQ Plugin Migration (DEPRECATED - Now managed by third-party plugin)
187
- -- Creates FAQ table for the FAQ plugin
188
- -- NOTE: This migration is kept for historical purposes.
189
- -- The FAQ functionality is now provided by the faq-plugin third-party plugin.
190
-
191
- CREATE TABLE IF NOT EXISTS faqs (
192
- id INTEGER PRIMARY KEY AUTOINCREMENT,
193
- question TEXT NOT NULL,
194
- answer TEXT NOT NULL,
195
- category TEXT,
196
- tags TEXT,
197
- isPublished INTEGER NOT NULL DEFAULT 1,
198
- sortOrder INTEGER NOT NULL DEFAULT 0,
199
- created_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now')),
200
- updated_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now'))
201
- );
202
-
203
- -- Create indexes for better performance
204
- CREATE INDEX IF NOT EXISTS idx_faqs_category ON faqs(category);
205
- CREATE INDEX IF NOT EXISTS idx_faqs_published ON faqs(isPublished);
206
- CREATE INDEX IF NOT EXISTS idx_faqs_sort_order ON faqs(sortOrder);
207
-
208
- -- Create trigger to update updated_at timestamp
209
- CREATE TRIGGER IF NOT EXISTS faqs_updated_at
210
- AFTER UPDATE ON faqs
211
- BEGIN
212
- UPDATE faqs SET updated_at = strftime('%s', 'now') WHERE id = NEW.id;
213
- END;
214
-
215
- -- Insert sample FAQ data
216
- INSERT OR IGNORE INTO faqs (question, answer, category, tags, isPublished, sortOrder) VALUES
217
- ('What is SonicJS AI?',
218
- 'SonicJS AI is a modern, TypeScript-first headless CMS built for Cloudflare''s edge platform. It provides a complete content management system with admin interface, API endpoints, and plugin architecture.',
219
- 'general',
220
- 'sonicjs, cms, cloudflare',
221
- 1,
222
- 1),
223
-
224
- ('How do I get started with SonicJS AI?',
225
- 'To get started: 1) Clone the repository, 2) Install dependencies with npm install, 3) Set up your Cloudflare account and services, 4) Run the development server with npm run dev, 5) Access the admin interface at /admin.',
226
- 'general',
227
- 'getting-started, setup',
228
- 1,
229
- 2),
230
-
231
- ('What technologies does SonicJS AI use?',
232
- 'SonicJS AI is built with: TypeScript for type safety, Hono.js as the web framework, Cloudflare D1 for the database, Cloudflare R2 for media storage, Cloudflare Workers for serverless execution, and Tailwind CSS for styling.',
233
- 'technical',
234
- 'technology-stack, typescript, cloudflare',
235
- 1,
236
- 3),
237
-
238
- ('How do I create content in SonicJS AI?',
239
- 'Content creation is done through the admin interface. Navigate to /admin, log in with your credentials, go to Content section, select a collection, and click "New Content" to create articles, pages, or other content types.',
240
- 'general',
241
- 'content-creation, admin',
242
- 1,
243
- 4),
244
-
245
- ('Is SonicJS AI free to use?',
246
- 'SonicJS AI is open source and free to use. You only pay for the Cloudflare services you consume (D1 database, R2 storage, Workers execution time). Cloudflare offers generous free tiers for development and small projects.',
247
- 'billing',
248
- 'pricing, open-source, cloudflare',
249
- 1,
250
- 5),
251
-
252
- ('How do I add custom functionality?',
253
- 'SonicJS AI features a plugin system that allows you to extend functionality. You can create plugins using the PluginBuilder API, add custom routes, models, admin pages, and integrate with external services.',
254
- 'technical',
255
- 'plugins, customization, development',
256
- 1,
257
- 6),
258
-
259
- ('Can I customize the admin interface?',
260
- 'Yes! The admin interface is built with TypeScript templates and can be customized. You can modify existing templates, create new components, add custom pages, and integrate your own styling while maintaining the dark theme.',
261
- 'technical',
262
- 'admin-interface, customization, templates',
263
- 1,
264
- 7),
265
-
266
- ('How does authentication work?',
267
- 'SonicJS AI includes a built-in authentication system with JWT tokens, role-based access control (admin, editor, viewer), secure password hashing, and session management. Users can be managed through the admin interface.',
268
- 'technical',
269
- 'authentication, security, users',
270
- 1,
271
- 8);`
272
- },
273
- {
274
- id: "003",
275
- name: "Stage5 Enhancements",
276
- filename: "003_stage5_enhancements.sql",
277
- description: "Migration 003: Stage5 Enhancements",
278
- sql: `-- Stage 5: Advanced Content Management enhancements
279
- -- Add scheduling and workflow features to content table
280
-
281
- -- Add content scheduling columns
282
- ALTER TABLE content ADD COLUMN scheduled_publish_at INTEGER;
283
- ALTER TABLE content ADD COLUMN scheduled_unpublish_at INTEGER;
284
-
285
- -- Add workflow and review columns
286
- ALTER TABLE content ADD COLUMN review_status TEXT DEFAULT 'none'; -- none, pending, approved, rejected
287
- ALTER TABLE content ADD COLUMN reviewer_id TEXT REFERENCES users(id);
288
- ALTER TABLE content ADD COLUMN reviewed_at INTEGER;
289
- ALTER TABLE content ADD COLUMN review_notes TEXT;
290
-
291
- -- Add content metadata
292
- ALTER TABLE content ADD COLUMN meta_title TEXT;
293
- ALTER TABLE content ADD COLUMN meta_description TEXT;
294
- ALTER TABLE content ADD COLUMN featured_image_id TEXT REFERENCES media(id);
295
- ALTER TABLE content ADD COLUMN content_type TEXT DEFAULT 'standard'; -- standard, template, component
296
-
297
- -- Create content_fields table for dynamic field definitions
298
- CREATE TABLE IF NOT EXISTS content_fields (
299
- id TEXT PRIMARY KEY,
300
- collection_id TEXT NOT NULL REFERENCES collections(id),
301
- field_name TEXT NOT NULL,
302
- field_type TEXT NOT NULL, -- text, richtext, number, boolean, date, select, media, relationship
303
- field_label TEXT NOT NULL,
304
- field_options TEXT, -- JSON for select options, validation rules, etc.
305
- field_order INTEGER NOT NULL DEFAULT 0,
306
- is_required INTEGER NOT NULL DEFAULT 0,
307
- is_searchable INTEGER NOT NULL DEFAULT 0,
308
- created_at INTEGER NOT NULL,
309
- updated_at INTEGER NOT NULL,
310
- UNIQUE(collection_id, field_name)
311
- );
312
-
313
- -- Create content_relationships table for content relationships
314
- CREATE TABLE IF NOT EXISTS content_relationships (
315
- id TEXT PRIMARY KEY,
316
- source_content_id TEXT NOT NULL REFERENCES content(id),
317
- target_content_id TEXT NOT NULL REFERENCES content(id),
318
- relationship_type TEXT NOT NULL, -- references, tags, categories
319
- created_at INTEGER NOT NULL,
320
- UNIQUE(source_content_id, target_content_id, relationship_type)
321
- );
322
-
323
- -- Create workflow_templates table for reusable workflows
324
- CREATE TABLE IF NOT EXISTS workflow_templates (
325
- id TEXT PRIMARY KEY,
326
- name TEXT NOT NULL,
327
- description TEXT,
328
- collection_id TEXT REFERENCES collections(id), -- null means applies to all collections
329
- workflow_steps TEXT NOT NULL, -- JSON array of workflow steps
330
- is_active INTEGER NOT NULL DEFAULT 1,
331
- created_at INTEGER NOT NULL,
332
- updated_at INTEGER NOT NULL
333
- );
334
-
335
- -- Add indexes for new columns
336
- CREATE INDEX IF NOT EXISTS idx_content_scheduled_publish ON content(scheduled_publish_at);
337
- CREATE INDEX IF NOT EXISTS idx_content_scheduled_unpublish ON content(scheduled_unpublish_at);
338
- CREATE INDEX IF NOT EXISTS idx_content_review_status ON content(review_status);
339
- CREATE INDEX IF NOT EXISTS idx_content_reviewer ON content(reviewer_id);
340
- CREATE INDEX IF NOT EXISTS idx_content_content_type ON content(content_type);
341
-
342
- CREATE INDEX IF NOT EXISTS idx_content_fields_collection ON content_fields(collection_id);
343
- CREATE INDEX IF NOT EXISTS idx_content_fields_name ON content_fields(field_name);
344
- CREATE INDEX IF NOT EXISTS idx_content_fields_type ON content_fields(field_type);
345
- CREATE INDEX IF NOT EXISTS idx_content_fields_order ON content_fields(field_order);
346
-
347
- CREATE INDEX IF NOT EXISTS idx_content_relationships_source ON content_relationships(source_content_id);
348
- CREATE INDEX IF NOT EXISTS idx_content_relationships_target ON content_relationships(target_content_id);
349
- CREATE INDEX IF NOT EXISTS idx_content_relationships_type ON content_relationships(relationship_type);
350
-
351
- CREATE INDEX IF NOT EXISTS idx_workflow_templates_collection ON workflow_templates(collection_id);
352
- CREATE INDEX IF NOT EXISTS idx_workflow_templates_active ON workflow_templates(is_active);
353
-
354
- -- Insert default workflow template
355
- INSERT OR IGNORE INTO workflow_templates (
356
- id, name, description, workflow_steps, is_active, created_at, updated_at
357
- ) VALUES (
358
- 'default-content-workflow',
359
- 'Default Content Workflow',
360
- 'Standard content workflow: Draft \u2192 Review \u2192 Published',
361
- '[
362
- {"step": "draft", "name": "Draft", "description": "Content is being created", "permissions": ["author", "editor", "admin"]},
363
- {"step": "review", "name": "Under Review", "description": "Content is pending review", "permissions": ["editor", "admin"]},
364
- {"step": "published", "name": "Published", "description": "Content is live", "permissions": ["editor", "admin"]},
365
- {"step": "archived", "name": "Archived", "description": "Content is archived", "permissions": ["admin"]}
366
- ]',
367
- 1,
368
- strftime('%s', 'now') * 1000,
369
- strftime('%s', 'now') * 1000
370
- );
371
-
372
- -- Insert enhanced field definitions for existing collections
373
- INSERT OR IGNORE INTO content_fields (
374
- id, collection_id, field_name, field_type, field_label, field_options, field_order, is_required, is_searchable, created_at, updated_at
375
- ) VALUES
376
- -- Blog Posts fields
377
- ('blog-title-field', 'blog-posts-collection', 'title', 'text', 'Title', '{"maxLength": 200, "placeholder": "Enter blog post title"}', 1, 1, 1, strftime('%s', 'now') * 1000, strftime('%s', 'now') * 1000),
378
- ('blog-content-field', 'blog-posts-collection', 'content', 'richtext', 'Content', '{"toolbar": "full", "height": 400}', 2, 1, 1, strftime('%s', 'now') * 1000, strftime('%s', 'now') * 1000),
379
- ('blog-excerpt-field', 'blog-posts-collection', 'excerpt', 'text', 'Excerpt', '{"maxLength": 500, "rows": 3, "placeholder": "Brief description of the post"}', 3, 0, 1, strftime('%s', 'now') * 1000, strftime('%s', 'now') * 1000),
380
- ('blog-tags-field', 'blog-posts-collection', 'tags', 'select', 'Tags', '{"multiple": true, "options": ["technology", "business", "tutorial", "news", "update"], "allowCustom": true}', 4, 0, 1, strftime('%s', 'now') * 1000, strftime('%s', 'now') * 1000),
381
- ('blog-featured-image-field', 'blog-posts-collection', 'featured_image', 'media', 'Featured Image', '{"accept": "image/*", "maxSize": "5MB"}', 5, 0, 0, strftime('%s', 'now') * 1000, strftime('%s', 'now') * 1000),
382
- ('blog-publish-date-field', 'blog-posts-collection', 'publish_date', 'date', 'Publish Date', '{"format": "YYYY-MM-DD", "defaultToday": true}', 6, 0, 0, strftime('%s', 'now') * 1000, strftime('%s', 'now') * 1000),
383
- ('blog-featured-field', 'blog-posts-collection', 'is_featured', 'boolean', 'Featured Post', '{"default": false}', 7, 0, 0, strftime('%s', 'now') * 1000, strftime('%s', 'now') * 1000),
384
-
385
- -- Pages fields
386
- ('pages-title-field', 'pages-collection', 'title', 'text', 'Title', '{"maxLength": 200, "placeholder": "Enter page title"}', 1, 1, 1, strftime('%s', 'now') * 1000, strftime('%s', 'now') * 1000),
387
- ('pages-content-field', 'pages-collection', 'content', 'richtext', 'Content', '{"toolbar": "full", "height": 500}', 2, 1, 1, strftime('%s', 'now') * 1000, strftime('%s', 'now') * 1000),
388
- ('pages-slug-field', 'pages-collection', 'slug', 'text', 'URL Slug', '{"pattern": "^[a-z0-9-]+$", "placeholder": "url-friendly-slug"}', 3, 1, 0, strftime('%s', 'now') * 1000, strftime('%s', 'now') * 1000),
389
- ('pages-meta-desc-field', 'pages-collection', 'meta_description', 'text', 'Meta Description', '{"maxLength": 160, "rows": 2, "placeholder": "SEO description for search engines"}', 4, 0, 0, strftime('%s', 'now') * 1000, strftime('%s', 'now') * 1000),
390
- ('pages-template-field', 'pages-collection', 'template', 'select', 'Page Template', '{"options": ["default", "landing", "contact", "about"], "default": "default"}', 5, 0, 0, strftime('%s', 'now') * 1000, strftime('%s', 'now') * 1000),
391
-
392
- -- News fields
393
- ('news-title-field', 'news-collection', 'title', 'text', 'Title', '{"maxLength": 200, "placeholder": "Enter news title"}', 1, 1, 1, strftime('%s', 'now') * 1000, strftime('%s', 'now') * 1000),
394
- ('news-content-field', 'news-collection', 'content', 'richtext', 'Content', '{"toolbar": "news", "height": 400}', 2, 1, 1, strftime('%s', 'now') * 1000, strftime('%s', 'now') * 1000),
395
- ('news-category-field', 'news-collection', 'category', 'select', 'Category', '{"options": ["technology", "business", "politics", "sports", "entertainment", "health"], "required": true}', 3, 1, 1, strftime('%s', 'now') * 1000, strftime('%s', 'now') * 1000),
396
- ('news-author-field', 'news-collection', 'author', 'text', 'Author', '{"placeholder": "Author name"}', 4, 1, 1, strftime('%s', 'now') * 1000, strftime('%s', 'now') * 1000),
397
- ('news-source-field', 'news-collection', 'source', 'text', 'Source', '{"placeholder": "News source"}', 5, 0, 1, strftime('%s', 'now') * 1000, strftime('%s', 'now') * 1000),
398
- ('news-priority-field', 'news-collection', 'priority', 'select', 'Priority', '{"options": ["low", "normal", "high", "breaking"], "default": "normal"}', 6, 0, 0, strftime('%s', 'now') * 1000, strftime('%s', 'now') * 1000);`
399
- },
400
- {
401
- id: "004",
402
- name: "Stage6 User Management",
403
- filename: "004_stage6_user_management.sql",
404
- description: "Migration 004: Stage6 User Management",
405
- sql: "-- Stage 6: User Management & Permissions enhancements\n-- Enhanced user system with profiles, teams, permissions, and activity logging\n\n-- Add user profile and preferences columns\nALTER TABLE users ADD COLUMN phone TEXT;\nALTER TABLE users ADD COLUMN bio TEXT;\nALTER TABLE users ADD COLUMN avatar_url TEXT;\nALTER TABLE users ADD COLUMN timezone TEXT DEFAULT 'UTC';\nALTER TABLE users ADD COLUMN language TEXT DEFAULT 'en';\nALTER TABLE users ADD COLUMN email_notifications INTEGER DEFAULT 1;\nALTER TABLE users ADD COLUMN theme TEXT DEFAULT 'dark';\nALTER TABLE users ADD COLUMN two_factor_enabled INTEGER DEFAULT 0;\nALTER TABLE users ADD COLUMN two_factor_secret TEXT;\nALTER TABLE users ADD COLUMN password_reset_token TEXT;\nALTER TABLE users ADD COLUMN password_reset_expires INTEGER;\nALTER TABLE users ADD COLUMN email_verified INTEGER DEFAULT 0;\nALTER TABLE users ADD COLUMN email_verification_token TEXT;\nALTER TABLE users ADD COLUMN invitation_token TEXT;\nALTER TABLE users ADD COLUMN invited_by TEXT REFERENCES users(id);\nALTER TABLE users ADD COLUMN invited_at INTEGER;\nALTER TABLE users ADD COLUMN accepted_invitation_at INTEGER;\n\n-- Create teams table for team-based collaboration\nCREATE TABLE IF NOT EXISTS teams (\n id TEXT PRIMARY KEY,\n name TEXT NOT NULL,\n description TEXT,\n slug TEXT NOT NULL UNIQUE,\n owner_id TEXT NOT NULL REFERENCES users(id),\n settings TEXT, -- JSON for team settings\n is_active INTEGER NOT NULL DEFAULT 1,\n created_at INTEGER NOT NULL,\n updated_at INTEGER NOT NULL\n);\n\n-- Create team memberships table\nCREATE TABLE IF NOT EXISTS team_memberships (\n id TEXT PRIMARY KEY,\n team_id TEXT NOT NULL REFERENCES teams(id) ON DELETE CASCADE,\n user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE,\n role TEXT NOT NULL DEFAULT 'member', -- owner, admin, editor, member, viewer\n permissions TEXT, -- JSON for specific permissions\n joined_at INTEGER NOT NULL,\n updated_at INTEGER NOT NULL,\n UNIQUE(team_id, user_id)\n);\n\n-- Create permissions table for granular access control\nCREATE TABLE IF NOT EXISTS permissions (\n id TEXT PRIMARY KEY,\n name TEXT NOT NULL UNIQUE,\n description TEXT,\n category TEXT NOT NULL, -- content, users, collections, media, settings\n created_at INTEGER NOT NULL\n);\n\n-- Create role permissions mapping\nCREATE TABLE IF NOT EXISTS role_permissions (\n id TEXT PRIMARY KEY,\n role TEXT NOT NULL,\n permission_id TEXT NOT NULL REFERENCES permissions(id),\n created_at INTEGER NOT NULL,\n UNIQUE(role, permission_id)\n);\n\n-- Create user sessions table for better session management\nCREATE TABLE IF NOT EXISTS user_sessions (\n id TEXT PRIMARY KEY,\n user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE,\n token_hash TEXT NOT NULL,\n ip_address TEXT,\n user_agent TEXT,\n is_active INTEGER NOT NULL DEFAULT 1,\n expires_at INTEGER NOT NULL,\n created_at INTEGER NOT NULL,\n last_used_at INTEGER\n);\n\n-- Create activity log table for audit trails\nCREATE TABLE IF NOT EXISTS activity_logs (\n id TEXT PRIMARY KEY,\n user_id TEXT REFERENCES users(id),\n action TEXT NOT NULL,\n resource_type TEXT, -- users, content, collections, media, etc.\n resource_id TEXT,\n details TEXT, -- JSON with additional details\n ip_address TEXT,\n user_agent TEXT,\n created_at INTEGER NOT NULL\n);\n\n-- Create password history table for security\nCREATE TABLE IF NOT EXISTS password_history (\n id TEXT PRIMARY KEY,\n user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE,\n password_hash TEXT NOT NULL,\n created_at INTEGER NOT NULL\n);\n\n-- Insert default permissions\nINSERT OR IGNORE INTO permissions (id, name, description, category, created_at) VALUES\n ('perm_content_create', 'content.create', 'Create new content', 'content', strftime('%s', 'now') * 1000),\n ('perm_content_read', 'content.read', 'View content', 'content', strftime('%s', 'now') * 1000),\n ('perm_content_update', 'content.update', 'Edit existing content', 'content', strftime('%s', 'now') * 1000),\n ('perm_content_delete', 'content.delete', 'Delete content', 'content', strftime('%s', 'now') * 1000),\n ('perm_content_publish', 'content.publish', 'Publish/unpublish content', 'content', strftime('%s', 'now') * 1000),\n \n ('perm_collections_create', 'collections.create', 'Create new collections', 'collections', strftime('%s', 'now') * 1000),\n ('perm_collections_read', 'collections.read', 'View collections', 'collections', strftime('%s', 'now') * 1000),\n ('perm_collections_update', 'collections.update', 'Edit collections', 'collections', strftime('%s', 'now') * 1000),\n ('perm_collections_delete', 'collections.delete', 'Delete collections', 'collections', strftime('%s', 'now') * 1000),\n ('perm_collections_fields', 'collections.fields', 'Manage collection fields', 'collections', strftime('%s', 'now') * 1000),\n \n ('perm_media_upload', 'media.upload', 'Upload media files', 'media', strftime('%s', 'now') * 1000),\n ('perm_media_read', 'media.read', 'View media files', 'media', strftime('%s', 'now') * 1000),\n ('perm_media_update', 'media.update', 'Edit media metadata', 'media', strftime('%s', 'now') * 1000),\n ('perm_media_delete', 'media.delete', 'Delete media files', 'media', strftime('%s', 'now') * 1000),\n \n ('perm_users_create', 'users.create', 'Invite new users', 'users', strftime('%s', 'now') * 1000),\n ('perm_users_read', 'users.read', 'View user profiles', 'users', strftime('%s', 'now') * 1000),\n ('perm_users_update', 'users.update', 'Edit user profiles', 'users', strftime('%s', 'now') * 1000),\n ('perm_users_delete', 'users.delete', 'Deactivate users', 'users', strftime('%s', 'now') * 1000),\n ('perm_users_roles', 'users.roles', 'Manage user roles', 'users', strftime('%s', 'now') * 1000),\n \n ('perm_settings_read', 'settings.read', 'View system settings', 'settings', strftime('%s', 'now') * 1000),\n ('perm_settings_update', 'settings.update', 'Modify system settings', 'settings', strftime('%s', 'now') * 1000),\n ('perm_activity_read', 'activity.read', 'View activity logs', 'settings', strftime('%s', 'now') * 1000);\n\n-- Assign permissions to default roles\nINSERT OR IGNORE INTO role_permissions (id, role, permission_id, created_at) VALUES\n -- Admin has all permissions\n ('rp_admin_content_create', 'admin', 'perm_content_create', strftime('%s', 'now') * 1000),\n ('rp_admin_content_read', 'admin', 'perm_content_read', strftime('%s', 'now') * 1000),\n ('rp_admin_content_update', 'admin', 'perm_content_update', strftime('%s', 'now') * 1000),\n ('rp_admin_content_delete', 'admin', 'perm_content_delete', strftime('%s', 'now') * 1000),\n ('rp_admin_content_publish', 'admin', 'perm_content_publish', strftime('%s', 'now') * 1000),\n ('rp_admin_collections_create', 'admin', 'perm_collections_create', strftime('%s', 'now') * 1000),\n ('rp_admin_collections_read', 'admin', 'perm_collections_read', strftime('%s', 'now') * 1000),\n ('rp_admin_collections_update', 'admin', 'perm_collections_update', strftime('%s', 'now') * 1000),\n ('rp_admin_collections_delete', 'admin', 'perm_collections_delete', strftime('%s', 'now') * 1000),\n ('rp_admin_collections_fields', 'admin', 'perm_collections_fields', strftime('%s', 'now') * 1000),\n ('rp_admin_media_upload', 'admin', 'perm_media_upload', strftime('%s', 'now') * 1000),\n ('rp_admin_media_read', 'admin', 'perm_media_read', strftime('%s', 'now') * 1000),\n ('rp_admin_media_update', 'admin', 'perm_media_update', strftime('%s', 'now') * 1000),\n ('rp_admin_media_delete', 'admin', 'perm_media_delete', strftime('%s', 'now') * 1000),\n ('rp_admin_users_create', 'admin', 'perm_users_create', strftime('%s', 'now') * 1000),\n ('rp_admin_users_read', 'admin', 'perm_users_read', strftime('%s', 'now') * 1000),\n ('rp_admin_users_update', 'admin', 'perm_users_update', strftime('%s', 'now') * 1000),\n ('rp_admin_users_delete', 'admin', 'perm_users_delete', strftime('%s', 'now') * 1000),\n ('rp_admin_users_roles', 'admin', 'perm_users_roles', strftime('%s', 'now') * 1000),\n ('rp_admin_settings_read', 'admin', 'perm_settings_read', strftime('%s', 'now') * 1000),\n ('rp_admin_settings_update', 'admin', 'perm_settings_update', strftime('%s', 'now') * 1000),\n ('rp_admin_activity_read', 'admin', 'perm_activity_read', strftime('%s', 'now') * 1000),\n \n -- Editor permissions\n ('rp_editor_content_create', 'editor', 'perm_content_create', strftime('%s', 'now') * 1000),\n ('rp_editor_content_read', 'editor', 'perm_content_read', strftime('%s', 'now') * 1000),\n ('rp_editor_content_update', 'editor', 'perm_content_update', strftime('%s', 'now') * 1000),\n ('rp_editor_content_publish', 'editor', 'perm_content_publish', strftime('%s', 'now') * 1000),\n ('rp_editor_collections_read', 'editor', 'perm_collections_read', strftime('%s', 'now') * 1000),\n ('rp_editor_media_upload', 'editor', 'perm_media_upload', strftime('%s', 'now') * 1000),\n ('rp_editor_media_read', 'editor', 'perm_media_read', strftime('%s', 'now') * 1000),\n ('rp_editor_media_update', 'editor', 'perm_media_update', strftime('%s', 'now') * 1000),\n ('rp_editor_users_read', 'editor', 'perm_users_read', strftime('%s', 'now') * 1000),\n \n -- Viewer permissions\n ('rp_viewer_content_read', 'viewer', 'perm_content_read', strftime('%s', 'now') * 1000),\n ('rp_viewer_collections_read', 'viewer', 'perm_collections_read', strftime('%s', 'now') * 1000),\n ('rp_viewer_media_read', 'viewer', 'perm_media_read', strftime('%s', 'now') * 1000),\n ('rp_viewer_users_read', 'viewer', 'perm_users_read', strftime('%s', 'now') * 1000);\n\n-- Create indexes for performance\nCREATE INDEX IF NOT EXISTS idx_team_memberships_team_id ON team_memberships(team_id);\nCREATE INDEX IF NOT EXISTS idx_team_memberships_user_id ON team_memberships(user_id);\nCREATE INDEX IF NOT EXISTS idx_user_sessions_user_id ON user_sessions(user_id);\nCREATE INDEX IF NOT EXISTS idx_user_sessions_token_hash ON user_sessions(token_hash);\nCREATE INDEX IF NOT EXISTS idx_activity_logs_user_id ON activity_logs(user_id);\nCREATE INDEX IF NOT EXISTS idx_activity_logs_created_at ON activity_logs(created_at);\nCREATE INDEX IF NOT EXISTS idx_activity_logs_resource ON activity_logs(resource_type, resource_id);\nCREATE INDEX IF NOT EXISTS idx_password_history_user_id ON password_history(user_id);\nCREATE INDEX IF NOT EXISTS idx_users_email_verification_token ON users(email_verification_token);\nCREATE INDEX IF NOT EXISTS idx_users_password_reset_token ON users(password_reset_token);\nCREATE INDEX IF NOT EXISTS idx_users_invitation_token ON users(invitation_token);"
406
- },
407
- {
408
- id: "005",
409
- name: "Stage7 Workflow Automation",
410
- filename: "005_stage7_workflow_automation.sql",
411
- description: "Migration 005: Stage7 Workflow Automation",
412
- sql: "-- Stage 7: Workflow & Automation Migration\n-- This migration adds workflow and automation capabilities to SonicJS\n\n-- Workflow States Table\nCREATE TABLE IF NOT EXISTS workflow_states (\n id TEXT PRIMARY KEY DEFAULT (lower(hex(randomblob(16)))),\n name TEXT NOT NULL,\n description TEXT,\n color TEXT DEFAULT '#6B7280',\n is_initial INTEGER DEFAULT 0,\n is_final INTEGER DEFAULT 0,\n created_at DATETIME DEFAULT CURRENT_TIMESTAMP\n);\n\n-- Insert default workflow states\nINSERT OR IGNORE INTO workflow_states (id, name, description, color, is_initial, is_final) VALUES\n('draft', 'Draft', 'Content is being worked on', '#F59E0B', 1, 0),\n('pending-review', 'Pending Review', 'Content is waiting for review', '#3B82F6', 0, 0),\n('approved', 'Approved', 'Content has been approved', '#10B981', 0, 0),\n('published', 'Published', 'Content is live', '#059669', 0, 1),\n('rejected', 'Rejected', 'Content was rejected', '#EF4444', 0, 1),\n('archived', 'Archived', 'Content has been archived', '#6B7280', 0, 1);\n\n-- Workflows Table\nCREATE TABLE IF NOT EXISTS workflows (\n id TEXT PRIMARY KEY DEFAULT (lower(hex(randomblob(16)))),\n name TEXT NOT NULL,\n description TEXT,\n collection_id TEXT,\n is_active INTEGER DEFAULT 1,\n auto_publish INTEGER DEFAULT 0,\n require_approval INTEGER DEFAULT 1,\n approval_levels INTEGER DEFAULT 1,\n created_at DATETIME DEFAULT CURRENT_TIMESTAMP,\n updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,\n FOREIGN KEY (collection_id) REFERENCES collections(id) ON DELETE CASCADE\n);\n\n-- Workflow Transitions Table\nCREATE TABLE IF NOT EXISTS workflow_transitions (\n id TEXT PRIMARY KEY DEFAULT (lower(hex(randomblob(16)))),\n workflow_id TEXT NOT NULL,\n from_state_id TEXT NOT NULL,\n to_state_id TEXT NOT NULL,\n required_permission TEXT,\n auto_transition INTEGER DEFAULT 0,\n transition_conditions TEXT, -- JSON\n created_at DATETIME DEFAULT CURRENT_TIMESTAMP,\n FOREIGN KEY (workflow_id) REFERENCES workflows(id) ON DELETE CASCADE,\n FOREIGN KEY (from_state_id) REFERENCES workflow_states(id),\n FOREIGN KEY (to_state_id) REFERENCES workflow_states(id)\n);\n\n-- Content Workflow Status Table\nCREATE TABLE IF NOT EXISTS content_workflow_status (\n id TEXT PRIMARY KEY DEFAULT (lower(hex(randomblob(16)))),\n content_id TEXT NOT NULL,\n workflow_id TEXT NOT NULL,\n current_state_id TEXT NOT NULL,\n assigned_to TEXT,\n due_date DATETIME,\n created_at DATETIME DEFAULT CURRENT_TIMESTAMP,\n updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,\n FOREIGN KEY (content_id) REFERENCES content(id) ON DELETE CASCADE,\n FOREIGN KEY (workflow_id) REFERENCES workflows(id),\n FOREIGN KEY (current_state_id) REFERENCES workflow_states(id),\n FOREIGN KEY (assigned_to) REFERENCES users(id),\n UNIQUE(content_id, workflow_id)\n);\n\n-- Workflow History Table\nCREATE TABLE IF NOT EXISTS workflow_history (\n id TEXT PRIMARY KEY DEFAULT (lower(hex(randomblob(16)))),\n content_id TEXT NOT NULL,\n workflow_id TEXT NOT NULL,\n from_state_id TEXT,\n to_state_id TEXT NOT NULL,\n user_id TEXT NOT NULL,\n comment TEXT,\n metadata TEXT, -- JSON\n created_at DATETIME DEFAULT CURRENT_TIMESTAMP,\n FOREIGN KEY (content_id) REFERENCES content(id) ON DELETE CASCADE,\n FOREIGN KEY (workflow_id) REFERENCES workflows(id),\n FOREIGN KEY (from_state_id) REFERENCES workflow_states(id),\n FOREIGN KEY (to_state_id) REFERENCES workflow_states(id),\n FOREIGN KEY (user_id) REFERENCES users(id)\n);\n\n-- Scheduled Content Table\nCREATE TABLE IF NOT EXISTS scheduled_content (\n id TEXT PRIMARY KEY DEFAULT (lower(hex(randomblob(16)))),\n content_id TEXT NOT NULL,\n action TEXT NOT NULL, -- 'publish', 'unpublish', 'archive'\n scheduled_at DATETIME NOT NULL,\n timezone TEXT DEFAULT 'UTC',\n user_id TEXT NOT NULL,\n status TEXT DEFAULT 'pending', -- 'pending', 'completed', 'failed', 'cancelled'\n executed_at DATETIME,\n error_message TEXT,\n created_at DATETIME DEFAULT CURRENT_TIMESTAMP,\n FOREIGN KEY (content_id) REFERENCES content(id) ON DELETE CASCADE,\n FOREIGN KEY (user_id) REFERENCES users(id)\n);\n\n-- Notifications Table\nCREATE TABLE IF NOT EXISTS notifications (\n id TEXT PRIMARY KEY DEFAULT (lower(hex(randomblob(16)))),\n user_id TEXT NOT NULL,\n type TEXT NOT NULL, -- 'workflow', 'schedule', 'system'\n title TEXT NOT NULL,\n message TEXT NOT NULL,\n data TEXT, -- JSON\n is_read INTEGER DEFAULT 0,\n created_at DATETIME DEFAULT CURRENT_TIMESTAMP,\n FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE\n);\n\n-- Notification Preferences Table\nCREATE TABLE IF NOT EXISTS notification_preferences (\n id TEXT PRIMARY KEY DEFAULT (lower(hex(randomblob(16)))),\n user_id TEXT NOT NULL,\n notification_type TEXT NOT NULL,\n email_enabled INTEGER DEFAULT 1,\n in_app_enabled INTEGER DEFAULT 1,\n digest_frequency TEXT DEFAULT 'daily', -- 'immediate', 'hourly', 'daily', 'weekly'\n created_at DATETIME DEFAULT CURRENT_TIMESTAMP,\n updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,\n FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,\n UNIQUE(user_id, notification_type)\n);\n\n-- Webhooks Table\nCREATE TABLE IF NOT EXISTS webhooks (\n id TEXT PRIMARY KEY DEFAULT (lower(hex(randomblob(16)))),\n name TEXT NOT NULL,\n url TEXT NOT NULL,\n secret TEXT,\n events TEXT NOT NULL, -- JSON array of event types\n is_active INTEGER DEFAULT 1,\n retry_count INTEGER DEFAULT 3,\n timeout_seconds INTEGER DEFAULT 30,\n last_success_at DATETIME,\n last_failure_at DATETIME,\n failure_count INTEGER DEFAULT 0,\n created_at DATETIME DEFAULT CURRENT_TIMESTAMP,\n updated_at DATETIME DEFAULT CURRENT_TIMESTAMP\n);\n\n-- Webhook Deliveries Table\nCREATE TABLE IF NOT EXISTS webhook_deliveries (\n id TEXT PRIMARY KEY DEFAULT (lower(hex(randomblob(16)))),\n webhook_id TEXT NOT NULL,\n event_type TEXT NOT NULL,\n payload TEXT NOT NULL, -- JSON\n response_status INTEGER,\n response_body TEXT,\n attempt_count INTEGER DEFAULT 1,\n delivered_at DATETIME DEFAULT CURRENT_TIMESTAMP,\n FOREIGN KEY (webhook_id) REFERENCES webhooks(id) ON DELETE CASCADE\n);\n\n-- Content Versions Table (for rollback functionality)\nCREATE TABLE IF NOT EXISTS content_versions (\n id TEXT PRIMARY KEY DEFAULT (lower(hex(randomblob(16)))),\n content_id TEXT NOT NULL,\n version_number INTEGER NOT NULL,\n title TEXT NOT NULL,\n content TEXT NOT NULL,\n fields TEXT, -- JSON\n user_id TEXT NOT NULL,\n change_summary TEXT,\n created_at DATETIME DEFAULT CURRENT_TIMESTAMP,\n FOREIGN KEY (content_id) REFERENCES content(id) ON DELETE CASCADE,\n FOREIGN KEY (user_id) REFERENCES users(id),\n UNIQUE(content_id, version_number)\n);\n\n-- Automation Rules Table\nCREATE TABLE IF NOT EXISTS automation_rules (\n id TEXT PRIMARY KEY DEFAULT (lower(hex(randomblob(16)))),\n name TEXT NOT NULL,\n description TEXT,\n trigger_type TEXT NOT NULL, -- 'content_created', 'content_updated', 'workflow_transition', 'schedule'\n trigger_conditions TEXT, -- JSON\n action_type TEXT NOT NULL, -- 'workflow_transition', 'send_notification', 'webhook_call', 'auto_save'\n action_config TEXT, -- JSON\n is_active INTEGER DEFAULT 1,\n created_at DATETIME DEFAULT CURRENT_TIMESTAMP,\n updated_at DATETIME DEFAULT CURRENT_TIMESTAMP\n);\n\n-- Auto-save Drafts Table\nCREATE TABLE IF NOT EXISTS auto_save_drafts (\n id TEXT PRIMARY KEY DEFAULT (lower(hex(randomblob(16)))),\n content_id TEXT,\n user_id TEXT NOT NULL,\n title TEXT,\n content TEXT,\n fields TEXT, -- JSON\n last_saved_at DATETIME DEFAULT CURRENT_TIMESTAMP,\n FOREIGN KEY (content_id) REFERENCES content(id) ON DELETE CASCADE,\n FOREIGN KEY (user_id) REFERENCES users(id),\n UNIQUE(content_id, user_id)\n);\n\n-- Add workflow-related columns to existing content table (skip existing columns)\nALTER TABLE content ADD COLUMN workflow_state_id TEXT DEFAULT 'draft';\nALTER TABLE content ADD COLUMN embargo_until DATETIME;\nALTER TABLE content ADD COLUMN expires_at DATETIME;\nALTER TABLE content ADD COLUMN version_number INTEGER DEFAULT 1;\nALTER TABLE content ADD COLUMN is_auto_saved INTEGER DEFAULT 0;\n\n-- Create indexes for performance\nCREATE INDEX IF NOT EXISTS idx_content_workflow_status_content_id ON content_workflow_status(content_id);\nCREATE INDEX IF NOT EXISTS idx_content_workflow_status_workflow_id ON content_workflow_status(workflow_id);\nCREATE INDEX IF NOT EXISTS idx_workflow_history_content_id ON workflow_history(content_id);\nCREATE INDEX IF NOT EXISTS idx_scheduled_content_scheduled_at ON scheduled_content(scheduled_at);\nCREATE INDEX IF NOT EXISTS idx_scheduled_content_status ON scheduled_content(status);\nCREATE INDEX IF NOT EXISTS idx_notifications_user_id ON notifications(user_id);\nCREATE INDEX IF NOT EXISTS idx_notifications_is_read ON notifications(is_read);\nCREATE INDEX IF NOT EXISTS idx_content_versions_content_id ON content_versions(content_id);\nCREATE INDEX IF NOT EXISTS idx_auto_save_drafts_user_id ON auto_save_drafts(user_id);\nCREATE INDEX IF NOT EXISTS idx_content_workflow_state ON content(workflow_state_id);\nCREATE INDEX IF NOT EXISTS idx_content_scheduled_publish ON content(scheduled_publish_at);\n\n-- Insert default workflow for collections\nINSERT OR IGNORE INTO workflows (id, name, description, collection_id, is_active, require_approval, approval_levels) \nSELECT \n 'default-' || id,\n 'Default Workflow for ' || name,\n 'Standard content approval workflow',\n id,\n 1,\n 1,\n 1\nFROM collections;\n\n-- Insert default workflow transitions\nINSERT OR IGNORE INTO workflow_transitions (workflow_id, from_state_id, to_state_id, required_permission) \nSELECT \n w.id,\n 'draft',\n 'pending-review',\n 'content:submit'\nFROM workflows w;\n\nINSERT OR IGNORE INTO workflow_transitions (workflow_id, from_state_id, to_state_id, required_permission) \nSELECT \n w.id,\n 'pending-review',\n 'approved',\n 'content:approve'\nFROM workflows w;\n\nINSERT OR IGNORE INTO workflow_transitions (workflow_id, from_state_id, to_state_id, required_permission) \nSELECT \n w.id,\n 'approved',\n 'published',\n 'content:publish'\nFROM workflows w;\n\nINSERT OR IGNORE INTO workflow_transitions (workflow_id, from_state_id, to_state_id, required_permission) \nSELECT \n w.id,\n 'pending-review',\n 'rejected',\n 'content:approve'\nFROM workflows w;\n\n-- Insert default notification preferences for all users\nINSERT OR IGNORE INTO notification_preferences (user_id, notification_type, email_enabled, in_app_enabled)\nSELECT \n id,\n 'workflow_assigned',\n 1,\n 1\nFROM users;\n\nINSERT OR IGNORE INTO notification_preferences (user_id, notification_type, email_enabled, in_app_enabled)\nSELECT \n id,\n 'workflow_status_change',\n 1,\n 1\nFROM users;\n\nINSERT OR IGNORE INTO notification_preferences (user_id, notification_type, email_enabled, in_app_enabled)\nSELECT \n id,\n 'content_scheduled',\n 1,\n 1\nFROM users;"
413
- },
414
- {
415
- id: "006",
416
- name: "Plugin System",
417
- filename: "006_plugin_system.sql",
418
- description: "Migration 006: Plugin System",
419
- sql: `-- Plugin System Tables
420
- -- Migration: 006_plugin_system
421
- -- Description: Add plugin management system tables
422
-
423
- -- Plugins table
424
- CREATE TABLE IF NOT EXISTS plugins (
425
- id TEXT PRIMARY KEY,
426
- name TEXT NOT NULL UNIQUE,
427
- display_name TEXT NOT NULL,
428
- description TEXT,
429
- version TEXT NOT NULL,
430
- author TEXT NOT NULL,
431
- category TEXT NOT NULL,
432
- icon TEXT,
433
- status TEXT DEFAULT 'inactive' CHECK (status IN ('active', 'inactive', 'error')),
434
- is_core BOOLEAN DEFAULT FALSE,
435
- settings JSON,
436
- permissions JSON,
437
- dependencies JSON,
438
- download_count INTEGER DEFAULT 0,
439
- rating REAL DEFAULT 0,
440
- installed_at INTEGER NOT NULL,
441
- activated_at INTEGER,
442
- last_updated INTEGER NOT NULL,
443
- error_message TEXT,
444
- created_at INTEGER DEFAULT (unixepoch()),
445
- updated_at INTEGER DEFAULT (unixepoch())
446
- );
447
-
448
- -- Plugin hooks table (registered hooks by plugins)
449
- CREATE TABLE IF NOT EXISTS plugin_hooks (
450
- id TEXT PRIMARY KEY,
451
- plugin_id TEXT NOT NULL,
452
- hook_name TEXT NOT NULL,
453
- handler_name TEXT NOT NULL,
454
- priority INTEGER DEFAULT 10,
455
- is_active BOOLEAN DEFAULT TRUE,
456
- created_at INTEGER DEFAULT (unixepoch()),
457
- FOREIGN KEY (plugin_id) REFERENCES plugins(id) ON DELETE CASCADE,
458
- UNIQUE(plugin_id, hook_name, handler_name)
459
- );
460
-
461
- -- Plugin routes table
462
- CREATE TABLE IF NOT EXISTS plugin_routes (
463
- id TEXT PRIMARY KEY,
464
- plugin_id TEXT NOT NULL,
465
- path TEXT NOT NULL,
466
- method TEXT NOT NULL,
467
- handler_name TEXT NOT NULL,
468
- middleware JSON,
469
- is_active BOOLEAN DEFAULT TRUE,
470
- created_at INTEGER DEFAULT (unixepoch()),
471
- FOREIGN KEY (plugin_id) REFERENCES plugins(id) ON DELETE CASCADE,
472
- UNIQUE(plugin_id, path, method)
473
- );
474
-
475
- -- Plugin assets table (CSS, JS files provided by plugins)
476
- CREATE TABLE IF NOT EXISTS plugin_assets (
477
- id TEXT PRIMARY KEY,
478
- plugin_id TEXT NOT NULL,
479
- asset_type TEXT NOT NULL CHECK (asset_type IN ('css', 'js', 'image', 'font')),
480
- asset_path TEXT NOT NULL,
481
- load_order INTEGER DEFAULT 100,
482
- load_location TEXT DEFAULT 'footer' CHECK (load_location IN ('header', 'footer')),
483
- is_active BOOLEAN DEFAULT TRUE,
484
- created_at INTEGER DEFAULT (unixepoch()),
485
- FOREIGN KEY (plugin_id) REFERENCES plugins(id) ON DELETE CASCADE
486
- );
487
-
488
- -- Plugin activity log
489
- CREATE TABLE IF NOT EXISTS plugin_activity_log (
490
- id TEXT PRIMARY KEY,
491
- plugin_id TEXT NOT NULL,
492
- action TEXT NOT NULL,
493
- user_id TEXT,
494
- details JSON,
495
- timestamp INTEGER DEFAULT (unixepoch()),
496
- FOREIGN KEY (plugin_id) REFERENCES plugins(id) ON DELETE CASCADE
497
- );
498
-
499
- -- Create indexes
500
- CREATE INDEX IF NOT EXISTS idx_plugins_status ON plugins(status);
501
- CREATE INDEX IF NOT EXISTS idx_plugins_category ON plugins(category);
502
- CREATE INDEX IF NOT EXISTS idx_plugin_hooks_plugin ON plugin_hooks(plugin_id);
503
- CREATE INDEX IF NOT EXISTS idx_plugin_routes_plugin ON plugin_routes(plugin_id);
504
- CREATE INDEX IF NOT EXISTS idx_plugin_assets_plugin ON plugin_assets(plugin_id);
505
- CREATE INDEX IF NOT EXISTS idx_plugin_activity_plugin ON plugin_activity_log(plugin_id);
506
- CREATE INDEX IF NOT EXISTS idx_plugin_activity_timestamp ON plugin_activity_log(timestamp);
507
-
508
- -- Insert core plugins
509
- INSERT OR IGNORE INTO plugins (
510
- id, name, display_name, description, version, author, category, icon,
511
- status, is_core, permissions, installed_at, last_updated
512
- ) VALUES
513
- (
514
- 'core-auth',
515
- 'core-auth',
516
- 'Authentication System',
517
- 'Core authentication and user management system',
518
- '1.0.0',
519
- 'SonicJS Team',
520
- 'security',
521
- '\u{1F510}',
522
- 'active',
523
- TRUE,
524
- '["manage:users", "manage:roles", "manage:permissions"]',
525
- unixepoch(),
526
- unixepoch()
527
- ),
528
- (
529
- 'core-media',
530
- 'core-media',
531
- 'Media Manager',
532
- 'Core media upload and management system',
533
- '1.0.0',
534
- 'SonicJS Team',
535
- 'media',
536
- '\u{1F4F8}',
537
- 'active',
538
- TRUE,
539
- '["manage:media", "upload:files"]',
540
- unixepoch(),
541
- unixepoch()
542
- ),
543
- (
544
- 'core-workflow',
545
- 'core-workflow',
546
- 'Workflow Engine',
547
- 'Content workflow and approval system',
548
- '1.0.0',
549
- 'SonicJS Team',
550
- 'content',
551
- '\u{1F504}',
552
- 'active',
553
- TRUE,
554
- '["manage:workflows", "approve:content"]',
555
- unixepoch(),
556
- unixepoch()
557
- );
558
-
559
- -- FAQ Plugin will be added as a third-party plugin through the admin interface
560
-
561
- -- Add plugin management permission
562
- INSERT OR IGNORE INTO permissions (id, name, description, category, created_at)
563
- VALUES (
564
- 'manage:plugins',
565
- 'Manage Plugins',
566
- 'Install, uninstall, activate, and configure plugins',
567
- 'system',
568
- unixepoch()
569
- );
570
-
571
- -- Grant plugin management permission to admin role
572
- INSERT OR IGNORE INTO role_permissions (id, role, permission_id, created_at)
573
- VALUES ('role-perm-manage-plugins', 'admin', 'manage:plugins', unixepoch());`
574
- },
575
- {
576
- id: "007",
577
- name: "Demo Login Plugin",
578
- filename: "007_demo_login_plugin.sql",
579
- description: "Migration 007: Demo Login Plugin",
580
- sql: "-- Demo Login Plugin Migration\n-- Migration: 007_demo_login_plugin\n-- Description: Add demo login prefill plugin to the plugin registry\n\n-- Insert demo login plugin\nINSERT INTO plugins (\n id, name, display_name, description, version, author, category, icon, \n status, is_core, permissions, installed_at, last_updated\n) VALUES (\n 'demo-login-prefill',\n 'demo-login-plugin',\n 'Demo Login Prefill',\n 'Prefills login form with demo credentials (admin@sonicjs.com/sonicjs!) for easy site demonstration',\n '1.0.0',\n 'SonicJS',\n 'demo',\n '\u{1F3AF}',\n 'inactive',\n TRUE,\n '[]',\n unixepoch(),\n unixepoch()\n);"
581
- },
582
- {
583
- id: "008",
584
- name: "Fix Slug Validation",
585
- filename: "008_fix_slug_validation.sql",
586
- description: "Migration 008: Fix Slug Validation",
587
- sql: `-- Migration: Fix overly restrictive slug validation patterns
588
- -- This migration relaxes the slug field validation to be more user-friendly
589
-
590
- -- Update the pages collection slug field to allow underscores and be less restrictive
591
- UPDATE content_fields
592
- SET field_options = '{"pattern": "^[a-zA-Z0-9_-]+$", "placeholder": "url-friendly-slug", "help": "Use letters, numbers, underscores, and hyphens only"}'
593
- WHERE field_name = 'slug' AND collection_id = 'pages-collection';
594
-
595
- -- Update blog posts slug field if it exists
596
- UPDATE content_fields
597
- SET field_options = '{"pattern": "^[a-zA-Z0-9_-]+$", "placeholder": "url-friendly-slug", "help": "Use letters, numbers, underscores, and hyphens only"}'
598
- WHERE field_name = 'slug' AND collection_id = 'blog-posts-collection';
599
-
600
- -- Update news slug field if it exists
601
- UPDATE content_fields
602
- SET field_options = '{"pattern": "^[a-zA-Z0-9_-]+$", "placeholder": "url-friendly-slug", "help": "Use letters, numbers, underscores, and hyphens only"}'
603
- WHERE field_name = 'slug' AND collection_id = 'news-collection';
604
-
605
- -- Update any other slug fields with the restrictive pattern
606
- UPDATE content_fields
607
- SET field_options = REPLACE(field_options, '"pattern": "^[a-z0-9-]+$"', '"pattern": "^[a-zA-Z0-9_-]+$"')
608
- WHERE field_options LIKE '%"pattern": "^[a-z0-9-]+$"%';`
609
- },
610
- {
611
- id: "009",
612
- name: "System Logging",
613
- filename: "009_system_logging.sql",
614
- description: "Migration 009: System Logging",
615
- sql: "-- System Logging Tables\n-- Migration: 009_system_logging\n-- Description: Add system logging and configuration tables\n\n-- System logs table for tracking application events\nCREATE TABLE IF NOT EXISTS system_logs (\n id TEXT PRIMARY KEY,\n level TEXT NOT NULL CHECK (level IN ('debug', 'info', 'warn', 'error', 'fatal')),\n category TEXT NOT NULL CHECK (category IN ('auth', 'api', 'workflow', 'plugin', 'media', 'system', 'security', 'error')),\n message TEXT NOT NULL,\n data TEXT, -- JSON data\n user_id TEXT,\n session_id TEXT,\n request_id TEXT,\n ip_address TEXT,\n user_agent TEXT,\n method TEXT,\n url TEXT,\n status_code INTEGER,\n duration INTEGER, -- milliseconds\n stack_trace TEXT,\n tags TEXT, -- JSON array\n source TEXT, -- source of the log entry\n created_at INTEGER NOT NULL DEFAULT (unixepoch()),\n FOREIGN KEY (user_id) REFERENCES users(id)\n);\n\n-- Log configuration table for managing log settings per category\nCREATE TABLE IF NOT EXISTS log_config (\n id TEXT PRIMARY KEY,\n category TEXT NOT NULL UNIQUE CHECK (category IN ('auth', 'api', 'workflow', 'plugin', 'media', 'system', 'security', 'error')),\n enabled BOOLEAN NOT NULL DEFAULT TRUE,\n level TEXT NOT NULL DEFAULT 'info' CHECK (level IN ('debug', 'info', 'warn', 'error', 'fatal')),\n retention_days INTEGER NOT NULL DEFAULT 30,\n max_size_mb INTEGER NOT NULL DEFAULT 100,\n created_at INTEGER NOT NULL DEFAULT (unixepoch()),\n updated_at INTEGER NOT NULL DEFAULT (unixepoch())\n);\n\n-- Create indexes for better performance\nCREATE INDEX IF NOT EXISTS idx_system_logs_level ON system_logs(level);\nCREATE INDEX IF NOT EXISTS idx_system_logs_category ON system_logs(category);\nCREATE INDEX IF NOT EXISTS idx_system_logs_created_at ON system_logs(created_at);\nCREATE INDEX IF NOT EXISTS idx_system_logs_user_id ON system_logs(user_id);\nCREATE INDEX IF NOT EXISTS idx_system_logs_status_code ON system_logs(status_code);\nCREATE INDEX IF NOT EXISTS idx_system_logs_source ON system_logs(source);\n\n-- Insert default log configurations\nINSERT OR IGNORE INTO log_config (id, category, enabled, level, retention_days, max_size_mb) VALUES\n('log-config-auth', 'auth', TRUE, 'info', 90, 50),\n('log-config-api', 'api', TRUE, 'info', 30, 100),\n('log-config-workflow', 'workflow', TRUE, 'info', 60, 50),\n('log-config-plugin', 'plugin', TRUE, 'warn', 30, 25),\n('log-config-media', 'media', TRUE, 'info', 30, 50),\n('log-config-system', 'system', TRUE, 'info', 90, 100),\n('log-config-security', 'security', TRUE, 'warn', 180, 100),\n('log-config-error', 'error', TRUE, 'error', 90, 200);"
616
- },
617
- {
618
- id: "011",
619
- name: "Config Managed Collections",
620
- filename: "011_config_managed_collections.sql",
621
- description: "Migration 011: Config Managed Collections",
622
- sql: "-- Migration: Add Config-Managed Collections Support\n-- Description: Add 'managed' column to collections table to support config-based collection definitions\n-- Created: 2025-10-03\n\n-- Add 'managed' column to collections table\n-- This column indicates whether a collection is managed by configuration files (true) or user-created (false)\n-- Managed collections cannot be edited through the admin UI\n-- Use a safe approach to add the column only if it doesn't exist\nALTER TABLE collections ADD COLUMN managed INTEGER DEFAULT 0 NOT NULL;\n\n-- Create an index on the managed column for faster queries\nCREATE INDEX IF NOT EXISTS idx_collections_managed ON collections(managed);\n\n-- Create an index on managed + is_active for efficient filtering\nCREATE INDEX IF NOT EXISTS idx_collections_managed_active ON collections(managed, is_active);\n"
623
- },
624
- {
625
- id: "012",
626
- name: "Testimonials Plugin",
627
- filename: "012_testimonials_plugin.sql",
628
- description: "Migration 012: Testimonials Plugin",
629
- sql: `-- Testimonials Plugin Migration
630
- -- Creates testimonials table for the testimonials plugin
631
- -- This demonstrates a code-based collection defined in src/plugins/core-plugins/testimonials-plugin.ts
632
-
633
- CREATE TABLE IF NOT EXISTS testimonials (
634
- id INTEGER PRIMARY KEY AUTOINCREMENT,
635
- author_name TEXT NOT NULL,
636
- author_title TEXT,
637
- author_company TEXT,
638
- testimonial_text TEXT NOT NULL,
639
- rating INTEGER CHECK(rating >= 1 AND rating <= 5),
640
- isPublished INTEGER NOT NULL DEFAULT 1,
641
- sortOrder INTEGER NOT NULL DEFAULT 0,
642
- created_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now')),
643
- updated_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now'))
644
- );
645
-
646
- -- Create indexes for better performance
647
- CREATE INDEX IF NOT EXISTS idx_testimonials_published ON testimonials(isPublished);
648
- CREATE INDEX IF NOT EXISTS idx_testimonials_sort_order ON testimonials(sortOrder);
649
- CREATE INDEX IF NOT EXISTS idx_testimonials_rating ON testimonials(rating);
650
-
651
- -- Create trigger to update updated_at timestamp
652
- CREATE TRIGGER IF NOT EXISTS testimonials_updated_at
653
- AFTER UPDATE ON testimonials
654
- BEGIN
655
- UPDATE testimonials SET updated_at = strftime('%s', 'now') WHERE id = NEW.id;
656
- END;
657
-
658
- -- Insert plugin record
659
- INSERT OR IGNORE INTO plugins (name, display_name, description, version, status, category, settings) VALUES
660
- ('testimonials',
661
- 'Customer Testimonials',
662
- 'Manage customer testimonials and reviews with rating support. This is a code-based collection example.',
663
- '1.0.0',
664
- 'active',
665
- 'content',
666
- '{"defaultPublished": true, "requireRating": false}');
667
-
668
- -- Insert sample testimonial data
669
- INSERT OR IGNORE INTO testimonials (author_name, author_title, author_company, testimonial_text, rating, isPublished, sortOrder) VALUES
670
- ('Jane Smith',
671
- 'CTO',
672
- 'TechStartup Inc',
673
- 'SonicJS AI has transformed how we manage our content. The plugin architecture is brilliant and the edge deployment is blazing fast.',
674
- 5,
675
- 1,
676
- 1),
677
-
678
- ('Michael Chen',
679
- 'Lead Developer',
680
- 'Digital Agency Co',
681
- 'We migrated from WordPress to SonicJS AI and couldn''t be happier. The TypeScript-first approach and modern tooling make development a joy.',
682
- 5,
683
- 1,
684
- 2),
685
-
686
- ('Sarah Johnson',
687
- 'Product Manager',
688
- 'E-commerce Solutions',
689
- 'The headless CMS approach combined with Cloudflare Workers gives us unmatched performance. Our content is served globally with minimal latency.',
690
- 4,
691
- 1,
692
- 3),
693
-
694
- ('David Rodriguez',
695
- 'Full Stack Developer',
696
- 'Creative Studio',
697
- 'Great CMS for modern web applications. The admin interface is clean and the API is well-designed. Plugin system is very flexible.',
698
- 5,
699
- 1,
700
- 4),
701
-
702
- ('Emily Watson',
703
- 'Technical Director',
704
- 'Media Company',
705
- 'SonicJS AI solved our content distribution challenges. The R2 integration for media storage works flawlessly and scales effortlessly.',
706
- 4,
707
- 1,
708
- 5);
709
- `
710
- },
711
- {
712
- id: "013",
713
- name: "Code Examples Plugin",
714
- filename: "013_code_examples_plugin.sql",
715
- description: "Migration 013: Code Examples Plugin",
716
- sql: `-- Code Examples Plugin Migration
717
- -- Creates code_examples table for the code examples plugin
718
- -- This demonstrates a code-based collection for storing and managing code snippets
719
-
720
- CREATE TABLE IF NOT EXISTS code_examples (
721
- id INTEGER PRIMARY KEY AUTOINCREMENT,
722
- title TEXT NOT NULL,
723
- description TEXT,
724
- code TEXT NOT NULL,
725
- language TEXT NOT NULL,
726
- category TEXT,
727
- tags TEXT,
728
- isPublished INTEGER NOT NULL DEFAULT 1,
729
- sortOrder INTEGER NOT NULL DEFAULT 0,
730
- created_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now')),
731
- updated_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now'))
732
- );
733
-
734
- -- Create indexes for better performance
735
- CREATE INDEX IF NOT EXISTS idx_code_examples_published ON code_examples(isPublished);
736
- CREATE INDEX IF NOT EXISTS idx_code_examples_sort_order ON code_examples(sortOrder);
737
- CREATE INDEX IF NOT EXISTS idx_code_examples_language ON code_examples(language);
738
- CREATE INDEX IF NOT EXISTS idx_code_examples_category ON code_examples(category);
739
-
740
- -- Create trigger to update updated_at timestamp
741
- CREATE TRIGGER IF NOT EXISTS code_examples_updated_at
742
- AFTER UPDATE ON code_examples
743
- BEGIN
744
- UPDATE code_examples SET updated_at = strftime('%s', 'now') WHERE id = NEW.id;
745
- END;
746
-
747
- -- Insert plugin record
748
- INSERT OR IGNORE INTO plugins (name, display_name, description, version, status, category, settings) VALUES
749
- ('code-examples',
750
- 'Code Examples',
751
- 'Manage code snippets and examples with syntax highlighting support. Perfect for documentation and tutorials.',
752
- '1.0.0',
753
- 'active',
754
- 'content',
755
- '{"defaultPublished": true, "supportedLanguages": ["javascript", "typescript", "python", "go", "rust", "java", "php", "ruby", "sql"]}');
756
-
757
- -- Insert sample code examples
758
- INSERT OR IGNORE INTO code_examples (title, description, code, language, category, tags, isPublished, sortOrder) VALUES
759
- ('React useState Hook',
760
- 'Basic example of using the useState hook in React for managing component state.',
761
- 'import { useState } from ''react'';
762
-
763
- function Counter() {
764
- const [count, setCount] = useState(0);
765
-
766
- return (
767
- <div>
768
- <p>Count: {count}</p>
769
- <button onClick={() => setCount(count + 1)}>
770
- Increment
771
- </button>
772
- </div>
773
- );
774
- }
775
-
776
- export default Counter;',
777
- 'javascript',
778
- 'frontend',
779
- 'react,hooks,state',
780
- 1,
781
- 1),
782
-
783
- ('TypeScript Interface Example',
784
- 'Defining a TypeScript interface for type-safe objects.',
785
- 'interface User {
786
- id: string;
787
- email: string;
788
- name: string;
789
- role: ''admin'' | ''editor'' | ''viewer'';
790
- createdAt: Date;
791
- }
792
-
793
- function greetUser(user: User): string {
794
- return \`Hello, \${user.name}!\`;
795
- }
796
-
797
- const user: User = {
798
- id: ''123'',
799
- email: ''user@example.com'',
800
- name: ''John Doe'',
801
- role: ''admin'',
802
- createdAt: new Date()
803
- };
804
-
805
- console.log(greetUser(user));',
806
- 'typescript',
807
- 'backend',
808
- 'typescript,types,interface',
809
- 1,
810
- 2),
811
-
812
- ('Python List Comprehension',
813
- 'Elegant way to create lists in Python using list comprehensions.',
814
- '# Basic list comprehension
815
- squares = [x**2 for x in range(10)]
816
- print(squares) # [0, 1, 4, 9, 16, 25, 36, 49, 64, 81]
817
-
818
- # With condition
819
- even_squares = [x**2 for x in range(10) if x % 2 == 0]
820
- print(even_squares) # [0, 4, 16, 36, 64]
821
-
822
- # Nested list comprehension
823
- matrix = [[i+j for j in range(3)] for i in range(3)]
824
- print(matrix) # [[0, 1, 2], [1, 2, 3], [2, 3, 4]]',
825
- 'python',
826
- 'general',
827
- 'python,lists,comprehension',
828
- 1,
829
- 3),
830
-
831
- ('SQL Join Example',
832
- 'Common SQL JOIN patterns for combining data from multiple tables.',
833
- '-- INNER JOIN: Returns only matching rows
834
- SELECT users.name, orders.total
835
- FROM users
836
- INNER JOIN orders ON users.id = orders.user_id;
837
-
838
- -- LEFT JOIN: Returns all users, even without orders
839
- SELECT users.name, orders.total
840
- FROM users
841
- LEFT JOIN orders ON users.id = orders.user_id;
842
-
843
- -- Multiple JOINs
844
- SELECT
845
- users.name,
846
- orders.order_date,
847
- products.name AS product_name
848
- FROM users
849
- INNER JOIN orders ON users.id = orders.user_id
850
- INNER JOIN order_items ON orders.id = order_items.order_id
851
- INNER JOIN products ON order_items.product_id = products.id;',
852
- 'sql',
853
- 'database',
854
- 'sql,joins,queries',
855
- 1,
856
- 4),
857
-
858
- ('Go Error Handling',
859
- 'Idiomatic error handling pattern in Go.',
860
- 'package main
861
-
862
- import (
863
- "errors"
864
- "fmt"
865
- )
866
-
867
- func divide(a, b float64) (float64, error) {
868
- if b == 0 {
869
- return 0, errors.New("division by zero")
870
- }
871
- return a / b, nil
872
- }
873
-
874
- func main() {
875
- result, err := divide(10, 2)
876
- if err != nil {
877
- fmt.Println("Error:", err)
878
- return
879
- }
880
- fmt.Printf("Result: %.2f\\n", result)
881
-
882
- // This will error
883
- _, err = divide(10, 0)
884
- if err != nil {
885
- fmt.Println("Error:", err)
886
- }
887
- }',
888
- 'go',
889
- 'backend',
890
- 'go,error-handling,functions',
891
- 1,
892
- 5);
893
- `
894
- },
895
- {
896
- id: "014",
897
- name: "Fix Plugin Registry",
898
- filename: "014_fix_plugin_registry.sql",
899
- description: "Migration 014: Fix Plugin Registry",
900
- sql: `-- Fix Plugin Registry
901
- -- Migration: 014_fix_plugin_registry
902
- -- Description: Add missing plugins and fix plugin name mismatches
903
-
904
- -- Note: Cannot easily update plugin names as they may be referenced elsewhere
905
- -- Instead we'll add the missing plugins with correct names
906
-
907
- -- Insert missing plugins
908
-
909
- -- Core Analytics Plugin
910
- INSERT OR IGNORE INTO plugins (
911
- id, name, display_name, description, version, author, category, icon,
912
- status, is_core, permissions, installed_at, last_updated
913
- ) VALUES (
914
- 'core-analytics',
915
- 'core-analytics',
916
- 'Analytics & Tracking',
917
- 'Core analytics tracking and reporting plugin with page views and event tracking',
918
- '1.0.0',
919
- 'SonicJS Team',
920
- 'seo',
921
- '\u{1F4CA}',
922
- 'active',
923
- TRUE,
924
- '["view:analytics", "manage:tracking"]',
925
- unixepoch(),
926
- unixepoch()
927
- );
928
-
929
- -- FAQ Plugin
930
- INSERT OR IGNORE INTO plugins (
931
- id, name, display_name, description, version, author, category, icon,
932
- status, is_core, permissions, installed_at, last_updated
933
- ) VALUES (
934
- 'faq-plugin',
935
- 'faq-plugin',
936
- 'FAQ Management',
937
- 'Frequently Asked Questions management plugin with categories, search, and custom styling',
938
- '1.0.0',
939
- 'SonicJS',
940
- 'content',
941
- '\u2753',
942
- 'active',
943
- FALSE,
944
- '["manage:faqs"]',
945
- unixepoch(),
946
- unixepoch()
947
- );
948
-
949
- -- Seed Data Plugin
950
- INSERT OR IGNORE INTO plugins (
951
- id, name, display_name, description, version, author, category, icon,
952
- status, is_core, permissions, installed_at, last_updated
953
- ) VALUES (
954
- 'seed-data',
955
- 'seed-data',
956
- 'Seed Data Generator',
957
- 'Generate realistic example users and content for testing and development',
958
- '1.0.0',
959
- 'SonicJS Team',
960
- 'development',
961
- '\u{1F331}',
962
- 'inactive',
963
- FALSE,
964
- '["admin"]',
965
- unixepoch(),
966
- unixepoch()
967
- );
968
-
969
- -- Database Tools Plugin
970
- INSERT OR IGNORE INTO plugins (
971
- id, name, display_name, description, version, author, category, icon,
972
- status, is_core, permissions, installed_at, last_updated
973
- ) VALUES (
974
- 'database-tools',
975
- 'database-tools',
976
- 'Database Tools',
977
- 'Database management tools including truncate, backup, and validation',
978
- '1.0.0',
979
- 'SonicJS Team',
980
- 'system',
981
- '\u{1F5C4}\uFE0F',
982
- 'active',
983
- FALSE,
984
- '["manage:database", "admin"]',
985
- unixepoch(),
986
- unixepoch()
987
- );
988
- `
989
- },
990
- {
991
- id: "015",
992
- name: "Add Remaining Plugins",
993
- filename: "015_add_remaining_plugins.sql",
994
- description: "Migration 015: Add Remaining Plugins",
995
- sql: `-- Add Remaining Plugins
996
- -- Migration: 015_add_remaining_plugins
997
- -- Description: Add all remaining core plugins that were missing from the registry
998
-
999
- -- Testimonials Plugin (with correct name)
1000
- INSERT OR IGNORE INTO plugins (
1001
- id, name, display_name, description, version, author, category, icon,
1002
- status, is_core, permissions, dependencies, settings, installed_at, last_updated
1003
- ) VALUES (
1004
- 'testimonials-plugin',
1005
- 'testimonials-plugin',
1006
- 'Customer Testimonials',
1007
- 'Manage customer testimonials and reviews with rating support',
1008
- '1.0.0',
1009
- 'SonicJS',
1010
- 'content',
1011
- '\u2B50',
1012
- 'active',
1013
- FALSE,
1014
- '["manage:testimonials"]',
1015
- '[]',
1016
- '{"defaultPublished": true, "requireRating": false}',
1017
- unixepoch(),
1018
- unixepoch()
1019
- );
1020
-
1021
- -- Code Examples Plugin (with correct name)
1022
- INSERT OR IGNORE INTO plugins (
1023
- id, name, display_name, description, version, author, category, icon,
1024
- status, is_core, permissions, dependencies, settings, installed_at, last_updated
1025
- ) VALUES (
1026
- 'code-examples-plugin',
1027
- 'code-examples-plugin',
1028
- 'Code Examples',
1029
- 'Manage code snippets and examples with syntax highlighting support',
1030
- '1.0.0',
1031
- 'SonicJS',
1032
- 'content',
1033
- '\u{1F4BB}',
1034
- 'active',
1035
- FALSE,
1036
- '["manage:code-examples"]',
1037
- '[]',
1038
- '{"defaultPublished": true, "supportedLanguages": ["javascript", "typescript", "python", "go", "rust", "java", "php", "ruby", "sql"]}',
1039
- unixepoch(),
1040
- unixepoch()
1041
- );
1042
-
1043
- -- Workflow Plugin (with correct name)
1044
- INSERT OR IGNORE INTO plugins (
1045
- id, name, display_name, description, version, author, category, icon,
1046
- status, is_core, permissions, dependencies, installed_at, last_updated
1047
- ) VALUES (
1048
- 'workflow-plugin',
1049
- 'workflow-plugin',
1050
- 'Workflow Engine',
1051
- 'Content workflow management with approval chains, scheduling, and automation',
1052
- '1.0.0',
1053
- 'SonicJS Team',
1054
- 'content',
1055
- '\u{1F504}',
1056
- 'active',
1057
- TRUE,
1058
- '["manage:workflows", "approve:content"]',
1059
- '[]',
1060
- unixepoch(),
1061
- unixepoch()
1062
- );
1063
-
1064
- -- Demo Login Plugin (already exists with correct name from migration 007, but let's ensure it's there)
1065
- INSERT OR IGNORE INTO plugins (
1066
- id, name, display_name, description, version, author, category, icon,
1067
- status, is_core, permissions, dependencies, installed_at, last_updated
1068
- ) VALUES (
1069
- 'demo-login-plugin',
1070
- 'demo-login-plugin',
1071
- 'Demo Login Prefill',
1072
- 'Prefills login form with demo credentials for easy site demonstration',
1073
- '1.0.0',
1074
- 'SonicJS',
1075
- 'demo',
1076
- '\u{1F3AF}',
1077
- 'active',
1078
- FALSE,
1079
- '[]',
1080
- '[]',
1081
- unixepoch(),
1082
- unixepoch()
1083
- );
1084
- `
1085
- },
1086
- {
1087
- id: "016",
1088
- name: "Remove Duplicate Cache Plugin",
1089
- filename: "016_remove_duplicate_cache_plugin.sql",
1090
- description: "Migration 016: Remove Duplicate Cache Plugin",
1091
- sql: "-- Migration: Remove duplicate cache plugin entry\n-- Description: Removes the old 'cache' plugin (id: 'cache') that is a duplicate of 'core-cache'\n-- This fixes the issue where Cache System appears twice in the plugins list\n-- Created: 2025-10-14\n\n-- Remove the old 'cache' plugin entry if it exists\n-- The correct plugin is 'core-cache' which is managed by plugin-bootstrap.ts\nDELETE FROM plugins WHERE id = 'cache' AND name = 'cache';\n\n-- Clean up any related entries in plugin activity log\nDELETE FROM plugin_activity_log WHERE plugin_id = 'cache';\n\n-- Clean up any related entries in plugin hooks\nDELETE FROM plugin_hooks WHERE plugin_id = 'cache';\n\n-- Clean up any related entries in plugin routes\nDELETE FROM plugin_routes WHERE plugin_id = 'cache';\n"
1092
- },
1093
- {
1094
- id: "017",
1095
- name: "Auth Configurable Fields",
1096
- filename: "017_auth_configurable_fields.sql",
1097
- description: "Migration 017: Auth Configurable Fields",
1098
- sql: `-- Migration: Make authentication fields configurable
1099
- -- This migration updates the core-auth plugin to support configurable required fields
1100
-
1101
- -- The settings will be stored in the plugins table as JSON
1102
- -- Default settings for core-auth plugin include:
1103
- -- {
1104
- -- "requiredFields": {
1105
- -- "email": { "required": true, "minLength": 5 },
1106
- -- "password": { "required": true, "minLength": 8 },
1107
- -- "username": { "required": true, "minLength": 3 },
1108
- -- "firstName": { "required": true, "minLength": 1 },
1109
- -- "lastName": { "required": true, "minLength": 1 }
1110
- -- },
1111
- -- "validation": {
1112
- -- "emailFormat": true,
1113
- -- "allowDuplicateUsernames": false
1114
- -- }
1115
- -- }
1116
-
1117
- -- Update core-auth plugin settings with configurable field requirements
1118
- UPDATE plugins
1119
- SET settings = json_object(
1120
- 'requiredFields', json_object(
1121
- 'email', json_object('required', 1, 'minLength', 5, 'label', 'Email', 'type', 'email'),
1122
- 'password', json_object('required', 1, 'minLength', 8, 'label', 'Password', 'type', 'password'),
1123
- 'username', json_object('required', 1, 'minLength', 3, 'label', 'Username', 'type', 'text'),
1124
- 'firstName', json_object('required', 1, 'minLength', 1, 'label', 'First Name', 'type', 'text'),
1125
- 'lastName', json_object('required', 1, 'minLength', 1, 'label', 'Last Name', 'type', 'text')
1126
- ),
1127
- 'validation', json_object(
1128
- 'emailFormat', 1,
1129
- 'allowDuplicateUsernames', 0,
1130
- 'passwordRequirements', json_object(
1131
- 'requireUppercase', 0,
1132
- 'requireLowercase', 0,
1133
- 'requireNumbers', 0,
1134
- 'requireSpecialChars', 0
1135
- )
1136
- ),
1137
- 'registration', json_object(
1138
- 'enabled', 1,
1139
- 'requireEmailVerification', 0,
1140
- 'defaultRole', 'viewer'
1141
- )
1142
- )
1143
- WHERE id = 'core-auth';
1144
-
1145
- -- If core-auth plugin doesn't exist, this migration will be handled by bootstrap
1146
- -- No need to insert here as plugin bootstrap handles initial plugin creation
1147
- `
1148
- },
1149
- {
1150
- id: "018",
1151
- name: "Settings Table",
1152
- filename: "018_settings_table.sql",
1153
- description: "Migration 018: Settings Table",
1154
- sql: `-- Create settings table for storing application settings
1155
- CREATE TABLE IF NOT EXISTS settings (
1156
- id TEXT PRIMARY KEY,
1157
- category TEXT NOT NULL, -- 'general', 'appearance', 'security', etc.
1158
- key TEXT NOT NULL,
1159
- value TEXT NOT NULL, -- JSON value
1160
- created_at INTEGER NOT NULL,
1161
- updated_at INTEGER NOT NULL,
1162
- UNIQUE(category, key)
1163
- );
1164
-
1165
- -- Insert default general settings
1166
- INSERT OR IGNORE INTO settings (id, category, key, value, created_at, updated_at)
1167
- VALUES
1168
- (lower(hex(randomblob(16))), 'general', 'siteName', '"SonicJS AI"', unixepoch() * 1000, unixepoch() * 1000),
1169
- (lower(hex(randomblob(16))), 'general', 'siteDescription', '"A modern headless CMS powered by AI"', unixepoch() * 1000, unixepoch() * 1000),
1170
- (lower(hex(randomblob(16))), 'general', 'timezone', '"UTC"', unixepoch() * 1000, unixepoch() * 1000),
1171
- (lower(hex(randomblob(16))), 'general', 'language', '"en"', unixepoch() * 1000, unixepoch() * 1000),
1172
- (lower(hex(randomblob(16))), 'general', 'maintenanceMode', 'false', unixepoch() * 1000, unixepoch() * 1000);
1173
-
1174
- -- Create index for faster lookups
1175
- CREATE INDEX IF NOT EXISTS idx_settings_category ON settings(category);
1176
- CREATE INDEX IF NOT EXISTS idx_settings_category_key ON settings(category, key);
1177
- `
1178
- },
1179
- {
1180
- id: "019",
1181
- name: "Remove Blog Posts Collection",
1182
- filename: "019_remove_blog_posts_collection.sql",
1183
- description: "Migration 019: Remove Blog Posts Collection",
1184
- sql: "-- Migration: Remove blog_posts from database-managed collections\n-- Description: Remove blog-posts-collection from the database so it can be managed by code-based collection\n-- Created: 2025-11-04\n\n-- Delete content associated with blog-posts-collection\nDELETE FROM content WHERE collection_id = 'blog-posts-collection';\n\n-- Delete content fields for blog-posts-collection\nDELETE FROM content_fields WHERE collection_id = 'blog-posts-collection';\n\n-- Delete the blog-posts collection itself\nDELETE FROM collections WHERE id = 'blog-posts-collection';\n\n-- The blog-posts collection will now be managed by the code-based collection\n-- in src/collections/blog-posts.collection.ts\n"
1185
- },
1186
- {
1187
- id: "020",
1188
- name: "Add Email Plugin",
1189
- filename: "020_add_email_plugin.sql",
1190
- description: "Migration 020: Add Email Plugin",
1191
- sql: `-- Add Email Plugin
1192
- -- Migration: 020_add_email_plugin
1193
- -- Description: Add email plugin for transactional emails via Resend
1194
-
1195
- INSERT OR IGNORE INTO plugins (
1196
- id, name, display_name, description, version, author, category, icon,
1197
- status, is_core, permissions, installed_at, last_updated
1198
- ) VALUES (
1199
- 'email',
1200
- 'email',
1201
- 'Email',
1202
- 'Send transactional emails using Resend',
1203
- '1.0.0-beta.1',
1204
- 'SonicJS Team',
1205
- 'utilities',
1206
- '\u{1F4E7}',
1207
- 'inactive',
1208
- TRUE,
1209
- '["email:manage", "email:send", "email:view-logs"]',
1210
- unixepoch(),
1211
- unixepoch()
1212
- );
1213
- `
1214
- },
1215
- {
1216
- id: "021",
1217
- name: "Add Magic Link Auth Plugin",
1218
- filename: "021_add_magic_link_auth_plugin.sql",
1219
- description: "Migration 021: Add Magic Link Auth Plugin",
1220
- sql: `-- Add Magic Link Authentication Plugin
1221
- -- Migration: 021_add_magic_link_auth_plugin
1222
- -- Description: Add magic link authentication plugin for passwordless login
1223
-
1224
- -- Create magic_links table
1225
- CREATE TABLE IF NOT EXISTS magic_links (
1226
- id TEXT PRIMARY KEY,
1227
- user_email TEXT NOT NULL,
1228
- token TEXT NOT NULL UNIQUE,
1229
- expires_at INTEGER NOT NULL,
1230
- used INTEGER DEFAULT 0,
1231
- used_at INTEGER,
1232
- ip_address TEXT,
1233
- user_agent TEXT,
1234
- created_at INTEGER NOT NULL
1235
- );
1236
-
1237
- -- Create indexes for performance
1238
- CREATE INDEX IF NOT EXISTS idx_magic_links_token ON magic_links(token);
1239
- CREATE INDEX IF NOT EXISTS idx_magic_links_email ON magic_links(user_email);
1240
- CREATE INDEX IF NOT EXISTS idx_magic_links_expires ON magic_links(expires_at);
1241
-
1242
- -- Register the plugin
1243
- INSERT OR IGNORE INTO plugins (
1244
- id, name, display_name, description, version, author, category, icon,
1245
- status, is_core, permissions, dependencies, installed_at, last_updated
1246
- ) VALUES (
1247
- 'magic-link-auth',
1248
- 'magic-link-auth',
1249
- 'Magic Link Authentication',
1250
- 'Passwordless authentication via email magic links. Users receive a secure one-time link to sign in without entering a password.',
1251
- '1.0.0',
1252
- 'SonicJS Team',
1253
- 'security',
1254
- '\u{1F517}',
1255
- 'inactive',
1256
- FALSE,
1257
- '["auth:manage", "auth:magic-link"]',
1258
- '["email"]',
1259
- unixepoch(),
1260
- unixepoch()
1261
- );
1262
- `
1263
- },
1264
- {
1265
- id: "022",
1266
- name: "Add Tinymce Plugin",
1267
- filename: "022_add_tinymce_plugin.sql",
1268
- description: "Migration 022: Add Tinymce Plugin",
1269
- sql: `-- Add TinyMCE Rich Text Editor Plugin
1270
- -- Migration: 022_add_tinymce_plugin
1271
- -- Description: Add TinyMCE plugin for WYSIWYG rich text editing
1272
-
1273
- -- Register the plugin (active by default since it replaces hardcoded TinyMCE)
1274
- INSERT OR IGNORE INTO plugins (
1275
- id, name, display_name, description, version, author, category, icon,
1276
- status, is_core, permissions, dependencies, settings, installed_at, last_updated
1277
- ) VALUES (
1278
- 'tinymce-plugin',
1279
- 'tinymce-plugin',
1280
- 'TinyMCE Rich Text Editor',
1281
- 'Powerful WYSIWYG rich text editor for content creation. Provides a full-featured editor with formatting, media embedding, and customizable toolbars for richtext fields.',
1282
- '1.0.0',
1283
- 'SonicJS Team',
1284
- 'editor',
1285
- '\u270F\uFE0F',
1286
- 'active',
1287
- FALSE,
1288
- '[]',
1289
- '[]',
1290
- '{"apiKey":"no-api-key","defaultHeight":300,"defaultToolbar":"full","skin":"oxide-dark"}',
1291
- unixepoch(),
1292
- unixepoch()
1293
- );
1294
- `
1295
- },
1296
- {
1297
- id: "023",
1298
- name: "Add Easy Mdx Plugin",
1299
- filename: "023_add_easy_mdx_plugin.sql",
1300
- description: "Migration 023: Add Easy Mdx Plugin",
1301
- sql: `-- Add EasyMDE Markdown Editor Plugin
1302
- -- Migration: 023_add_easy_mdx_plugin
1303
- -- Description: Add EasyMDE plugin for lightweight markdown editing
1304
-
1305
- -- Register the plugin (active by default)
1306
- INSERT OR IGNORE INTO plugins (
1307
- id, name, display_name, description, version, author, category, icon,
1308
- status, is_core, permissions, dependencies, settings, installed_at, last_updated
1309
- ) VALUES (
1310
- 'easy-mdx',
1311
- 'easy-mdx',
1312
- 'EasyMDE Markdown Editor',
1313
- 'Lightweight markdown editor with live preview. Provides a simple and efficient editor with markdown support for richtext fields.',
1314
- '1.0.0',
1315
- 'SonicJS Team',
1316
- 'editor',
1317
- '\u{1F4DD}',
1318
- 'active',
1319
- FALSE,
1320
- '[]',
1321
- '[]',
1322
- '{"defaultHeight":400,"theme":"dark","toolbar":"full","placeholder":"Start writing your content..."}',
1323
- unixepoch(),
1324
- unixepoch()
1325
- );
1326
- `
1327
- },
1328
- {
1329
- id: "024",
1330
- name: "Add Quill Editor Plugin",
1331
- filename: "024_add_quill_editor_plugin.sql",
1332
- description: "Migration 024: Add Quill Editor Plugin",
1333
- sql: `-- Add Quill Rich Text Editor Plugin
1334
- -- Migration: 024_add_quill_editor_plugin
1335
- -- Description: Add Quill plugin for modern rich text editing
1336
-
1337
- -- Register the plugin (active by default)
1338
- INSERT OR IGNORE INTO plugins (
1339
- id, name, display_name, description, version, author, category, icon,
1340
- status, is_core, permissions, dependencies, settings, installed_at, last_updated
1341
- ) VALUES (
1342
- 'quill-editor',
1343
- 'quill-editor',
1344
- 'Quill Rich Text Editor',
1345
- 'Modern rich text editor for content creation. Provides a clean, intuitive WYSIWYG editor with customizable toolbars for richtext fields.',
1346
- '1.0.0',
1347
- 'SonicJS Team',
1348
- 'editor',
1349
- '\u270D\uFE0F',
1350
- 'active',
1351
- FALSE,
1352
- '[]',
1353
- '[]',
1354
- '{"theme":"snow","defaultHeight":300,"defaultToolbar":"full","placeholder":"Start writing your content..."}',
1355
- unixepoch(),
1356
- unixepoch()
1357
- );
1358
- `
1359
- },
1360
- {
1361
- id: "025",
1362
- name: "Add Easymde Plugin",
1363
- filename: "025_add_easymde_plugin.sql",
1364
- description: "Migration 025: Add Easymde Plugin",
1365
- sql: `-- Add EasyMDE Rich Text Editor Plugin
1366
- -- Migration: 025_add_easymde_plugin
1367
- -- Description: Add EasyMDE plugin for markdown-based rich text editing
1368
-
1369
- -- Register the plugin (inactive by default, replacing MDXEditor)
1370
- INSERT OR IGNORE INTO plugins (
1371
- id, name, display_name, description, version, author, category, icon,
1372
- status, is_core, permissions, dependencies, settings, installed_at, last_updated
1373
- ) VALUES (
1374
- 'easymde-editor',
1375
- 'easymde-editor',
1376
- 'EasyMDE Editor',
1377
- 'Lightweight markdown editor for content creation. Simple, elegant WYSIWYG markdown editor with live preview, toolbar, and dark mode support for richtext fields.',
1378
- '1.0.0',
1379
- 'SonicJS Team',
1380
- 'editor',
1381
- '\u270D\uFE0F',
1382
- 'inactive',
1383
- TRUE,
1384
- '[]',
1385
- '[]',
1386
- '{"theme":"dark","defaultHeight":300,"toolbar":"full","spellChecker":false,"placeholder":"Start writing your content..."}',
1387
- unixepoch(),
1388
- unixepoch()
1389
- );
1390
- `
1391
- },
1392
- {
1393
- id: "026",
1394
- name: "Add Otp Login",
1395
- filename: "026_add_otp_login.sql",
1396
- description: "Migration 026: Add Otp Login",
1397
- sql: `-- Add OTP Login Plugin
1398
- -- Migration: 021_add_otp_login
1399
- -- Description: Add OTP login plugin for passwordless authentication via email codes
1400
-
1401
- -- Create table for OTP codes
1402
- CREATE TABLE IF NOT EXISTS otp_codes (
1403
- id TEXT PRIMARY KEY,
1404
- user_email TEXT NOT NULL,
1405
- code TEXT NOT NULL,
1406
- expires_at INTEGER NOT NULL,
1407
- used INTEGER DEFAULT 0,
1408
- used_at INTEGER,
1409
- ip_address TEXT,
1410
- user_agent TEXT,
1411
- attempts INTEGER DEFAULT 0,
1412
- created_at INTEGER NOT NULL
1413
- );
1414
-
1415
- -- Create indexes for performance
1416
- CREATE INDEX IF NOT EXISTS idx_otp_email_code ON otp_codes(user_email, code);
1417
- CREATE INDEX IF NOT EXISTS idx_otp_expires ON otp_codes(expires_at);
1418
- CREATE INDEX IF NOT EXISTS idx_otp_used ON otp_codes(used);
1419
-
1420
- -- Add plugin record
1421
- INSERT OR IGNORE INTO plugins (
1422
- id, name, display_name, description, version, author, category, icon,
1423
- status, is_core, permissions, installed_at, last_updated
1424
- ) VALUES (
1425
- 'otp-login',
1426
- 'otp-login',
1427
- 'OTP Login',
1428
- 'Passwordless authentication via email one-time codes',
1429
- '1.0.0-beta.1',
1430
- 'SonicJS Team',
1431
- 'security',
1432
- '\u{1F522}',
1433
- 'inactive',
1434
- TRUE,
1435
- '["otp:manage", "otp:request", "otp:verify"]',
1436
- unixepoch(),
1437
- unixepoch()
1438
- );
1439
- `
1440
- },
1441
- {
1442
- id: "027",
1443
- name: "Fix Slug Field Type",
1444
- filename: "027_fix_slug_field_type.sql",
1445
- description: "Migration 027: Fix Slug Field Type",
1446
- sql: "-- Migration: Fix slug field type\n-- Description: Update slug fields to use 'slug' field type instead of 'text' for proper auto-generation\n-- Created: 2026-01-10\n\n-- Update pages collection slug field to use 'slug' field type\nUPDATE content_fields \nSET field_type = 'slug'\nWHERE field_name = 'slug' AND collection_id = 'pages-collection';\n\n-- Update blog posts slug field if it exists\nUPDATE content_fields \nSET field_type = 'slug'\nWHERE field_name = 'slug' AND collection_id = 'blog-posts-collection';\n\n-- Update news slug field if it exists\nUPDATE content_fields \nSET field_type = 'slug'\nWHERE field_name = 'slug' AND collection_id = 'news-collection';\n"
1447
- },
1448
- {
1449
- id: "028",
1450
- name: "Fix Slug Field Type In Schemas",
1451
- filename: "028_fix_slug_field_type_in_schemas.sql",
1452
- description: "Migration 028: Fix Slug Field Type In Schemas",
1453
- sql: `-- Migration: Fix slug field type in collection schemas
1454
- -- Description: Update slug fields in collection schemas to use 'slug' type instead of 'string'
1455
- -- Created: 2026-01-10
1456
-
1457
- -- Update pages-collection schema
1458
- UPDATE collections
1459
- SET schema = REPLACE(
1460
- schema,
1461
- '"slug":{"type":"string"',
1462
- '"slug":{"type":"slug"'
1463
- )
1464
- WHERE id = 'pages-collection' AND schema LIKE '%"slug":{"type":"string"%';
1465
-
1466
- -- Update blog-posts-collection schema if it exists
1467
- UPDATE collections
1468
- SET schema = REPLACE(
1469
- schema,
1470
- '"slug":{"type":"string"',
1471
- '"slug":{"type":"slug"'
1472
- )
1473
- WHERE id = 'blog-posts-collection' AND schema LIKE '%"slug":{"type":"string"%';
1474
-
1475
- -- Update news-collection schema if it exists
1476
- UPDATE collections
1477
- SET schema = REPLACE(
1478
- schema,
1479
- '"slug":{"type":"string"',
1480
- '"slug":{"type":"slug"'
1481
- )
1482
- WHERE id = 'news-collection' AND schema LIKE '%"slug":{"type":"string"%';
1483
- `
1484
- },
1485
- {
1486
- id: "029",
1487
- name: "Add Forms System",
1488
- filename: "029_add_forms_system.sql",
1489
- description: "Migration 029: Add Forms System",
1490
- sql: `-- Migration: 029_add_forms_system.sql
1491
- -- Description: Add Form.io integration for advanced form building
1492
- -- Date: January 23, 2026
1493
- -- Phase: 1 - Database Schema
1494
-
1495
- -- =====================================================
1496
- -- Table: forms
1497
- -- Description: Stores form definitions and configuration
1498
- -- =====================================================
1499
-
1500
- CREATE TABLE IF NOT EXISTS forms (
1501
- id TEXT PRIMARY KEY,
1502
- name TEXT NOT NULL UNIQUE, -- Machine name (e.g., "contact-form")
1503
- display_name TEXT NOT NULL, -- Human name (e.g., "Contact Form")
1504
- description TEXT, -- Optional description
1505
- category TEXT DEFAULT 'general', -- Form category (contact, survey, registration, etc.)
1506
-
1507
- -- Form.io schema (JSON)
1508
- formio_schema TEXT NOT NULL, -- Complete Form.io JSON schema
1509
-
1510
- -- Settings
1511
- settings TEXT, -- JSON: {
1512
- -- emailNotifications: true,
1513
- -- notifyEmail: "admin@example.com",
1514
- -- successMessage: "Thank you!",
1515
- -- redirectUrl: "/thank-you",
1516
- -- allowAnonymous: true,
1517
- -- requireAuth: false,
1518
- -- maxSubmissions: null,
1519
- -- submitButtonText: "Submit",
1520
- -- saveProgress: true,
1521
- -- webhookUrl: null
1522
- -- }
1523
-
1524
- -- Status & Management
1525
- is_active INTEGER DEFAULT 1, -- Active/inactive flag
1526
- is_public INTEGER DEFAULT 1, -- Public (anyone) vs private (auth required)
1527
- managed INTEGER DEFAULT 0, -- Code-managed (like collections)
1528
-
1529
- -- Metadata
1530
- icon TEXT, -- Optional icon for admin UI
1531
- color TEXT, -- Optional color (hex) for admin UI
1532
- tags TEXT, -- JSON array of tags
1533
-
1534
- -- Stats
1535
- submission_count INTEGER DEFAULT 0, -- Total submissions received
1536
- view_count INTEGER DEFAULT 0, -- Form views (optional tracking)
1537
-
1538
- -- Ownership
1539
- created_by TEXT REFERENCES users(id), -- User who created the form
1540
- updated_by TEXT REFERENCES users(id), -- User who last updated
1541
-
1542
- -- Timestamps
1543
- created_at INTEGER NOT NULL,
1544
- updated_at INTEGER NOT NULL
1545
- );
1546
-
1547
- -- Indexes for forms
1548
- CREATE INDEX IF NOT EXISTS idx_forms_name ON forms(name);
1549
- CREATE INDEX IF NOT EXISTS idx_forms_category ON forms(category);
1550
- CREATE INDEX IF NOT EXISTS idx_forms_active ON forms(is_active);
1551
- CREATE INDEX IF NOT EXISTS idx_forms_public ON forms(is_public);
1552
- CREATE INDEX IF NOT EXISTS idx_forms_created_by ON forms(created_by);
1553
-
1554
- -- =====================================================
1555
- -- Table: form_submissions
1556
- -- Description: Stores submitted form data
1557
- -- =====================================================
1558
-
1559
- CREATE TABLE IF NOT EXISTS form_submissions (
1560
- id TEXT PRIMARY KEY,
1561
- form_id TEXT NOT NULL REFERENCES forms(id) ON DELETE CASCADE,
1562
-
1563
- -- Submission data
1564
- submission_data TEXT NOT NULL, -- JSON: The actual form data submitted
1565
-
1566
- -- Submission metadata
1567
- status TEXT DEFAULT 'pending', -- pending, reviewed, approved, rejected, spam
1568
- submission_number INTEGER, -- Sequential number per form
1569
-
1570
- -- User information (if authenticated)
1571
- user_id TEXT REFERENCES users(id), -- Submitter user ID (if logged in)
1572
- user_email TEXT, -- Email from form (or user account)
1573
-
1574
- -- Tracking information
1575
- ip_address TEXT, -- IP address of submitter
1576
- user_agent TEXT, -- Browser user agent
1577
- referrer TEXT, -- Page that referred to form
1578
- utm_source TEXT, -- UTM tracking params
1579
- utm_medium TEXT,
1580
- utm_campaign TEXT,
1581
-
1582
- -- Review/Processing
1583
- reviewed_by TEXT REFERENCES users(id), -- Admin who reviewed
1584
- reviewed_at INTEGER, -- Review timestamp
1585
- review_notes TEXT, -- Admin notes
1586
-
1587
- -- Flags
1588
- is_spam INTEGER DEFAULT 0, -- Spam flag
1589
- is_archived INTEGER DEFAULT 0, -- Archived flag
1590
-
1591
- -- Timestamps
1592
- submitted_at INTEGER NOT NULL,
1593
- updated_at INTEGER NOT NULL
1594
- );
1595
-
1596
- -- Indexes for submissions
1597
- CREATE INDEX IF NOT EXISTS idx_form_submissions_form_id ON form_submissions(form_id);
1598
- CREATE INDEX IF NOT EXISTS idx_form_submissions_status ON form_submissions(status);
1599
- CREATE INDEX IF NOT EXISTS idx_form_submissions_user_id ON form_submissions(user_id);
1600
- CREATE INDEX IF NOT EXISTS idx_form_submissions_email ON form_submissions(user_email);
1601
- CREATE INDEX IF NOT EXISTS idx_form_submissions_submitted_at ON form_submissions(submitted_at);
1602
- CREATE INDEX IF NOT EXISTS idx_form_submissions_spam ON form_submissions(is_spam);
1603
-
1604
- -- Trigger to auto-increment submission_number per form
1605
- CREATE TRIGGER IF NOT EXISTS set_submission_number
1606
- AFTER INSERT ON form_submissions
1607
- BEGIN
1608
- UPDATE form_submissions
1609
- SET submission_number = (
1610
- SELECT COUNT(*)
1611
- FROM form_submissions
1612
- WHERE form_id = NEW.form_id
1613
- AND id <= NEW.id
1614
- )
1615
- WHERE id = NEW.id;
1616
- END;
1617
-
1618
- -- Trigger to update form submission_count
1619
- CREATE TRIGGER IF NOT EXISTS increment_form_submission_count
1620
- AFTER INSERT ON form_submissions
1621
- BEGIN
1622
- UPDATE forms
1623
- SET submission_count = submission_count + 1,
1624
- updated_at = unixepoch() * 1000
1625
- WHERE id = NEW.form_id;
1626
- END;
1627
-
1628
- -- =====================================================
1629
- -- Table: form_files (Optional)
1630
- -- Description: Link form submissions to uploaded files
1631
- -- =====================================================
1632
-
1633
- CREATE TABLE IF NOT EXISTS form_files (
1634
- id TEXT PRIMARY KEY,
1635
- submission_id TEXT NOT NULL REFERENCES form_submissions(id) ON DELETE CASCADE,
1636
- media_id TEXT NOT NULL REFERENCES media(id) ON DELETE CASCADE,
1637
- field_name TEXT NOT NULL, -- Form field that uploaded this file
1638
- uploaded_at INTEGER NOT NULL
1639
- );
1640
-
1641
- -- Indexes for form files
1642
- CREATE INDEX IF NOT EXISTS idx_form_files_submission ON form_files(submission_id);
1643
- CREATE INDEX IF NOT EXISTS idx_form_files_media ON form_files(media_id);
1644
-
1645
- -- =====================================================
1646
- -- Sample Data: Create a default contact form
1647
- -- =====================================================
1648
-
1649
- INSERT OR IGNORE INTO forms (
1650
- id,
1651
- name,
1652
- display_name,
1653
- description,
1654
- category,
1655
- formio_schema,
1656
- settings,
1657
- is_active,
1658
- is_public,
1659
- created_at,
1660
- updated_at
1661
- ) VALUES (
1662
- 'default-contact-form',
1663
- 'contact',
1664
- 'Contact Form',
1665
- 'A simple contact form for getting in touch',
1666
- 'contact',
1667
- '{"components":[]}',
1668
- '{"emailNotifications":false,"successMessage":"Thank you for your submission!","submitButtonText":"Submit","requireAuth":false}',
1669
- 1,
1670
- 1,
1671
- unixepoch() * 1000,
1672
- unixepoch() * 1000
1673
- );
1674
- `
1675
- },
1676
- {
1677
- id: "030",
1678
- name: "Add Turnstile To Forms",
1679
- filename: "030_add_turnstile_to_forms.sql",
1680
- description: "Migration 030: Add Turnstile To Forms",
1681
- sql: `-- Add Turnstile configuration to forms table
1682
- -- This allows per-form Turnstile settings with global fallback
1683
-
1684
- -- Add columns (D1 may not support CHECK constraints in ALTER TABLE)
1685
- ALTER TABLE forms ADD COLUMN turnstile_enabled INTEGER DEFAULT 0;
1686
- ALTER TABLE forms ADD COLUMN turnstile_settings TEXT;
1687
-
1688
- -- Set default to inherit global settings for existing forms
1689
- UPDATE forms
1690
- SET turnstile_settings = '{"inherit":true}'
1691
- WHERE turnstile_settings IS NULL;
1692
-
1693
- -- Add index for faster lookups
1694
- CREATE INDEX IF NOT EXISTS idx_forms_turnstile ON forms(turnstile_enabled);
1695
- `
1696
- },
1697
- {
1698
- id: "031",
1699
- name: "Ai Search Plugin",
1700
- filename: "031_ai_search_plugin.sql",
1701
- description: "Migration 031: Ai Search Plugin",
1702
- sql: "-- AI Search plugin settings\nCREATE TABLE IF NOT EXISTS ai_search_settings (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n enabled BOOLEAN DEFAULT 0,\n ai_mode_enabled BOOLEAN DEFAULT 1,\n selected_collections TEXT, -- JSON array of collection IDs to index\n dismissed_collections TEXT, -- JSON array of collection IDs user chose not to index\n autocomplete_enabled BOOLEAN DEFAULT 1,\n cache_duration INTEGER DEFAULT 1, -- hours\n results_limit INTEGER DEFAULT 20,\n index_media BOOLEAN DEFAULT 0,\n index_status TEXT, -- JSON object with status per collection\n last_indexed_at INTEGER,\n created_at INTEGER DEFAULT (strftime('%s', 'now') * 1000),\n updated_at INTEGER DEFAULT (strftime('%s', 'now') * 1000)\n);\n\n-- Search history/analytics\nCREATE TABLE IF NOT EXISTS ai_search_history (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n query TEXT NOT NULL,\n mode TEXT, -- 'ai' or 'keyword'\n results_count INTEGER,\n user_id INTEGER,\n created_at INTEGER DEFAULT (strftime('%s', 'now') * 1000)\n);\n\n-- Index metadata tracking (per collection)\nCREATE TABLE IF NOT EXISTS ai_search_index_meta (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n collection_id INTEGER NOT NULL,\n collection_name TEXT NOT NULL, -- Cache collection name for display\n total_items INTEGER DEFAULT 0,\n indexed_items INTEGER DEFAULT 0,\n last_sync_at INTEGER,\n status TEXT DEFAULT 'pending', -- 'pending', 'indexing', 'completed', 'error'\n error_message TEXT,\n UNIQUE(collection_id)\n);\n\n-- Indexes for performance\nCREATE INDEX IF NOT EXISTS idx_ai_search_history_created_at ON ai_search_history(created_at);\nCREATE INDEX IF NOT EXISTS idx_ai_search_history_mode ON ai_search_history(mode);\nCREATE INDEX IF NOT EXISTS idx_ai_search_index_meta_collection_id ON ai_search_index_meta(collection_id);\nCREATE INDEX IF NOT EXISTS idx_ai_search_index_meta_status ON ai_search_index_meta(status);\n"
1703
- },
1704
- {
1705
- id: "032",
1706
- name: "User Profiles",
1707
- filename: "032_user_profiles.sql",
1708
- description: "Migration 032: User Profiles",
1709
- sql: "-- User Profiles Table (Core Migration)\n-- Stores extended user profile data separate from auth concerns\n-- Required by admin-users.ts for user edit page profile management\n--\n-- Originally introduced as app-level migration (my-sonicjs-app/migrations/018_user_profiles.sql)\n-- in upstream PR #508. Core routes (admin-users.ts) were updated to query this table in PR #512,\n-- but no corresponding core migration was added. This migration corrects that gap.\n--\n-- IF NOT EXISTS guards ensure idempotency for databases that already have the table\n-- from the app-level migration.\n\nCREATE TABLE IF NOT EXISTS user_profiles (\n id TEXT PRIMARY KEY,\n user_id TEXT NOT NULL UNIQUE REFERENCES users(id) ON DELETE CASCADE,\n\n display_name TEXT,\n bio TEXT,\n company TEXT,\n job_title TEXT,\n website TEXT,\n location TEXT,\n date_of_birth INTEGER,\n data TEXT DEFAULT '{}',\n\n created_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now') * 1000),\n updated_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now') * 1000)\n);\n\n-- Index for fast user lookups\nCREATE INDEX IF NOT EXISTS idx_user_profiles_user_id ON user_profiles(user_id);\n\n-- Trigger to auto-update updated_at timestamp\nCREATE TRIGGER IF NOT EXISTS user_profiles_updated_at\n AFTER UPDATE ON user_profiles\nBEGIN\n UPDATE user_profiles SET updated_at = strftime('%s', 'now') * 1000 WHERE id = NEW.id;\nEND;\n"
1710
- },
1711
- {
1712
- id: "033",
1713
- name: "Form Content Integration",
1714
- filename: "033_form_content_integration.sql",
1715
- description: "Migration 033: Form Content Integration",
1716
- sql: "-- Migration 033: Form-Content Integration\n-- Adds bridge columns to link forms to collections and submissions to content items\n\n-- Add source_type and source_id to collections for form-derived collections\nALTER TABLE collections ADD COLUMN source_type TEXT DEFAULT 'user';\nALTER TABLE collections ADD COLUMN source_id TEXT;\n\n-- Index for efficient lookup of form-derived collections\nCREATE INDEX IF NOT EXISTS idx_collections_source ON collections(source_type, source_id);\n\n-- Add content_id to form_submissions for linking to content items\nALTER TABLE form_submissions ADD COLUMN content_id TEXT REFERENCES content(id);\n\n-- Index for efficient lookup by content_id\nCREATE INDEX IF NOT EXISTS idx_form_submissions_content_id ON form_submissions(content_id);\n\n-- Create system user for anonymous form submissions\nINSERT OR IGNORE INTO users (id, email, username, first_name, last_name, password_hash, role, is_active, created_at, updated_at)\nVALUES ('system-form-submission', 'system-forms@sonicjs.internal', 'system-forms', 'Form', 'Submission', NULL, 'viewer', 0, strftime('%s','now') * 1000, strftime('%s','now') * 1000);\n"
1717
- },
1718
- {
1719
- id: "034",
1720
- name: "Security Audit Plugin",
1721
- filename: "034_security_audit_plugin.sql",
1722
- description: "Migration 034: Security Audit Plugin",
1723
- sql: "-- Security Audit Plugin\n-- Tracks login attempts, registrations, and security events for monitoring and brute-force detection\n\nCREATE TABLE IF NOT EXISTS security_events (\n id TEXT PRIMARY KEY,\n event_type TEXT NOT NULL,\n severity TEXT NOT NULL DEFAULT 'info',\n user_id TEXT,\n email TEXT,\n ip_address TEXT,\n user_agent TEXT,\n country_code TEXT,\n request_path TEXT,\n request_method TEXT,\n details TEXT,\n fingerprint TEXT,\n blocked INTEGER NOT NULL DEFAULT 0,\n created_at INTEGER NOT NULL\n);\n\nCREATE INDEX IF NOT EXISTS idx_security_events_type ON security_events(event_type);\nCREATE INDEX IF NOT EXISTS idx_security_events_user ON security_events(user_id);\nCREATE INDEX IF NOT EXISTS idx_security_events_email ON security_events(email);\nCREATE INDEX IF NOT EXISTS idx_security_events_ip ON security_events(ip_address);\nCREATE INDEX IF NOT EXISTS idx_security_events_severity ON security_events(severity);\nCREATE INDEX IF NOT EXISTS idx_security_events_created ON security_events(created_at);\nCREATE INDEX IF NOT EXISTS idx_security_events_fingerprint ON security_events(fingerprint);\n"
1724
- },
1725
- {
1726
- id: "035",
1727
- name: "User Profiles Data Column",
1728
- filename: "035_user_profiles_data_column.sql",
1729
- description: "Migration 035: User Profiles Data Column",
1730
- sql: `-- Migration 035: Add data column to user_profiles (no-op)
1731
- --
1732
- -- This migration originally added a missing 'data' column to user_profiles.
1733
- -- Migration 032 has since been updated to include the column in the CREATE TABLE,
1734
- -- so on fresh installs the column already exists by the time this runs.
1735
- --
1736
- -- The ALTER TABLE has been removed to prevent "duplicate column name: data" errors
1737
- -- during fresh installs (GitHub issue #771). Wrangler's migration runner does not
1738
- -- gracefully handle duplicate column errors like the runtime MigrationService does.
1739
- --
1740
- -- Existing databases that ran the old 032 (without the data column) get the column
1741
- -- added at runtime by the core MigrationService, which skips duplicate-column errors.
1742
- --
1743
- -- This file is kept as a no-op so that wrangler's migration tracking remains
1744
- -- consistent (it tracks migrations by filename).
1745
- SELECT 1;
1746
- `
1747
- },
1748
- {
1749
- id: "036",
1750
- name: "Analytics Events",
1751
- filename: "036_analytics_events.sql",
1752
- description: "Migration 036: Analytics Events",
1753
- sql: "-- Migration 036: Analytics Events Table\n-- Provides storage for user behavior event tracking (page views, custom events)\n\nCREATE TABLE IF NOT EXISTS analytics_events (\n id TEXT PRIMARY KEY,\n event TEXT NOT NULL,\n category TEXT NOT NULL DEFAULT 'user-activity',\n properties TEXT,\n user_id TEXT,\n session_id TEXT,\n ip_address TEXT,\n user_agent TEXT,\n path TEXT,\n created_at INTEGER NOT NULL DEFAULT (unixepoch())\n);\n\nCREATE INDEX IF NOT EXISTS idx_analytics_events_event ON analytics_events(event);\nCREATE INDEX IF NOT EXISTS idx_analytics_events_category ON analytics_events(category);\nCREATE INDEX IF NOT EXISTS idx_analytics_events_user_id ON analytics_events(user_id);\nCREATE INDEX IF NOT EXISTS idx_analytics_events_session_id ON analytics_events(session_id);\nCREATE INDEX IF NOT EXISTS idx_analytics_events_created_at ON analytics_events(created_at);\nCREATE INDEX IF NOT EXISTS idx_analytics_events_path ON analytics_events(path);\n"
1754
- }
1755
- ];
1756
- var migrationsByIdMap = new Map(
1757
- bundledMigrations.map((m) => [m.id, m])
1758
- );
1759
- function getMigrationSQLById(id) {
1760
- return migrationsByIdMap.get(id)?.sql ?? null;
1761
- }
1762
-
1763
- // src/services/migrations.ts
1764
- var MigrationService = class {
1765
- constructor(db) {
1766
- this.db = db;
1767
- }
1768
- /**
1769
- * Initialize the migrations tracking table
1770
- */
1771
- async initializeMigrationsTable() {
1772
- const createTableQuery = `
1773
- CREATE TABLE IF NOT EXISTS migrations (
1774
- id TEXT PRIMARY KEY,
1775
- name TEXT NOT NULL,
1776
- filename TEXT NOT NULL,
1777
- applied_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
1778
- checksum TEXT
1779
- )
1780
- `;
1781
- await this.db.prepare(createTableQuery).run();
1782
- }
1783
- /**
1784
- * Get all available migrations from the bundled migrations
1785
- */
1786
- async getAvailableMigrations() {
1787
- const migrations = [];
1788
- const appliedResult = await this.db.prepare(
1789
- "SELECT id, name, filename, applied_at FROM migrations ORDER BY applied_at ASC"
1790
- ).all();
1791
- const appliedMigrations = new Map(
1792
- appliedResult.results?.map((row) => [row.id, row]) || []
1793
- );
1794
- await this.autoDetectAppliedMigrations(appliedMigrations);
1795
- for (const bundled of bundledMigrations) {
1796
- const applied = appliedMigrations.has(bundled.id);
1797
- const appliedData = appliedMigrations.get(bundled.id);
1798
- migrations.push({
1799
- id: bundled.id,
1800
- name: bundled.name,
1801
- filename: bundled.filename,
1802
- description: bundled.description,
1803
- applied,
1804
- appliedAt: applied ? appliedData?.applied_at : void 0,
1805
- size: bundled.sql.length
1806
- });
1807
- }
1808
- return migrations;
1809
- }
1810
- /**
1811
- * Auto-detect applied migrations by checking if their tables exist
1812
- */
1813
- async autoDetectAppliedMigrations(appliedMigrations) {
1814
- if (!appliedMigrations.has("001")) {
1815
- const hasBasicTables = await this.checkTablesExist(["users", "content", "collections", "media"]);
1816
- if (hasBasicTables) {
1817
- appliedMigrations.set("001", {
1818
- id: "001",
1819
- applied_at: (/* @__PURE__ */ new Date()).toISOString(),
1820
- name: "Initial Schema",
1821
- filename: "001_initial_schema.sql"
1822
- });
1823
- await this.markMigrationApplied("001", "Initial Schema", "001_initial_schema.sql");
1824
- }
1825
- }
1826
- if (!appliedMigrations.has("002")) {
1827
- const hasFaqTables = await this.checkTablesExist(["faqs"]);
1828
- if (hasFaqTables) {
1829
- appliedMigrations.set("002", {
1830
- id: "002",
1831
- applied_at: (/* @__PURE__ */ new Date()).toISOString(),
1832
- name: "Faq Plugin",
1833
- filename: "002_faq_plugin.sql"
1834
- });
1835
- await this.markMigrationApplied("002", "Faq Plugin", "002_faq_plugin.sql");
1836
- }
1837
- }
1838
- if (!appliedMigrations.has("003")) {
1839
- const hasStage5Tables = await this.checkTablesExist(["content_fields", "content_relationships", "workflow_templates"]);
1840
- if (hasStage5Tables) {
1841
- appliedMigrations.set("003", {
1842
- id: "003",
1843
- applied_at: (/* @__PURE__ */ new Date()).toISOString(),
1844
- name: "Stage 5 Enhancements",
1845
- filename: "003_stage5_enhancements.sql"
1846
- });
1847
- await this.markMigrationApplied("003", "Stage 5 Enhancements", "003_stage5_enhancements.sql");
1848
- }
1849
- }
1850
- if (!appliedMigrations.has("012")) {
1851
- const hasTestimonialsTables = await this.checkTablesExist(["testimonials"]);
1852
- if (hasTestimonialsTables) {
1853
- appliedMigrations.set("012", {
1854
- id: "012",
1855
- applied_at: (/* @__PURE__ */ new Date()).toISOString(),
1856
- name: "Testimonials Plugin",
1857
- filename: "012_testimonials_plugin.sql"
1858
- });
1859
- await this.markMigrationApplied("012", "Testimonials Plugin", "012_testimonials_plugin.sql");
1860
- }
1861
- }
1862
- if (!appliedMigrations.has("013")) {
1863
- const hasCodeExamplesTables = await this.checkTablesExist(["code_examples"]);
1864
- if (hasCodeExamplesTables) {
1865
- appliedMigrations.set("013", {
1866
- id: "013",
1867
- applied_at: (/* @__PURE__ */ new Date()).toISOString(),
1868
- name: "Code Examples Plugin",
1869
- filename: "013_code_examples_plugin.sql"
1870
- });
1871
- await this.markMigrationApplied("013", "Code Examples Plugin", "013_code_examples_plugin.sql");
1872
- }
1873
- }
1874
- if (!appliedMigrations.has("004")) {
1875
- const hasUserTables = await this.checkTablesExist(["api_tokens", "workflow_history"]);
1876
- if (hasUserTables) {
1877
- appliedMigrations.set("004", {
1878
- id: "004",
1879
- applied_at: (/* @__PURE__ */ new Date()).toISOString(),
1880
- name: "User Management",
1881
- filename: "004_stage6_user_management.sql"
1882
- });
1883
- await this.markMigrationApplied("004", "User Management", "004_stage6_user_management.sql");
1884
- }
1885
- }
1886
- if (!appliedMigrations.has("006")) {
1887
- const hasPluginTables = await this.checkTablesExist(["plugins", "plugin_hooks"]);
1888
- if (hasPluginTables) {
1889
- appliedMigrations.set("006", {
1890
- id: "006",
1891
- applied_at: (/* @__PURE__ */ new Date()).toISOString(),
1892
- name: "Plugin System",
1893
- filename: "006_plugin_system.sql"
1894
- });
1895
- await this.markMigrationApplied("006", "Plugin System", "006_plugin_system.sql");
1896
- }
1897
- }
1898
- const hasManagedColumn = await this.checkColumnExists("collections", "managed");
1899
- if (!appliedMigrations.has("011") && hasManagedColumn) {
1900
- appliedMigrations.set("011", {
1901
- id: "011",
1902
- applied_at: (/* @__PURE__ */ new Date()).toISOString(),
1903
- name: "Config Managed Collections",
1904
- filename: "011_config_managed_collections.sql"
1905
- });
1906
- await this.markMigrationApplied("011", "Config Managed Collections", "011_config_managed_collections.sql");
1907
- } else if (appliedMigrations.has("011") && !hasManagedColumn) {
1908
- console.log("[Migration] Migration 011 marked as applied but managed column missing - will re-run");
1909
- appliedMigrations.delete("011");
1910
- await this.removeMigrationApplied("011");
1911
- }
1912
- if (!appliedMigrations.has("009")) {
1913
- const hasLoggingTables = await this.checkTablesExist(["system_logs", "log_config"]);
1914
- if (hasLoggingTables) {
1915
- appliedMigrations.set("009", {
1916
- id: "009",
1917
- applied_at: (/* @__PURE__ */ new Date()).toISOString(),
1918
- name: "System Logging",
1919
- filename: "009_system_logging.sql"
1920
- });
1921
- await this.markMigrationApplied("009", "System Logging", "009_system_logging.sql");
1922
- }
1923
- }
1924
- if (!appliedMigrations.has("018")) {
1925
- const hasSettingsTable = await this.checkTablesExist(["settings"]);
1926
- if (hasSettingsTable) {
1927
- appliedMigrations.set("018", {
1928
- id: "018",
1929
- applied_at: (/* @__PURE__ */ new Date()).toISOString(),
1930
- name: "Settings Table",
1931
- filename: "018_settings_table.sql"
1932
- });
1933
- await this.markMigrationApplied("018", "Settings Table", "018_settings_table.sql");
1934
- }
1935
- }
1936
- const hasFormsTables = await this.checkTablesExist(["forms", "form_submissions", "form_files"]);
1937
- if (!appliedMigrations.has("029") && hasFormsTables) {
1938
- appliedMigrations.set("029", {
1939
- id: "029",
1940
- applied_at: (/* @__PURE__ */ new Date()).toISOString(),
1941
- name: "Add Forms System",
1942
- filename: "029_add_forms_system.sql"
1943
- });
1944
- await this.markMigrationApplied("029", "Add Forms System", "029_add_forms_system.sql");
1945
- } else if (appliedMigrations.has("029") && !hasFormsTables) {
1946
- console.log("[Migration] Migration 029 marked as applied but forms tables missing - will re-run");
1947
- appliedMigrations.delete("029");
1948
- await this.removeMigrationApplied("029");
1949
- }
1950
- if (!appliedMigrations.has("032")) {
1951
- const hasUserProfilesTable2 = await this.checkTablesExist(["user_profiles"]);
1952
- if (hasUserProfilesTable2) {
1953
- appliedMigrations.set("032", {
1954
- id: "032",
1955
- applied_at: (/* @__PURE__ */ new Date()).toISOString(),
1956
- name: "User Profiles",
1957
- filename: "032_user_profiles.sql"
1958
- });
1959
- await this.markMigrationApplied("032", "User Profiles", "032_user_profiles.sql");
1960
- }
1961
- }
1962
- const hasUserProfilesTable = await this.checkTablesExist(["user_profiles"]);
1963
- if (hasUserProfilesTable) {
1964
- const hasDataColumn = await this.checkColumnExists("user_profiles", "data");
1965
- if (!hasDataColumn) {
1966
- try {
1967
- await this.db.prepare(`ALTER TABLE user_profiles ADD COLUMN data TEXT DEFAULT '{}'`).run();
1968
- console.log("[Migration] Added missing data column to user_profiles");
1969
- } catch (error) {
1970
- const msg = error instanceof Error ? error.message : String(error);
1971
- if (!msg.includes("duplicate column name")) {
1972
- console.error("[Migration] Failed to add data column to user_profiles:", msg);
1973
- }
1974
- }
1975
- }
1976
- }
1977
- if (!appliedMigrations.has("035")) {
1978
- const hasDataCol = hasUserProfilesTable && await this.checkColumnExists("user_profiles", "data");
1979
- if (hasDataCol) {
1980
- appliedMigrations.set("035", {
1981
- id: "035",
1982
- applied_at: (/* @__PURE__ */ new Date()).toISOString(),
1983
- name: "User Profiles Data Column",
1984
- filename: "035_user_profiles_data_column.sql"
1985
- });
1986
- await this.markMigrationApplied("035", "User Profiles Data Column", "035_user_profiles_data_column.sql");
1987
- }
1988
- }
1989
- }
1990
- /**
1991
- * Check if specific tables exist in the database
1992
- */
1993
- async checkTablesExist(tableNames) {
1994
- try {
1995
- for (const tableName of tableNames) {
1996
- const result = await this.db.prepare(
1997
- `SELECT name FROM sqlite_master WHERE type='table' AND name=?`
1998
- ).bind(tableName).first();
1999
- if (!result) {
2000
- return false;
2001
- }
2002
- }
2003
- return true;
2004
- } catch (error) {
2005
- return false;
2006
- }
2007
- }
2008
- /**
2009
- * Check if a specific column exists in a table
2010
- */
2011
- async checkColumnExists(tableName, columnName) {
2012
- try {
2013
- const result = await this.db.prepare(
2014
- `SELECT * FROM pragma_table_info(?) WHERE name = ?`
2015
- ).bind(tableName, columnName).first();
2016
- return !!result;
2017
- } catch (error) {
2018
- return false;
2019
- }
2020
- }
2021
- /**
2022
- * Get migration status summary
2023
- */
2024
- async getMigrationStatus() {
2025
- await this.initializeMigrationsTable();
2026
- const migrations = await this.getAvailableMigrations();
2027
- const appliedMigrations = migrations.filter((m) => m.applied);
2028
- const pendingMigrations = migrations.filter((m) => !m.applied);
2029
- const lastApplied = appliedMigrations.length > 0 ? appliedMigrations[appliedMigrations.length - 1]?.appliedAt : void 0;
2030
- return {
2031
- totalMigrations: migrations.length,
2032
- appliedMigrations: appliedMigrations.length,
2033
- pendingMigrations: pendingMigrations.length,
2034
- lastApplied,
2035
- migrations
2036
- };
2037
- }
2038
- /**
2039
- * Mark a migration as applied
2040
- */
2041
- async markMigrationApplied(migrationId, name, filename) {
2042
- await this.initializeMigrationsTable();
2043
- await this.db.prepare(
2044
- "INSERT OR REPLACE INTO migrations (id, name, filename, applied_at) VALUES (?, ?, ?, CURRENT_TIMESTAMP)"
2045
- ).bind(migrationId, name, filename).run();
2046
- }
2047
- /**
2048
- * Remove a migration from the applied list (so it can be re-run)
2049
- */
2050
- async removeMigrationApplied(migrationId) {
2051
- await this.initializeMigrationsTable();
2052
- await this.db.prepare(
2053
- "DELETE FROM migrations WHERE id = ?"
2054
- ).bind(migrationId).run();
2055
- }
2056
- /**
2057
- * Check if a specific migration has been applied
2058
- */
2059
- async isMigrationApplied(migrationId) {
2060
- await this.initializeMigrationsTable();
2061
- const result = await this.db.prepare(
2062
- "SELECT COUNT(*) as count FROM migrations WHERE id = ?"
2063
- ).bind(migrationId).first();
2064
- return result?.count > 0;
2065
- }
2066
- /**
2067
- * Get the last applied migration
2068
- */
2069
- async getLastAppliedMigration() {
2070
- await this.initializeMigrationsTable();
2071
- const result = await this.db.prepare(
2072
- "SELECT id, name, filename, applied_at FROM migrations ORDER BY applied_at DESC LIMIT 1"
2073
- ).first();
2074
- if (!result) return null;
2075
- return {
2076
- id: result.id,
2077
- name: result.name,
2078
- filename: result.filename,
2079
- applied: true,
2080
- appliedAt: result.applied_at
2081
- };
2082
- }
2083
- /**
2084
- * Run pending migrations
2085
- */
2086
- async runPendingMigrations() {
2087
- await this.initializeMigrationsTable();
2088
- const status = await this.getMigrationStatus();
2089
- const pendingMigrations = status.migrations.filter((m) => !m.applied);
2090
- if (pendingMigrations.length === 0) {
2091
- return {
2092
- success: true,
2093
- message: "All migrations are up to date",
2094
- applied: [],
2095
- errors: []
2096
- };
2097
- }
2098
- const applied = [];
2099
- const errors = [];
2100
- for (const migration of pendingMigrations) {
2101
- try {
2102
- console.log(`[Migration] Applying ${migration.id}: ${migration.name}`);
2103
- await this.applyMigration(migration);
2104
- await this.markMigrationApplied(migration.id, migration.name, migration.filename);
2105
- applied.push(migration.id);
2106
- console.log(`[Migration] Successfully applied ${migration.id}`);
2107
- } catch (error) {
2108
- const errorMessage = error instanceof Error ? error.message : String(error);
2109
- console.error(`[Migration] Failed to apply migration ${migration.id}:`, errorMessage);
2110
- errors.push(`${migration.id}: ${errorMessage}`);
2111
- }
2112
- }
2113
- if (errors.length > 0 && applied.length === 0) {
2114
- return {
2115
- success: false,
2116
- message: `Failed to apply migrations: ${errors.join("; ")}`,
2117
- applied,
2118
- errors
2119
- };
2120
- }
2121
- return {
2122
- success: true,
2123
- message: applied.length > 0 ? `Applied ${applied.length} migration(s)${errors.length > 0 ? ` (${errors.length} failed)` : ""}` : "No migrations applied",
2124
- applied,
2125
- errors
2126
- };
2127
- }
2128
- /**
2129
- * Apply a specific migration
2130
- */
2131
- async applyMigration(migration) {
2132
- const migrationSQL = getMigrationSQLById(migration.id);
2133
- if (migrationSQL === null) {
2134
- throw new Error(`Migration SQL not found for ${migration.id}`);
2135
- }
2136
- if (migrationSQL.trim() === "") {
2137
- console.log(`[Migration] Skipping empty migration ${migration.id}`);
2138
- return;
2139
- }
2140
- const statements = this.splitSQLStatements(migrationSQL);
2141
- for (const statement of statements) {
2142
- if (statement.trim()) {
2143
- try {
2144
- await this.db.prepare(statement).run();
2145
- } catch (error) {
2146
- const errorMessage = error instanceof Error ? error.message : String(error);
2147
- if (errorMessage.includes("already exists") || errorMessage.includes("duplicate column name") || errorMessage.includes("UNIQUE constraint failed")) {
2148
- console.log(`[Migration] Skipping (already exists): ${statement.substring(0, 50)}...`);
2149
- continue;
2150
- }
2151
- console.error(`[Migration] Error executing statement: ${statement.substring(0, 100)}...`);
2152
- throw error;
2153
- }
2154
- }
2155
- }
2156
- }
2157
- /**
2158
- * Split SQL into statements, handling CREATE TRIGGER properly
2159
- */
2160
- splitSQLStatements(sql) {
2161
- const statements = [];
2162
- let current = "";
2163
- let inTrigger = false;
2164
- const lines = sql.split("\n");
2165
- for (const line of lines) {
2166
- const trimmed = line.trim();
2167
- if (trimmed.startsWith("--") || trimmed.length === 0) {
2168
- continue;
2169
- }
2170
- if (trimmed.toUpperCase().includes("CREATE TRIGGER")) {
2171
- inTrigger = true;
2172
- }
2173
- current += line + "\n";
2174
- if (inTrigger && trimmed.toUpperCase() === "END;") {
2175
- statements.push(current.trim());
2176
- current = "";
2177
- inTrigger = false;
2178
- } else if (!inTrigger && trimmed.endsWith(";")) {
2179
- statements.push(current.trim());
2180
- current = "";
2181
- }
2182
- }
2183
- if (current.trim()) {
2184
- statements.push(current.trim());
2185
- }
2186
- return statements.filter((s) => s.length > 0);
2187
- }
2188
- /**
2189
- * Validate database schema
2190
- */
2191
- async validateSchema() {
2192
- const issues = [];
2193
- const requiredTables = [
2194
- "users",
2195
- "content",
2196
- "collections",
2197
- "media"
2198
- ];
2199
- for (const table of requiredTables) {
2200
- try {
2201
- await this.db.prepare(`SELECT COUNT(*) FROM ${table} LIMIT 1`).first();
2202
- } catch (error) {
2203
- issues.push(`Missing table: ${table}`);
2204
- }
2205
- }
2206
- const hasManagedColumn = await this.checkColumnExists("collections", "managed");
2207
- if (!hasManagedColumn) {
2208
- issues.push("Missing column: collections.managed");
2209
- }
2210
- return {
2211
- valid: issues.length === 0,
2212
- issues
2213
- };
2214
- }
2215
- };
2216
-
2217
- exports.MigrationService = MigrationService;
2218
- //# sourceMappingURL=chunk-RLMUFFUD.cjs.map
2219
- //# sourceMappingURL=chunk-RLMUFFUD.cjs.map