dineway 0.1.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 (96) hide show
  1. package/LICENSE +9 -0
  2. package/README.md +89 -0
  3. package/dist/adapters-BlzWJG82.d.mts +106 -0
  4. package/dist/apply-CAPvMfoU.mjs +1339 -0
  5. package/dist/astro/index.d.mts +50 -0
  6. package/dist/astro/index.mjs +1326 -0
  7. package/dist/astro/middleware/auth.d.mts +30 -0
  8. package/dist/astro/middleware/auth.mjs +708 -0
  9. package/dist/astro/middleware/redirect.d.mts +21 -0
  10. package/dist/astro/middleware/redirect.mjs +62 -0
  11. package/dist/astro/middleware/request-context.d.mts +17 -0
  12. package/dist/astro/middleware/request-context.mjs +1371 -0
  13. package/dist/astro/middleware/setup.d.mts +19 -0
  14. package/dist/astro/middleware/setup.mjs +46 -0
  15. package/dist/astro/middleware.d.mts +12 -0
  16. package/dist/astro/middleware.mjs +1716 -0
  17. package/dist/astro/types.d.mts +269 -0
  18. package/dist/astro/types.mjs +1 -0
  19. package/dist/base64-F8-DUraK.mjs +58 -0
  20. package/dist/byline-DeWCMU_i.mjs +234 -0
  21. package/dist/bylines-DyqBV9EQ.mjs +137 -0
  22. package/dist/chunk-ClPoSABd.mjs +21 -0
  23. package/dist/cli/index.d.mts +1 -0
  24. package/dist/cli/index.mjs +3987 -0
  25. package/dist/client/external-auth-headers.d.mts +38 -0
  26. package/dist/client/external-auth-headers.mjs +101 -0
  27. package/dist/client/index.d.mts +397 -0
  28. package/dist/client/index.mjs +345 -0
  29. package/dist/config-Cq8H0SfX.mjs +46 -0
  30. package/dist/connection-C9pxzuag.mjs +52 -0
  31. package/dist/content-zSgdNmnt.mjs +836 -0
  32. package/dist/db/index.d.mts +4 -0
  33. package/dist/db/index.mjs +62 -0
  34. package/dist/db/libsql.d.mts +10 -0
  35. package/dist/db/libsql.mjs +21 -0
  36. package/dist/db/postgres.d.mts +10 -0
  37. package/dist/db/postgres.mjs +29 -0
  38. package/dist/db/sqlite.d.mts +10 -0
  39. package/dist/db/sqlite.mjs +15 -0
  40. package/dist/default-WYlzADZL.mjs +80 -0
  41. package/dist/dialect-helpers-B9uSp2GJ.mjs +89 -0
  42. package/dist/error-DrxtnGPg.mjs +26 -0
  43. package/dist/index-C-jx21qs.d.mts +4771 -0
  44. package/dist/index.d.mts +16 -0
  45. package/dist/index.mjs +30 -0
  46. package/dist/load-C6FCD1FU.mjs +27 -0
  47. package/dist/loader-qKmo0wAY.mjs +446 -0
  48. package/dist/manifest-schema-CTSEyIJ3.mjs +186 -0
  49. package/dist/media/index.d.mts +25 -0
  50. package/dist/media/index.mjs +54 -0
  51. package/dist/media/local-runtime.d.mts +38 -0
  52. package/dist/media/local-runtime.mjs +132 -0
  53. package/dist/media-DMTr80Gv.mjs +199 -0
  54. package/dist/mode-BlyYtIFO.mjs +22 -0
  55. package/dist/page/index.d.mts +148 -0
  56. package/dist/page/index.mjs +419 -0
  57. package/dist/placeholder-B3knXwNc.mjs +267 -0
  58. package/dist/placeholder-bOx1xCTY.d.mts +283 -0
  59. package/dist/plugin-utils.d.mts +57 -0
  60. package/dist/plugin-utils.mjs +77 -0
  61. package/dist/plugins/adapt-sandbox-entry.d.mts +21 -0
  62. package/dist/plugins/adapt-sandbox-entry.mjs +112 -0
  63. package/dist/query-BiaPl_g2.mjs +459 -0
  64. package/dist/redirect-JPqLAbxa.mjs +328 -0
  65. package/dist/registry-DSd1GWB8.mjs +851 -0
  66. package/dist/request-context.d.mts +49 -0
  67. package/dist/request-context.mjs +42 -0
  68. package/dist/runner-B5l1JfOj.d.mts +26 -0
  69. package/dist/runner-BGUGywgG.mjs +1529 -0
  70. package/dist/runtime.d.mts +25 -0
  71. package/dist/runtime.mjs +41 -0
  72. package/dist/search-BNruJHDL.mjs +11054 -0
  73. package/dist/seed/index.d.mts +3 -0
  74. package/dist/seed/index.mjs +15 -0
  75. package/dist/seo/index.d.mts +69 -0
  76. package/dist/seo/index.mjs +69 -0
  77. package/dist/storage/local.d.mts +38 -0
  78. package/dist/storage/local.mjs +165 -0
  79. package/dist/storage/s3.d.mts +31 -0
  80. package/dist/storage/s3.mjs +174 -0
  81. package/dist/tokens-4vgYuXsZ.mjs +170 -0
  82. package/dist/transport-C5FYnid7.mjs +417 -0
  83. package/dist/transport-gIL-e43D.d.mts +41 -0
  84. package/dist/types-BawVha09.mjs +30 -0
  85. package/dist/types-BgQeVaPj.d.mts +192 -0
  86. package/dist/types-CLLdsG3g.d.mts +103 -0
  87. package/dist/types-D38djUXv.d.mts +1196 -0
  88. package/dist/types-DShnjzb6.mjs +15 -0
  89. package/dist/types-DkvMXalq.d.mts +425 -0
  90. package/dist/types-DuNbGKjF.mjs +74 -0
  91. package/dist/types-ju-_ORz7.d.mts +182 -0
  92. package/dist/validate-CXnRKfJK.mjs +327 -0
  93. package/dist/validate-CqRJb_xU.mjs +96 -0
  94. package/dist/validate-DVKJJ-M_.d.mts +377 -0
  95. package/locals.d.ts +47 -0
  96. package/package.json +313 -0
@@ -0,0 +1,1529 @@
1
+ import { t as __exportAll } from "./chunk-ClPoSABd.mjs";
2
+ import { a as isSqlite, n as currentTimestamp, r as currentTimestampValue, s as listTablesLike, t as binaryType } from "./dialect-helpers-B9uSp2GJ.mjs";
3
+ import { t as validateIdentifier } from "./validate-CqRJb_xU.mjs";
4
+ import { Migrator, sql } from "kysely";
5
+
6
+ //#region src/database/migrations/001_initial.ts
7
+ var _001_initial_exports = /* @__PURE__ */ __exportAll({
8
+ down: () => down$32,
9
+ up: () => up$32
10
+ });
11
+ /**
12
+ * Initial schema migration
13
+ *
14
+ * Note: Content tables (ec_posts, ec_pages, etc.) are created dynamically
15
+ * by the SchemaRegistry when collections are added via the admin UI.
16
+ * This migration only creates system tables.
17
+ */
18
+ async function up$32(db) {
19
+ await db.schema.createTable("revisions").ifNotExists().addColumn("id", "text", (col) => col.primaryKey()).addColumn("collection", "text", (col) => col.notNull()).addColumn("entry_id", "text", (col) => col.notNull()).addColumn("data", "text", (col) => col.notNull()).addColumn("author_id", "text").addColumn("created_at", "text", (col) => col.defaultTo(currentTimestamp(db))).execute();
20
+ await db.schema.createIndex("idx_revisions_entry").ifNotExists().on("revisions").columns(["collection", "entry_id"]).execute();
21
+ await db.schema.createTable("taxonomies").ifNotExists().addColumn("id", "text", (col) => col.primaryKey()).addColumn("name", "text", (col) => col.notNull()).addColumn("slug", "text", (col) => col.notNull()).addColumn("label", "text", (col) => col.notNull()).addColumn("parent_id", "text").addColumn("data", "text").addUniqueConstraint("taxonomies_name_slug_unique", ["name", "slug"]).addForeignKeyConstraint("taxonomies_parent_fk", ["parent_id"], "taxonomies", ["id"], (cb) => cb.onDelete("set null")).execute();
22
+ await db.schema.createIndex("idx_taxonomies_name").ifNotExists().on("taxonomies").column("name").execute();
23
+ await db.schema.createTable("content_taxonomies").ifNotExists().addColumn("collection", "text", (col) => col.notNull()).addColumn("entry_id", "text", (col) => col.notNull()).addColumn("taxonomy_id", "text", (col) => col.notNull()).addPrimaryKeyConstraint("content_taxonomies_pk", [
24
+ "collection",
25
+ "entry_id",
26
+ "taxonomy_id"
27
+ ]).addForeignKeyConstraint("content_taxonomies_taxonomy_fk", ["taxonomy_id"], "taxonomies", ["id"], (cb) => cb.onDelete("cascade")).execute();
28
+ await db.schema.createTable("media").ifNotExists().addColumn("id", "text", (col) => col.primaryKey()).addColumn("filename", "text", (col) => col.notNull()).addColumn("mime_type", "text", (col) => col.notNull()).addColumn("size", "integer").addColumn("width", "integer").addColumn("height", "integer").addColumn("alt", "text").addColumn("caption", "text").addColumn("storage_key", "text", (col) => col.notNull()).addColumn("content_hash", "text").addColumn("created_at", "text", (col) => col.defaultTo(currentTimestamp(db))).addColumn("author_id", "text").execute();
29
+ await db.schema.createIndex("idx_media_content_hash").ifNotExists().on("media").column("content_hash").execute();
30
+ await db.schema.createTable("users").ifNotExists().addColumn("id", "text", (col) => col.primaryKey()).addColumn("email", "text", (col) => col.notNull().unique()).addColumn("password_hash", "text", (col) => col.notNull()).addColumn("name", "text").addColumn("role", "text", (col) => col.defaultTo("subscriber")).addColumn("avatar_id", "text").addColumn("data", "text").addColumn("created_at", "text", (col) => col.defaultTo(currentTimestamp(db))).execute();
31
+ await db.schema.createIndex("idx_users_email").ifNotExists().on("users").column("email").execute();
32
+ await db.schema.createTable("options").ifNotExists().addColumn("name", "text", (col) => col.primaryKey()).addColumn("value", "text", (col) => col.notNull()).execute();
33
+ await db.schema.createTable("audit_logs").ifNotExists().addColumn("id", "text", (col) => col.primaryKey()).addColumn("timestamp", "text", (col) => col.defaultTo(currentTimestamp(db))).addColumn("actor_id", "text").addColumn("actor_ip", "text").addColumn("action", "text", (col) => col.notNull()).addColumn("resource_type", "text").addColumn("resource_id", "text").addColumn("details", "text").addColumn("status", "text").execute();
34
+ await db.schema.createIndex("idx_audit_actor").ifNotExists().on("audit_logs").column("actor_id").execute();
35
+ await db.schema.createIndex("idx_audit_action").ifNotExists().on("audit_logs").column("action").execute();
36
+ await db.schema.createIndex("idx_audit_timestamp").ifNotExists().on("audit_logs").column("timestamp").execute();
37
+ }
38
+ async function down$32(db) {
39
+ await db.schema.dropTable("audit_logs").execute();
40
+ await db.schema.dropTable("options").execute();
41
+ await db.schema.dropTable("users").execute();
42
+ await db.schema.dropTable("media").execute();
43
+ await db.schema.dropTable("content_taxonomies").execute();
44
+ await db.schema.dropTable("taxonomies").execute();
45
+ await db.schema.dropTable("revisions").execute();
46
+ }
47
+
48
+ //#endregion
49
+ //#region src/database/migrations/002_media_status.ts
50
+ var _002_media_status_exports = /* @__PURE__ */ __exportAll({
51
+ down: () => down$31,
52
+ up: () => up$31
53
+ });
54
+ /**
55
+ * Add status column to media table for tracking upload state.
56
+ * Status values: 'pending' | 'ready' | 'failed'
57
+ */
58
+ async function up$31(db) {
59
+ await db.schema.alterTable("media").addColumn("status", "text", (col) => col.notNull().defaultTo("ready")).execute();
60
+ await db.schema.createIndex("idx_media_status").on("media").column("status").execute();
61
+ }
62
+ async function down$31(db) {
63
+ await db.schema.dropIndex("idx_media_status").execute();
64
+ }
65
+
66
+ //#endregion
67
+ //#region src/database/migrations/003_schema_registry.ts
68
+ var _003_schema_registry_exports = /* @__PURE__ */ __exportAll({
69
+ down: () => down$30,
70
+ up: () => up$30
71
+ });
72
+ /**
73
+ * Migration: Schema Registry Tables
74
+ *
75
+ * Creates the schema registry tables that store collection and field definitions.
76
+ * This enables dynamic schema management where the database is the source of truth.
77
+ */
78
+ async function up$30(db) {
79
+ await db.schema.createTable("_dineway_collections").addColumn("id", "text", (col) => col.primaryKey()).addColumn("slug", "text", (col) => col.notNull().unique()).addColumn("label", "text", (col) => col.notNull()).addColumn("label_singular", "text").addColumn("description", "text").addColumn("icon", "text").addColumn("supports", "text").addColumn("source", "text").addColumn("created_at", "text", (col) => col.defaultTo(currentTimestamp(db))).addColumn("updated_at", "text", (col) => col.defaultTo(currentTimestamp(db))).execute();
80
+ await db.schema.createTable("_dineway_fields").addColumn("id", "text", (col) => col.primaryKey()).addColumn("collection_id", "text", (col) => col.notNull()).addColumn("slug", "text", (col) => col.notNull()).addColumn("label", "text", (col) => col.notNull()).addColumn("type", "text", (col) => col.notNull()).addColumn("column_type", "text", (col) => col.notNull()).addColumn("required", "integer", (col) => col.defaultTo(0)).addColumn("unique", "integer", (col) => col.defaultTo(0)).addColumn("default_value", "text").addColumn("validation", "text").addColumn("widget", "text").addColumn("options", "text").addColumn("sort_order", "integer", (col) => col.defaultTo(0)).addColumn("created_at", "text", (col) => col.defaultTo(currentTimestamp(db))).addForeignKeyConstraint("fields_collection_fk", ["collection_id"], "_dineway_collections", ["id"], (cb) => cb.onDelete("cascade")).execute();
81
+ await db.schema.createIndex("idx_fields_collection_slug").on("_dineway_fields").columns(["collection_id", "slug"]).unique().execute();
82
+ await db.schema.createIndex("idx_fields_collection").on("_dineway_fields").column("collection_id").execute();
83
+ await db.schema.createIndex("idx_fields_sort").on("_dineway_fields").columns(["collection_id", "sort_order"]).execute();
84
+ }
85
+ async function down$30(db) {
86
+ await db.schema.dropTable("_dineway_fields").execute();
87
+ await db.schema.dropTable("_dineway_collections").execute();
88
+ }
89
+
90
+ //#endregion
91
+ //#region src/database/migrations/004_plugins.ts
92
+ var _004_plugins_exports = /* @__PURE__ */ __exportAll({
93
+ down: () => down$29,
94
+ up: () => up$29
95
+ });
96
+ /**
97
+ * Migration: Plugin System Tables
98
+ *
99
+ * Creates the plugin storage table and plugin state tracking.
100
+ * Plugin storage uses a document store with declared indexes.
101
+ *
102
+ * @see PLUGIN-SYSTEM.md § Plugin Storage
103
+ */
104
+ async function up$29(db) {
105
+ await db.schema.createTable("_plugin_storage").addColumn("plugin_id", "text", (col) => col.notNull()).addColumn("collection", "text", (col) => col.notNull()).addColumn("id", "text", (col) => col.notNull()).addColumn("data", "text", (col) => col.notNull()).addColumn("created_at", "text", (col) => col.defaultTo(currentTimestamp(db))).addColumn("updated_at", "text", (col) => col.defaultTo(currentTimestamp(db))).addPrimaryKeyConstraint("pk_plugin_storage", [
106
+ "plugin_id",
107
+ "collection",
108
+ "id"
109
+ ]).execute();
110
+ await db.schema.createIndex("idx_plugin_storage_list").on("_plugin_storage").columns([
111
+ "plugin_id",
112
+ "collection",
113
+ "created_at"
114
+ ]).execute();
115
+ await db.schema.createTable("_plugin_state").addColumn("plugin_id", "text", (col) => col.primaryKey()).addColumn("version", "text", (col) => col.notNull()).addColumn("status", "text", (col) => col.notNull().defaultTo("installed")).addColumn("installed_at", "text", (col) => col.defaultTo(currentTimestamp(db))).addColumn("activated_at", "text").addColumn("deactivated_at", "text").addColumn("data", "text").execute();
116
+ await db.schema.createTable("_plugin_indexes").addColumn("plugin_id", "text", (col) => col.notNull()).addColumn("collection", "text", (col) => col.notNull()).addColumn("index_name", "text", (col) => col.notNull()).addColumn("fields", "text", (col) => col.notNull()).addColumn("created_at", "text", (col) => col.defaultTo(currentTimestamp(db))).addPrimaryKeyConstraint("pk_plugin_indexes", [
117
+ "plugin_id",
118
+ "collection",
119
+ "index_name"
120
+ ]).execute();
121
+ }
122
+ async function down$29(db) {
123
+ await db.schema.dropTable("_plugin_indexes").execute();
124
+ await db.schema.dropTable("_plugin_state").execute();
125
+ await db.schema.dropTable("_plugin_storage").execute();
126
+ }
127
+
128
+ //#endregion
129
+ //#region src/database/migrations/005_menus.ts
130
+ var _005_menus_exports = /* @__PURE__ */ __exportAll({
131
+ down: () => down$28,
132
+ up: () => up$28
133
+ });
134
+ /**
135
+ * Navigation Menus migration
136
+ *
137
+ * Creates tables for admin-editable navigation menus.
138
+ * Menu items can reference content entries, taxonomy terms, or custom URLs.
139
+ */
140
+ async function up$28(db) {
141
+ await db.schema.createTable("_dineway_menus").addColumn("id", "text", (col) => col.primaryKey()).addColumn("name", "text", (col) => col.notNull().unique()).addColumn("label", "text", (col) => col.notNull()).addColumn("created_at", "text", (col) => col.defaultTo(currentTimestamp(db))).addColumn("updated_at", "text", (col) => col.defaultTo(currentTimestamp(db))).execute();
142
+ await db.schema.createTable("_dineway_menu_items").addColumn("id", "text", (col) => col.primaryKey()).addColumn("menu_id", "text", (col) => col.notNull()).addColumn("parent_id", "text").addColumn("sort_order", "integer", (col) => col.notNull().defaultTo(0)).addColumn("type", "text", (col) => col.notNull()).addColumn("reference_collection", "text").addColumn("reference_id", "text").addColumn("custom_url", "text").addColumn("label", "text", (col) => col.notNull()).addColumn("title_attr", "text").addColumn("target", "text").addColumn("css_classes", "text").addColumn("created_at", "text", (col) => col.defaultTo(currentTimestamp(db))).addForeignKeyConstraint("menu_items_menu_fk", ["menu_id"], "_dineway_menus", ["id"], (cb) => cb.onDelete("cascade")).addForeignKeyConstraint("menu_items_parent_fk", ["parent_id"], "_dineway_menu_items", ["id"], (cb) => cb.onDelete("cascade")).execute();
143
+ await db.schema.createIndex("idx_menu_items_menu").on("_dineway_menu_items").columns(["menu_id", "sort_order"]).execute();
144
+ await db.schema.createIndex("idx_menu_items_parent").on("_dineway_menu_items").column("parent_id").execute();
145
+ }
146
+ async function down$28(db) {
147
+ await db.schema.dropTable("_dineway_menu_items").execute();
148
+ await db.schema.dropTable("_dineway_menus").execute();
149
+ }
150
+
151
+ //#endregion
152
+ //#region src/database/migrations/006_taxonomy_defs.ts
153
+ var _006_taxonomy_defs_exports = /* @__PURE__ */ __exportAll({
154
+ down: () => down$27,
155
+ up: () => up$27
156
+ });
157
+ /**
158
+ * Taxonomy definitions migration
159
+ *
160
+ * Adds _dineway_taxonomy_defs table to store taxonomy definitions (category, tag, custom)
161
+ * and seeds default category and tag taxonomies.
162
+ */
163
+ async function up$27(db) {
164
+ await db.schema.createTable("_dineway_taxonomy_defs").addColumn("id", "text", (col) => col.primaryKey()).addColumn("name", "text", (col) => col.notNull().unique()).addColumn("label", "text", (col) => col.notNull()).addColumn("label_singular", "text").addColumn("hierarchical", "integer", (col) => col.defaultTo(0)).addColumn("collections", "text").addColumn("created_at", "text", (col) => col.defaultTo(currentTimestamp(db))).execute();
165
+ await db.insertInto("_dineway_taxonomy_defs").values([{
166
+ id: "taxdef_category",
167
+ name: "category",
168
+ label: "Categories",
169
+ label_singular: "Category",
170
+ hierarchical: 1,
171
+ collections: JSON.stringify(["posts"])
172
+ }, {
173
+ id: "taxdef_tag",
174
+ name: "tag",
175
+ label: "Tags",
176
+ label_singular: "Tag",
177
+ hierarchical: 0,
178
+ collections: JSON.stringify(["posts"])
179
+ }]).execute();
180
+ }
181
+ async function down$27(db) {
182
+ await db.schema.dropTable("_dineway_taxonomy_defs").execute();
183
+ }
184
+
185
+ //#endregion
186
+ //#region src/database/migrations/007_widgets.ts
187
+ var _007_widgets_exports = /* @__PURE__ */ __exportAll({
188
+ down: () => down$26,
189
+ up: () => up$26
190
+ });
191
+ async function up$26(db) {
192
+ await db.schema.createTable("_dineway_widget_areas").addColumn("id", "text", (col) => col.primaryKey()).addColumn("name", "text", (col) => col.notNull().unique()).addColumn("label", "text", (col) => col.notNull()).addColumn("description", "text").addColumn("created_at", "text", (col) => col.defaultTo(sql`CURRENT_TIMESTAMP`)).execute();
193
+ await db.schema.createTable("_dineway_widgets").addColumn("id", "text", (col) => col.primaryKey()).addColumn("area_id", "text", (col) => col.notNull().references("_dineway_widget_areas.id").onDelete("cascade")).addColumn("sort_order", "integer", (col) => col.notNull().defaultTo(0)).addColumn("type", "text", (col) => col.notNull()).addColumn("title", "text").addColumn("content", "text").addColumn("menu_name", "text").addColumn("component_id", "text").addColumn("component_props", "text").addColumn("created_at", "text", (col) => col.defaultTo(sql`CURRENT_TIMESTAMP`)).execute();
194
+ await db.schema.createIndex("idx_widgets_area").on("_dineway_widgets").columns(["area_id", "sort_order"]).execute();
195
+ }
196
+ async function down$26(db) {
197
+ await db.schema.dropTable("_dineway_widgets").execute();
198
+ await db.schema.dropTable("_dineway_widget_areas").execute();
199
+ }
200
+
201
+ //#endregion
202
+ //#region src/database/migrations/008_auth.ts
203
+ var _008_auth_exports = /* @__PURE__ */ __exportAll({
204
+ down: () => down$25,
205
+ up: () => up$25
206
+ });
207
+ /**
208
+ * Auth migration - passkey-first authentication
209
+ *
210
+ * Changes:
211
+ * - Removes password_hash from users (no passwords)
212
+ * - Adds role as integer (RBAC levels)
213
+ * - Adds email_verified, avatar_url, updated_at to users
214
+ * - Creates credentials table (passkeys)
215
+ * - Creates auth_tokens table (magic links, invites)
216
+ * - Creates oauth_accounts table (external provider links)
217
+ * - Creates allowed_domains table (self-signup)
218
+ */
219
+ async function up$25(db) {
220
+ await db.schema.createTable("users_new").addColumn("id", "text", (col) => col.primaryKey()).addColumn("email", "text", (col) => col.notNull().unique()).addColumn("name", "text").addColumn("avatar_url", "text").addColumn("role", "integer", (col) => col.notNull().defaultTo(10)).addColumn("email_verified", "integer", (col) => col.notNull().defaultTo(0)).addColumn("data", "text").addColumn("created_at", "text", (col) => col.defaultTo(currentTimestamp(db))).addColumn("updated_at", "text", (col) => col.defaultTo(currentTimestamp(db))).execute();
221
+ await sql`
222
+ INSERT INTO users_new (id, email, name, role, data, created_at, updated_at)
223
+ SELECT
224
+ id,
225
+ email,
226
+ name,
227
+ CASE role
228
+ WHEN 'admin' THEN 50
229
+ WHEN 'editor' THEN 40
230
+ WHEN 'author' THEN 30
231
+ WHEN 'contributor' THEN 20
232
+ ELSE 10
233
+ END,
234
+ data,
235
+ created_at,
236
+ ${currentTimestampValue(db)}
237
+ FROM users
238
+ `.execute(db);
239
+ await db.schema.dropTable("users").execute();
240
+ await sql`ALTER TABLE users_new RENAME TO users`.execute(db);
241
+ await db.schema.createIndex("idx_users_email").on("users").column("email").execute();
242
+ await db.schema.createTable("credentials").addColumn("id", "text", (col) => col.primaryKey()).addColumn("user_id", "text", (col) => col.notNull()).addColumn("public_key", binaryType(db), (col) => col.notNull()).addColumn("counter", "integer", (col) => col.notNull().defaultTo(0)).addColumn("device_type", "text", (col) => col.notNull()).addColumn("backed_up", "integer", (col) => col.notNull().defaultTo(0)).addColumn("transports", "text").addColumn("name", "text").addColumn("created_at", "text", (col) => col.defaultTo(currentTimestamp(db))).addColumn("last_used_at", "text", (col) => col.defaultTo(currentTimestamp(db))).addForeignKeyConstraint("credentials_user_fk", ["user_id"], "users", ["id"], (cb) => cb.onDelete("cascade")).execute();
243
+ await db.schema.createIndex("idx_credentials_user").on("credentials").column("user_id").execute();
244
+ await db.schema.createTable("auth_tokens").addColumn("hash", "text", (col) => col.primaryKey()).addColumn("user_id", "text").addColumn("email", "text").addColumn("type", "text", (col) => col.notNull()).addColumn("role", "integer").addColumn("invited_by", "text").addColumn("expires_at", "text", (col) => col.notNull()).addColumn("created_at", "text", (col) => col.defaultTo(currentTimestamp(db))).addForeignKeyConstraint("auth_tokens_user_fk", ["user_id"], "users", ["id"], (cb) => cb.onDelete("cascade")).addForeignKeyConstraint("auth_tokens_invited_by_fk", ["invited_by"], "users", ["id"], (cb) => cb.onDelete("set null")).execute();
245
+ await db.schema.createIndex("idx_auth_tokens_email").on("auth_tokens").column("email").execute();
246
+ await db.schema.createTable("oauth_accounts").addColumn("provider", "text", (col) => col.notNull()).addColumn("provider_account_id", "text", (col) => col.notNull()).addColumn("user_id", "text", (col) => col.notNull()).addColumn("created_at", "text", (col) => col.defaultTo(currentTimestamp(db))).addPrimaryKeyConstraint("oauth_accounts_pk", ["provider", "provider_account_id"]).addForeignKeyConstraint("oauth_accounts_user_fk", ["user_id"], "users", ["id"], (cb) => cb.onDelete("cascade")).execute();
247
+ await db.schema.createIndex("idx_oauth_accounts_user").on("oauth_accounts").column("user_id").execute();
248
+ await db.schema.createTable("allowed_domains").addColumn("domain", "text", (col) => col.primaryKey()).addColumn("default_role", "integer", (col) => col.notNull().defaultTo(20)).addColumn("enabled", "integer", (col) => col.notNull().defaultTo(1)).addColumn("created_at", "text", (col) => col.defaultTo(currentTimestamp(db))).execute();
249
+ await db.schema.createTable("auth_challenges").addColumn("challenge", "text", (col) => col.primaryKey()).addColumn("type", "text", (col) => col.notNull()).addColumn("user_id", "text").addColumn("data", "text").addColumn("expires_at", "text", (col) => col.notNull()).addColumn("created_at", "text", (col) => col.defaultTo(currentTimestamp(db))).execute();
250
+ await db.schema.createIndex("idx_auth_challenges_expires").on("auth_challenges").column("expires_at").execute();
251
+ }
252
+ async function down$25(db) {
253
+ await db.schema.dropTable("auth_challenges").execute();
254
+ await db.schema.dropTable("allowed_domains").execute();
255
+ await db.schema.dropTable("oauth_accounts").execute();
256
+ await db.schema.dropTable("auth_tokens").execute();
257
+ await db.schema.dropTable("credentials").execute();
258
+ await db.schema.createTable("users_old").addColumn("id", "text", (col) => col.primaryKey()).addColumn("email", "text", (col) => col.notNull().unique()).addColumn("password_hash", "text", (col) => col.notNull()).addColumn("name", "text").addColumn("role", "text", (col) => col.defaultTo("subscriber")).addColumn("avatar_id", "text").addColumn("data", "text").addColumn("created_at", "text", (col) => col.defaultTo(currentTimestamp(db))).execute();
259
+ await sql`
260
+ INSERT INTO users_old (id, email, password_hash, name, role, data, created_at)
261
+ SELECT
262
+ id,
263
+ email,
264
+ '', -- No way to restore password
265
+ name,
266
+ CASE role
267
+ WHEN 50 THEN 'admin'
268
+ WHEN 40 THEN 'editor'
269
+ WHEN 30 THEN 'author'
270
+ WHEN 20 THEN 'contributor'
271
+ ELSE 'subscriber'
272
+ END,
273
+ data,
274
+ created_at
275
+ FROM users
276
+ `.execute(db);
277
+ await db.schema.dropTable("users").execute();
278
+ await sql`ALTER TABLE users_old RENAME TO users`.execute(db);
279
+ await db.schema.createIndex("idx_users_email").on("users").column("email").execute();
280
+ }
281
+
282
+ //#endregion
283
+ //#region src/database/migrations/009_user_disabled.ts
284
+ var _009_user_disabled_exports = /* @__PURE__ */ __exportAll({
285
+ down: () => down$24,
286
+ up: () => up$24
287
+ });
288
+ /**
289
+ * User disabled column - for soft-disabling users
290
+ *
291
+ * Changes:
292
+ * - Adds disabled column to users table (INTEGER, default 0)
293
+ * - Disabled users cannot log in
294
+ */
295
+ async function up$24(db) {
296
+ await sql`ALTER TABLE users ADD COLUMN disabled INTEGER NOT NULL DEFAULT 0`.execute(db);
297
+ await db.schema.createIndex("idx_users_disabled").on("users").column("disabled").execute();
298
+ }
299
+ async function down$24(db) {
300
+ await db.schema.dropIndex("idx_users_disabled").execute();
301
+ }
302
+
303
+ //#endregion
304
+ //#region src/database/migrations/011_sections.ts
305
+ var _011_sections_exports = /* @__PURE__ */ __exportAll({
306
+ down: () => down$23,
307
+ up: () => up$23
308
+ });
309
+ /**
310
+ * Migration: Add sections tables and performance indexes
311
+ *
312
+ * Sections are reusable content blocks that can be inserted into any Portable Text field.
313
+ * They provide a library of pre-built page sections (heroes, CTAs, testimonials, etc.)
314
+ * that content authors can browse and insert with a single click.
315
+ */
316
+ async function up$23(db) {
317
+ await db.schema.createTable("_dineway_section_categories").addColumn("id", "text", (col) => col.primaryKey()).addColumn("slug", "text", (col) => col.notNull().unique()).addColumn("label", "text", (col) => col.notNull()).addColumn("sort_order", "integer", (col) => col.defaultTo(0)).addColumn("created_at", "text", (col) => col.defaultTo(sql`CURRENT_TIMESTAMP`)).execute();
318
+ await db.schema.createTable("_dineway_sections").addColumn("id", "text", (col) => col.primaryKey()).addColumn("slug", "text", (col) => col.notNull().unique()).addColumn("title", "text", (col) => col.notNull()).addColumn("description", "text").addColumn("category_id", "text", (col) => col.references("_dineway_section_categories.id").onDelete("set null")).addColumn("keywords", "text").addColumn("content", "text", (col) => col.notNull()).addColumn("preview_media_id", "text").addColumn("source", "text", (col) => col.notNull().defaultTo("user")).addColumn("theme_id", "text").addColumn("created_at", "text", (col) => col.defaultTo(sql`CURRENT_TIMESTAMP`)).addColumn("updated_at", "text", (col) => col.defaultTo(sql`CURRENT_TIMESTAMP`)).execute();
319
+ await db.schema.createIndex("idx_sections_category").on("_dineway_sections").columns(["category_id"]).execute();
320
+ await db.schema.createIndex("idx_sections_source").on("_dineway_sections").columns(["source"]).execute();
321
+ }
322
+ async function down$23(db) {
323
+ await db.schema.dropIndex("idx_content_taxonomies_term").execute();
324
+ await db.schema.dropIndex("idx_media_mime_type").execute();
325
+ await db.schema.dropTable("_dineway_sections").execute();
326
+ await db.schema.dropTable("_dineway_section_categories").execute();
327
+ }
328
+
329
+ //#endregion
330
+ //#region src/database/migrations/012_search.ts
331
+ var _012_search_exports = /* @__PURE__ */ __exportAll({
332
+ down: () => down$22,
333
+ up: () => up$22
334
+ });
335
+ /**
336
+ * Migration: Search Support
337
+ *
338
+ * Adds search configuration to collections and searchable flag to fields.
339
+ * FTS5 tables are created dynamically when search is enabled for a collection.
340
+ */
341
+ async function up$22(db) {
342
+ await db.schema.alterTable("_dineway_collections").addColumn("search_config", "text").execute();
343
+ await db.schema.alterTable("_dineway_fields").addColumn("searchable", "integer", (col) => col.defaultTo(0)).execute();
344
+ }
345
+ async function down$22(db) {
346
+ await db.schema.alterTable("_dineway_fields").dropColumn("searchable").execute();
347
+ await db.schema.alterTable("_dineway_collections").dropColumn("search_config").execute();
348
+ }
349
+
350
+ //#endregion
351
+ //#region src/database/migrations/013_scheduled_publishing.ts
352
+ var _013_scheduled_publishing_exports = /* @__PURE__ */ __exportAll({
353
+ down: () => down$21,
354
+ up: () => up$21
355
+ });
356
+ /**
357
+ * Migration: Add scheduled publishing support
358
+ *
359
+ * Adds scheduled_at column to all ec_* content tables.
360
+ * When scheduled_at is set and status is 'scheduled', the content
361
+ * will be auto-published when the scheduled time is reached.
362
+ */
363
+ async function up$21(db) {
364
+ const tableNames = await listTablesLike(db, "ec_%");
365
+ for (const tableName of tableNames) {
366
+ const table = { name: tableName };
367
+ await sql`
368
+ ALTER TABLE ${sql.ref(table.name)}
369
+ ADD COLUMN scheduled_at TEXT
370
+ `.execute(db);
371
+ await sql`
372
+ CREATE INDEX ${sql.ref(`idx_${table.name}_scheduled`)}
373
+ ON ${sql.ref(table.name)} (scheduled_at)
374
+ WHERE scheduled_at IS NOT NULL AND status = 'scheduled'
375
+ `.execute(db);
376
+ }
377
+ }
378
+ async function down$21(db) {
379
+ const tableNames = await listTablesLike(db, "ec_%");
380
+ for (const tableName of tableNames) {
381
+ const table = { name: tableName };
382
+ await sql`
383
+ DROP INDEX IF EXISTS ${sql.ref(`idx_${table.name}_scheduled`)}
384
+ `.execute(db);
385
+ await sql`
386
+ ALTER TABLE ${sql.ref(table.name)}
387
+ DROP COLUMN scheduled_at
388
+ `.execute(db);
389
+ }
390
+ }
391
+
392
+ //#endregion
393
+ //#region src/database/migrations/014_draft_revisions.ts
394
+ var _014_draft_revisions_exports = /* @__PURE__ */ __exportAll({
395
+ down: () => down$20,
396
+ up: () => up$20
397
+ });
398
+ async function up$20(db) {
399
+ const tables = await db.selectFrom("_dineway_collections").select("slug").execute();
400
+ for (const row of tables) {
401
+ const tableName = `ec_${row.slug}`;
402
+ await sql`
403
+ ALTER TABLE ${sql.ref(tableName)}
404
+ ADD COLUMN live_revision_id TEXT REFERENCES revisions(id)
405
+ `.execute(db);
406
+ await sql`
407
+ ALTER TABLE ${sql.ref(tableName)}
408
+ ADD COLUMN draft_revision_id TEXT REFERENCES revisions(id)
409
+ `.execute(db);
410
+ await sql`
411
+ CREATE INDEX ${sql.ref(`idx_${row.slug}_live_revision`)}
412
+ ON ${sql.ref(tableName)} (live_revision_id)
413
+ `.execute(db);
414
+ await sql`
415
+ CREATE INDEX ${sql.ref(`idx_${row.slug}_draft_revision`)}
416
+ ON ${sql.ref(tableName)} (draft_revision_id)
417
+ `.execute(db);
418
+ }
419
+ }
420
+ async function down$20(db) {
421
+ const tables = await db.selectFrom("_dineway_collections").select("slug").execute();
422
+ for (const row of tables) {
423
+ const tableName = `ec_${row.slug}`;
424
+ await sql`
425
+ DROP INDEX IF EXISTS ${sql.ref(`idx_${row.slug}_draft_revision`)}
426
+ `.execute(db);
427
+ await sql`
428
+ DROP INDEX IF EXISTS ${sql.ref(`idx_${row.slug}_live_revision`)}
429
+ `.execute(db);
430
+ await sql`
431
+ ALTER TABLE ${sql.ref(tableName)}
432
+ DROP COLUMN draft_revision_id
433
+ `.execute(db);
434
+ await sql`
435
+ ALTER TABLE ${sql.ref(tableName)}
436
+ DROP COLUMN live_revision_id
437
+ `.execute(db);
438
+ }
439
+ }
440
+
441
+ //#endregion
442
+ //#region src/database/migrations/015_indexes.ts
443
+ var _015_indexes_exports = /* @__PURE__ */ __exportAll({
444
+ down: () => down$19,
445
+ up: () => up$19
446
+ });
447
+ /**
448
+ * Add performance indexes for common query patterns.
449
+ *
450
+ * Covers:
451
+ * 1. Media table: mime_type, filename, created_at
452
+ * 2. content_taxonomies: reverse lookup by taxonomy_id
453
+ * 3. taxonomies: parent_id FK
454
+ * 4. audit_logs: compound (resource_type, resource_id)
455
+ * 5. Retroactive author_id + updated_at on existing ec_* content tables
456
+ * (new tables get these from createContentTable() in registry.ts)
457
+ */
458
+ async function up$19(db) {
459
+ await db.schema.createIndex("idx_media_mime_type").on("media").column("mime_type").execute();
460
+ await db.schema.createIndex("idx_media_filename").on("media").column("filename").execute();
461
+ await db.schema.createIndex("idx_media_created_at").on("media").column("created_at").execute();
462
+ await db.schema.createIndex("idx_content_taxonomies_term").on("content_taxonomies").column("taxonomy_id").execute();
463
+ await db.schema.createIndex("idx_taxonomies_parent").on("taxonomies").column("parent_id").execute();
464
+ await db.schema.createIndex("idx_audit_resource").on("audit_logs").columns(["resource_type", "resource_id"]).execute();
465
+ const tableNames = await listTablesLike(db, "ec_%");
466
+ for (const tableName of tableNames) {
467
+ const table = { name: tableName };
468
+ await sql`
469
+ CREATE INDEX ${sql.ref(`idx_${table.name}_author`)}
470
+ ON ${sql.ref(table.name)} (author_id)
471
+ `.execute(db);
472
+ await sql`
473
+ CREATE INDEX ${sql.ref(`idx_${table.name}_updated`)}
474
+ ON ${sql.ref(table.name)} (updated_at)
475
+ `.execute(db);
476
+ }
477
+ }
478
+ async function down$19(db) {
479
+ const tableNames = await listTablesLike(db, "ec_%");
480
+ for (const tableName of tableNames) {
481
+ const table = { name: tableName };
482
+ await sql`DROP INDEX IF EXISTS ${sql.ref(`idx_${table.name}_updated`)}`.execute(db);
483
+ await sql`DROP INDEX IF EXISTS ${sql.ref(`idx_${table.name}_author`)}`.execute(db);
484
+ }
485
+ await db.schema.dropIndex("idx_audit_resource").execute();
486
+ await db.schema.dropIndex("idx_taxonomies_parent").execute();
487
+ await db.schema.dropIndex("idx_content_taxonomies_term").execute();
488
+ await db.schema.dropIndex("idx_media_created_at").execute();
489
+ await db.schema.dropIndex("idx_media_filename").execute();
490
+ await db.schema.dropIndex("idx_media_mime_type").execute();
491
+ }
492
+
493
+ //#endregion
494
+ //#region src/database/migrations/016_api_tokens.ts
495
+ var _016_api_tokens_exports = /* @__PURE__ */ __exportAll({
496
+ down: () => down$18,
497
+ up: () => up$18
498
+ });
499
+ /**
500
+ * API token tables for programmatic access.
501
+ *
502
+ * Three tables:
503
+ * 1. _dineway_api_tokens — Personal Access Tokens (ec_pat_...)
504
+ * 2. _dineway_oauth_tokens — OAuth access/refresh tokens (ec_oat_/ec_ort_...)
505
+ * 3. _dineway_device_codes — OAuth Device Flow state (RFC 8628)
506
+ */
507
+ async function up$18(db) {
508
+ await db.schema.createTable("_dineway_api_tokens").addColumn("id", "text", (col) => col.primaryKey()).addColumn("name", "text", (col) => col.notNull()).addColumn("token_hash", "text", (col) => col.notNull().unique()).addColumn("prefix", "text", (col) => col.notNull()).addColumn("user_id", "text", (col) => col.notNull()).addColumn("scopes", "text", (col) => col.notNull()).addColumn("expires_at", "text").addColumn("last_used_at", "text").addColumn("created_at", "text", (col) => col.defaultTo(currentTimestamp(db))).addForeignKeyConstraint("api_tokens_user_fk", ["user_id"], "users", ["id"], (cb) => cb.onDelete("cascade")).execute();
509
+ await db.schema.createIndex("idx_api_tokens_token_hash").on("_dineway_api_tokens").column("token_hash").execute();
510
+ await db.schema.createIndex("idx_api_tokens_user_id").on("_dineway_api_tokens").column("user_id").execute();
511
+ await db.schema.createTable("_dineway_oauth_tokens").addColumn("token_hash", "text", (col) => col.primaryKey()).addColumn("token_type", "text", (col) => col.notNull()).addColumn("user_id", "text", (col) => col.notNull()).addColumn("scopes", "text", (col) => col.notNull()).addColumn("client_type", "text", (col) => col.notNull().defaultTo("cli")).addColumn("expires_at", "text", (col) => col.notNull()).addColumn("refresh_token_hash", "text").addColumn("created_at", "text", (col) => col.defaultTo(currentTimestamp(db))).addForeignKeyConstraint("oauth_tokens_user_fk", ["user_id"], "users", ["id"], (cb) => cb.onDelete("cascade")).execute();
512
+ await db.schema.createIndex("idx_oauth_tokens_user_id").on("_dineway_oauth_tokens").column("user_id").execute();
513
+ await db.schema.createIndex("idx_oauth_tokens_expires").on("_dineway_oauth_tokens").column("expires_at").execute();
514
+ await db.schema.createTable("_dineway_device_codes").addColumn("device_code", "text", (col) => col.primaryKey()).addColumn("user_code", "text", (col) => col.notNull().unique()).addColumn("scopes", "text", (col) => col.notNull()).addColumn("user_id", "text").addColumn("status", "text", (col) => col.notNull().defaultTo("pending")).addColumn("expires_at", "text", (col) => col.notNull()).addColumn("interval", "integer", (col) => col.notNull().defaultTo(5)).addColumn("created_at", "text", (col) => col.defaultTo(currentTimestamp(db))).execute();
515
+ }
516
+ async function down$18(db) {
517
+ await db.schema.dropTable("_dineway_device_codes").execute();
518
+ await db.schema.dropTable("_dineway_oauth_tokens").execute();
519
+ await db.schema.dropTable("_dineway_api_tokens").execute();
520
+ }
521
+
522
+ //#endregion
523
+ //#region src/database/migrations/017_authorization_codes.ts
524
+ var _017_authorization_codes_exports = /* @__PURE__ */ __exportAll({
525
+ down: () => down$17,
526
+ up: () => up$17
527
+ });
528
+ /**
529
+ * Authorization codes for OAuth 2.1 Authorization Code + PKCE flow.
530
+ *
531
+ * Used by MCP clients (Claude Desktop, VS Code, etc.) to authenticate
532
+ * via the standard OAuth authorization code grant.
533
+ *
534
+ * Also adds client_id tracking to oauth_tokens for per-client revocation.
535
+ */
536
+ async function up$17(db) {
537
+ await db.schema.createTable("_dineway_authorization_codes").addColumn("code_hash", "text", (col) => col.primaryKey()).addColumn("client_id", "text", (col) => col.notNull()).addColumn("redirect_uri", "text", (col) => col.notNull()).addColumn("user_id", "text", (col) => col.notNull()).addColumn("scopes", "text", (col) => col.notNull()).addColumn("code_challenge", "text", (col) => col.notNull()).addColumn("code_challenge_method", "text", (col) => col.notNull().defaultTo("S256")).addColumn("resource", "text").addColumn("expires_at", "text", (col) => col.notNull()).addColumn("created_at", "text", (col) => col.defaultTo(currentTimestamp(db))).addForeignKeyConstraint("auth_codes_user_fk", ["user_id"], "users", ["id"], (cb) => cb.onDelete("cascade")).execute();
538
+ await db.schema.createIndex("idx_auth_codes_expires").on("_dineway_authorization_codes").column("expires_at").execute();
539
+ await sql`ALTER TABLE _dineway_oauth_tokens ADD COLUMN client_id TEXT`.execute(db);
540
+ }
541
+ async function down$17(db) {
542
+ await db.schema.dropTable("_dineway_authorization_codes").execute();
543
+ }
544
+
545
+ //#endregion
546
+ //#region src/database/migrations/018_seo.ts
547
+ var _018_seo_exports = /* @__PURE__ */ __exportAll({
548
+ down: () => down$16,
549
+ up: () => up$16
550
+ });
551
+ /**
552
+ * Migration: SEO support
553
+ *
554
+ * Creates:
555
+ * - `_dineway_seo` table: per-content SEO metadata (separate from content tables)
556
+ * - `has_seo` column on `_dineway_collections`: opt-in flag per collection
557
+ *
558
+ * SEO is not a universal concern — only collections representing web pages
559
+ * need it. The `has_seo` flag controls whether the admin shows SEO fields
560
+ * and whether the collection's content appears in sitemaps.
561
+ */
562
+ async function up$16(db) {
563
+ await db.schema.createTable("_dineway_seo").addColumn("collection", "text", (col) => col.notNull()).addColumn("content_id", "text", (col) => col.notNull()).addColumn("seo_title", "text").addColumn("seo_description", "text").addColumn("seo_image", "text").addColumn("seo_canonical", "text").addColumn("seo_no_index", "integer", (col) => col.notNull().defaultTo(0)).addColumn("created_at", "text", (col) => col.notNull().defaultTo(currentTimestamp(db))).addColumn("updated_at", "text", (col) => col.notNull().defaultTo(currentTimestamp(db))).addPrimaryKeyConstraint("_dineway_seo_pk", ["collection", "content_id"]).execute();
564
+ await sql`
565
+ CREATE INDEX idx_dineway_seo_collection
566
+ ON _dineway_seo (collection)
567
+ `.execute(db);
568
+ await sql`
569
+ ALTER TABLE _dineway_collections
570
+ ADD COLUMN has_seo INTEGER NOT NULL DEFAULT 0
571
+ `.execute(db);
572
+ }
573
+ async function down$16(db) {
574
+ await sql`DROP TABLE IF EXISTS _dineway_seo`.execute(db);
575
+ await sql`
576
+ ALTER TABLE _dineway_collections
577
+ DROP COLUMN has_seo
578
+ `.execute(db);
579
+ }
580
+
581
+ //#endregion
582
+ //#region src/database/migrations/019_i18n.ts
583
+ var _019_i18n_exports = /* @__PURE__ */ __exportAll({
584
+ down: () => down$15,
585
+ up: () => up$15
586
+ });
587
+ /**
588
+ * Quote an identifier for use in raw SQL. Escapes embedded double-quotes
589
+ * per SQL standard (double them). The name should first pass
590
+ * validateIdentifier() or validateTableName() for defense-in-depth.
591
+ */
592
+ const DOUBLE_QUOTE_RE = /"/g;
593
+ function quoteIdent(name) {
594
+ return `"${name.replace(DOUBLE_QUOTE_RE, "\"\"")}"`;
595
+ }
596
+ /** Suffix added to tmp tables during i18n migration rebuild. */
597
+ const I18N_TMP_SUFFIX = /_i18n_tmp$/;
598
+ /** Table names from sqlite_master are ec_{slug} — validate the pattern. */
599
+ const TABLE_NAME_PATTERN = /^ec_[a-z][a-z0-9_]*$/;
600
+ function validateTableName(name) {
601
+ if (!TABLE_NAME_PATTERN.test(name)) throw new Error(`Invalid content table name: "${name}"`);
602
+ }
603
+ /** SQLite column types produced by Dineway's schema registry. */
604
+ const ALLOWED_COLUMN_TYPES = new Set([
605
+ "TEXT",
606
+ "INTEGER",
607
+ "REAL",
608
+ "BLOB",
609
+ "JSON",
610
+ "NUMERIC",
611
+ ""
612
+ ]);
613
+ function validateColumnType(type, colName) {
614
+ if (!ALLOWED_COLUMN_TYPES.has(type.toUpperCase())) throw new Error(`Unexpected column type "${type}" for column "${colName}"`);
615
+ }
616
+ /**
617
+ * Validate that a default value expression from PRAGMA table_info is safe
618
+ * to interpolate into DDL. Allows: string literals, numeric literals,
619
+ * NULL, and known function calls like datetime('now').
620
+ *
621
+ * Note: PRAGMA table_info strips the outer parens from expression defaults,
622
+ * so `DEFAULT (datetime('now'))` is reported as `datetime('now')`.
623
+ * We accept both forms and re-wrap in parens via normalizeDdlDefault().
624
+ */
625
+ const SAFE_DEFAULT_PATTERN = /^(?:'[^']*'|NULL|-?\d+(?:\.\d+)?|\(?datetime\('now'\)\)?|\(?json\('[^']*'\)\)?|0|1)$/i;
626
+ function validateDefaultValue(value, colName) {
627
+ if (!SAFE_DEFAULT_PATTERN.test(value)) throw new Error(`Unexpected default value "${value}" for column "${colName}"`);
628
+ }
629
+ /**
630
+ * Normalize a PRAGMA table_info default value for use in DDL.
631
+ * Function-call defaults (e.g. `datetime('now')`) must be wrapped in parens
632
+ * to form valid expression defaults: `DEFAULT (datetime('now'))`.
633
+ * PRAGMA strips the outer parens, so we re-add them here.
634
+ */
635
+ const FUNCTION_DEFAULT_PATTERN = /^(?:datetime|json)\(/i;
636
+ function normalizeDdlDefault(value) {
637
+ if (value.startsWith("(")) return value;
638
+ if (FUNCTION_DEFAULT_PATTERN.test(value)) return `(${value})`;
639
+ return value;
640
+ }
641
+ /**
642
+ * Validate that a CREATE INDEX statement from sqlite_master is safe to replay.
643
+ * Must start with CREATE [UNIQUE] INDEX and not contain suspicious patterns.
644
+ */
645
+ const CREATE_INDEX_PATTERN = /^CREATE\s+(UNIQUE\s+)?INDEX\s+/i;
646
+ function validateCreateIndexSql(sqlStr, idxName) {
647
+ if (!CREATE_INDEX_PATTERN.test(sqlStr)) throw new Error(`Unexpected index SQL for "${idxName}": does not match CREATE INDEX pattern`);
648
+ if (sqlStr.includes(";")) throw new Error(`Unexpected index SQL for "${idxName}": contains semicolon`);
649
+ }
650
+ /**
651
+ * PostgreSQL path: ALTER TABLE supports ADD COLUMN and DROP CONSTRAINT directly.
652
+ * No table rebuild needed.
653
+ */
654
+ async function upPostgres(db) {
655
+ const tableNames = await listTablesLike(db, "ec_%");
656
+ for (const t of tableNames) {
657
+ validateTableName(t);
658
+ if ((await sql`
659
+ SELECT EXISTS(
660
+ SELECT 1 FROM information_schema.columns
661
+ WHERE table_schema = 'public' AND table_name = ${t} AND column_name = 'locale'
662
+ ) as exists
663
+ `.execute(db)).rows[0]?.exists === true) continue;
664
+ await sql`ALTER TABLE ${sql.ref(t)} ADD COLUMN locale TEXT NOT NULL DEFAULT 'en'`.execute(db);
665
+ await sql`ALTER TABLE ${sql.ref(t)} ADD COLUMN translation_group TEXT`.execute(db);
666
+ const constraints = await sql`
667
+ SELECT conname FROM pg_constraint
668
+ WHERE conrelid = ${t}::regclass
669
+ AND contype = 'u'
670
+ AND array_length(conkey, 1) = 1
671
+ AND conkey[1] = (
672
+ SELECT attnum FROM pg_attribute
673
+ WHERE attrelid = ${t}::regclass AND attname = 'slug'
674
+ )
675
+ `.execute(db);
676
+ for (const c of constraints.rows) await sql`ALTER TABLE ${sql.ref(t)} DROP CONSTRAINT ${sql.ref(c.conname)}`.execute(db);
677
+ await sql`
678
+ ALTER TABLE ${sql.ref(t)}
679
+ ADD CONSTRAINT ${sql.ref(`${t}_slug_locale_unique`)} UNIQUE (slug, locale)
680
+ `.execute(db);
681
+ await sql`UPDATE ${sql.ref(t)} SET translation_group = id`.execute(db);
682
+ await sql`CREATE INDEX ${sql.ref(`idx_${t}_locale`)} ON ${sql.ref(t)} (locale)`.execute(db);
683
+ await sql`
684
+ CREATE INDEX ${sql.ref(`idx_${t}_translation_group`)}
685
+ ON ${sql.ref(t)} (translation_group)
686
+ `.execute(db);
687
+ }
688
+ if ((await sql`
689
+ SELECT EXISTS(
690
+ SELECT 1 FROM information_schema.columns
691
+ WHERE table_schema = 'public' AND table_name = '_dineway_fields' AND column_name = 'translatable'
692
+ ) as exists
693
+ `.execute(db)).rows[0]?.exists !== true) await sql`
694
+ ALTER TABLE _dineway_fields
695
+ ADD COLUMN translatable INTEGER NOT NULL DEFAULT 1
696
+ `.execute(db);
697
+ }
698
+ async function up$15(db) {
699
+ if (!isSqlite(db)) return upPostgres(db);
700
+ const orphanedTmps = await listTablesLike(db, "ec_%_i18n_tmp");
701
+ for (const tmpName of orphanedTmps) {
702
+ validateTableName(tmpName.replace(I18N_TMP_SUFFIX, ""));
703
+ await sql`DROP TABLE IF EXISTS ${sql.ref(tmpName)}`.execute(db);
704
+ }
705
+ const tables = { rows: (await listTablesLike(db, "ec_%")).map((name) => ({ name })) };
706
+ for (const table of tables.rows) {
707
+ const t = table.name;
708
+ validateTableName(t);
709
+ const tmp = `${t}_i18n_tmp`;
710
+ {
711
+ const trx = db;
712
+ const columns = (await sql`
713
+ PRAGMA table_info(${sql.ref(t)})
714
+ `.execute(trx)).rows;
715
+ if (columns.some((col) => col.name === "locale")) continue;
716
+ const idxResult = await sql`
717
+ PRAGMA index_list(${sql.ref(t)})
718
+ `.execute(trx);
719
+ const indexDefs = [];
720
+ for (const idx of idxResult.rows) {
721
+ if (idx.origin === "pk" || idx.name.startsWith("sqlite_autoindex_")) continue;
722
+ const idxColResult = await sql`
723
+ PRAGMA index_info(${sql.ref(idx.name)})
724
+ `.execute(trx);
725
+ indexDefs.push({
726
+ name: idx.name,
727
+ unique: idx.unique === 1,
728
+ columns: idxColResult.rows.map((c) => c.name),
729
+ partial: idx.partial
730
+ });
731
+ }
732
+ const partialSqls = /* @__PURE__ */ new Map();
733
+ for (const idx of indexDefs) if (idx.partial) {
734
+ const createResult = await sql`
735
+ SELECT sql FROM sqlite_master
736
+ WHERE type = 'index' AND name = ${idx.name}
737
+ `.execute(trx);
738
+ if (createResult.rows[0]?.sql) partialSqls.set(idx.name, createResult.rows[0].sql);
739
+ }
740
+ for (const col of columns) validateIdentifier(col.name, "column name");
741
+ const colDefs = [];
742
+ const colNames = [];
743
+ for (const col of columns) {
744
+ validateColumnType(col.type || "TEXT", col.name);
745
+ colNames.push(quoteIdent(col.name));
746
+ let def = `${quoteIdent(col.name)} ${col.type || "TEXT"}`;
747
+ if (col.pk) def += " PRIMARY KEY";
748
+ else if (col.name === "slug") {} else if (col.notnull) def += " NOT NULL";
749
+ if (col.dflt_value !== null) {
750
+ validateDefaultValue(col.dflt_value, col.name);
751
+ def += ` DEFAULT ${normalizeDdlDefault(col.dflt_value)}`;
752
+ }
753
+ colDefs.push(def);
754
+ }
755
+ colDefs.push("\"locale\" TEXT NOT NULL DEFAULT 'en'");
756
+ colDefs.push("\"translation_group\" TEXT");
757
+ colDefs.push("UNIQUE(\"slug\", \"locale\")");
758
+ const createColsSql = colDefs.join(",\n ");
759
+ const selectColsSql = colNames.join(", ");
760
+ for (const idx of indexDefs) await sql`DROP INDEX IF EXISTS ${sql.ref(idx.name)}`.execute(trx);
761
+ await sql.raw(`CREATE TABLE ${quoteIdent(tmp)} (\n\t\t\t\t${createColsSql}\n\t\t\t)`).execute(trx);
762
+ await sql.raw(`INSERT INTO ${quoteIdent(tmp)} (${selectColsSql}, "locale", "translation_group")\n\t\t\t SELECT ${selectColsSql}, 'en', "id" FROM ${quoteIdent(t)}`).execute(trx);
763
+ await sql`DROP TABLE ${sql.ref(t)}`.execute(trx);
764
+ await sql.raw(`ALTER TABLE ${quoteIdent(tmp)} RENAME TO ${quoteIdent(t)}`).execute(trx);
765
+ for (const idx of indexDefs) {
766
+ if (idx.name === `idx_${t}_slug`) continue;
767
+ if (idx.partial && partialSqls.has(idx.name)) {
768
+ const idxSql = partialSqls.get(idx.name);
769
+ validateCreateIndexSql(idxSql, idx.name);
770
+ await sql.raw(idxSql).execute(trx);
771
+ } else {
772
+ for (const c of idx.columns) validateIdentifier(c, "index column name");
773
+ const cols = idx.columns.map((c) => quoteIdent(c)).join(", ");
774
+ const unique = idx.unique ? "UNIQUE " : "";
775
+ await sql.raw(`CREATE ${unique}INDEX ${quoteIdent(idx.name)} ON ${quoteIdent(t)} (${cols})`).execute(trx);
776
+ }
777
+ }
778
+ await sql`
779
+ CREATE INDEX ${sql.ref(`idx_${t}_slug`)}
780
+ ON ${sql.ref(t)} (slug)
781
+ `.execute(trx);
782
+ await sql`
783
+ CREATE INDEX ${sql.ref(`idx_${t}_locale`)}
784
+ ON ${sql.ref(t)} (locale)
785
+ `.execute(trx);
786
+ await sql`
787
+ CREATE INDEX ${sql.ref(`idx_${t}_translation_group`)}
788
+ ON ${sql.ref(t)} (translation_group)
789
+ `.execute(trx);
790
+ }
791
+ }
792
+ if (!(await sql`
793
+ PRAGMA table_info(_dineway_fields)
794
+ `.execute(db)).rows.some((col) => col.name === "translatable")) await sql`
795
+ ALTER TABLE _dineway_fields
796
+ ADD COLUMN translatable INTEGER NOT NULL DEFAULT 1
797
+ `.execute(db);
798
+ }
799
+ /**
800
+ * PostgreSQL down path: straightforward ALTER TABLE operations.
801
+ */
802
+ async function downPostgres(db) {
803
+ await sql`ALTER TABLE _dineway_fields DROP COLUMN translatable`.execute(db);
804
+ const tableNames = await listTablesLike(db, "ec_%");
805
+ for (const t of tableNames) {
806
+ validateTableName(t);
807
+ await sql`DROP INDEX IF EXISTS ${sql.ref(`idx_${t}_locale`)}`.execute(db);
808
+ await sql`DROP INDEX IF EXISTS ${sql.ref(`idx_${t}_translation_group`)}`.execute(db);
809
+ await sql`ALTER TABLE ${sql.ref(t)} DROP CONSTRAINT IF EXISTS ${sql.ref(`${t}_slug_locale_unique`)}`.execute(db);
810
+ await sql`ALTER TABLE ${sql.ref(t)} ADD CONSTRAINT ${sql.ref(`${t}_slug_unique`)} UNIQUE (slug)`.execute(db);
811
+ await sql`ALTER TABLE ${sql.ref(t)} DROP COLUMN locale`.execute(db);
812
+ await sql`ALTER TABLE ${sql.ref(t)} DROP COLUMN translation_group`.execute(db);
813
+ }
814
+ }
815
+ async function down$15(db) {
816
+ if (!isSqlite(db)) return downPostgres(db);
817
+ await sql`
818
+ ALTER TABLE _dineway_fields
819
+ DROP COLUMN translatable
820
+ `.execute(db);
821
+ const tableNames = await listTablesLike(db, "ec_%");
822
+ for (const tableName of tableNames) {
823
+ const t = tableName;
824
+ validateTableName(t);
825
+ const tmp = `${t}_i18n_tmp`;
826
+ {
827
+ const trx = db;
828
+ const columns = (await sql`
829
+ PRAGMA table_info(${sql.ref(t)})
830
+ `.execute(trx)).rows;
831
+ const idxResult = await sql`
832
+ PRAGMA index_list(${sql.ref(t)})
833
+ `.execute(trx);
834
+ const indexDefs = [];
835
+ for (const idx of idxResult.rows) {
836
+ if (idx.origin === "pk" || idx.name.startsWith("sqlite_autoindex_")) continue;
837
+ const idxColResult = await sql`
838
+ PRAGMA index_info(${sql.ref(idx.name)})
839
+ `.execute(trx);
840
+ indexDefs.push({
841
+ name: idx.name,
842
+ unique: idx.unique === 1,
843
+ columns: idxColResult.rows.map((c) => c.name),
844
+ partial: idx.partial
845
+ });
846
+ }
847
+ const partialSqls = /* @__PURE__ */ new Map();
848
+ for (const idx of indexDefs) if (idx.partial) {
849
+ const createResult = await sql`
850
+ SELECT sql FROM sqlite_master
851
+ WHERE type = 'index' AND name = ${idx.name}
852
+ `.execute(trx);
853
+ if (createResult.rows[0]?.sql) partialSqls.set(idx.name, createResult.rows[0].sql);
854
+ }
855
+ for (const col of columns) {
856
+ if (col.name === "locale" || col.name === "translation_group") continue;
857
+ validateIdentifier(col.name, "column name");
858
+ }
859
+ const colDefs = [];
860
+ const colNames = [];
861
+ for (const col of columns) {
862
+ if (col.name === "locale" || col.name === "translation_group") continue;
863
+ validateColumnType(col.type || "TEXT", col.name);
864
+ colNames.push(quoteIdent(col.name));
865
+ let def = `${quoteIdent(col.name)} ${col.type || "TEXT"}`;
866
+ if (col.pk) def += " PRIMARY KEY";
867
+ else if (col.name === "slug") def += " UNIQUE";
868
+ else if (col.notnull) def += " NOT NULL";
869
+ if (col.dflt_value !== null) {
870
+ validateDefaultValue(col.dflt_value, col.name);
871
+ def += ` DEFAULT ${normalizeDdlDefault(col.dflt_value)}`;
872
+ }
873
+ colDefs.push(def);
874
+ }
875
+ const createColsSql = colDefs.join(",\n ");
876
+ const selectColsSql = colNames.join(", ");
877
+ for (const idx of indexDefs) await sql`DROP INDEX IF EXISTS ${sql.ref(idx.name)}`.execute(trx);
878
+ await sql.raw(`CREATE TABLE ${quoteIdent(tmp)} (\n\t\t\t\t${createColsSql}\n\t\t\t)`).execute(trx);
879
+ await sql.raw(`INSERT OR IGNORE INTO ${quoteIdent(tmp)} (${selectColsSql})
880
+ SELECT ${selectColsSql} FROM ${quoteIdent(t)}
881
+ WHERE "locale" = 'en'`).execute(trx);
882
+ await sql.raw(`INSERT OR IGNORE INTO ${quoteIdent(tmp)} (${selectColsSql})
883
+ SELECT ${selectColsSql} FROM ${quoteIdent(t)}
884
+ WHERE "id" NOT IN (SELECT "id" FROM ${quoteIdent(tmp)})
885
+ AND "id" IN (
886
+ SELECT "id" FROM ${quoteIdent(t)} AS t2
887
+ WHERE t2."translation_group" IS NOT NULL
888
+ AND t2."locale" = (
889
+ SELECT MIN(t3."locale") FROM ${quoteIdent(t)} AS t3
890
+ WHERE t3."translation_group" = t2."translation_group"
891
+ )
892
+ )`).execute(trx);
893
+ await sql.raw(`INSERT OR IGNORE INTO ${quoteIdent(tmp)} (${selectColsSql})
894
+ SELECT ${selectColsSql} FROM ${quoteIdent(t)}
895
+ WHERE "id" NOT IN (SELECT "id" FROM ${quoteIdent(tmp)})
896
+ AND "translation_group" IS NULL`).execute(trx);
897
+ await sql`DROP TABLE ${sql.ref(t)}`.execute(trx);
898
+ await sql.raw(`ALTER TABLE ${quoteIdent(tmp)} RENAME TO ${quoteIdent(t)}`).execute(trx);
899
+ for (const idx of indexDefs) {
900
+ if (idx.name === `idx_${t}_locale`) continue;
901
+ if (idx.name === `idx_${t}_translation_group`) continue;
902
+ if (idx.partial && partialSqls.has(idx.name)) {
903
+ const idxSql = partialSqls.get(idx.name);
904
+ validateCreateIndexSql(idxSql, idx.name);
905
+ await sql.raw(idxSql).execute(trx);
906
+ } else {
907
+ const cols = idx.columns.filter((c) => c !== "locale" && c !== "translation_group");
908
+ if (cols.length === 0) continue;
909
+ for (const c of cols) validateIdentifier(c, "index column name");
910
+ const colsSql = cols.map((c) => quoteIdent(c)).join(", ");
911
+ const unique = idx.unique ? "UNIQUE " : "";
912
+ await sql.raw(`CREATE ${unique}INDEX ${quoteIdent(idx.name)} ON ${quoteIdent(t)} (${colsSql})`).execute(trx);
913
+ }
914
+ }
915
+ }
916
+ }
917
+ }
918
+
919
+ //#endregion
920
+ //#region src/database/migrations/020_collection_url_pattern.ts
921
+ var _020_collection_url_pattern_exports = /* @__PURE__ */ __exportAll({
922
+ down: () => down$14,
923
+ up: () => up$14
924
+ });
925
+ /**
926
+ * Migration: URL pattern for collections
927
+ *
928
+ * Adds `url_pattern` column to `_dineway_collections` so each collection
929
+ * can declare its own URL structure (e.g. "/{slug}" for pages, "/blog/{slug}"
930
+ * for posts). Used for menu URL resolution, sitemaps, and path-based lookups.
931
+ */
932
+ async function up$14(db) {
933
+ await sql`
934
+ ALTER TABLE _dineway_collections
935
+ ADD COLUMN url_pattern TEXT
936
+ `.execute(db);
937
+ }
938
+ async function down$14(db) {
939
+ await sql`
940
+ ALTER TABLE _dineway_collections
941
+ DROP COLUMN url_pattern
942
+ `.execute(db);
943
+ }
944
+
945
+ //#endregion
946
+ //#region src/database/migrations/021_remove_section_categories.ts
947
+ var _021_remove_section_categories_exports = /* @__PURE__ */ __exportAll({
948
+ down: () => down$13,
949
+ up: () => up$13
950
+ });
951
+ /**
952
+ * Migration: Remove section categories
953
+ *
954
+ * Section categories had a complete backend but no UI to create or manage them.
955
+ * Rather than building the missing UI for a feature with very little need at this stage,
956
+ * we're removing the feature entirely.
957
+ */
958
+ async function up$13(db) {
959
+ await db.schema.dropIndex("idx_sections_category").ifExists().execute();
960
+ await db.schema.alterTable("_dineway_sections").dropColumn("category_id").execute();
961
+ await db.schema.dropTable("_dineway_section_categories").execute();
962
+ }
963
+ async function down$13(db) {
964
+ await db.schema.createTable("_dineway_section_categories").addColumn("id", "text", (col) => col.primaryKey()).addColumn("slug", "text", (col) => col.notNull().unique()).addColumn("label", "text", (col) => col.notNull()).addColumn("sort_order", "integer", (col) => col.defaultTo(0)).addColumn("created_at", "text", (col) => col.defaultTo(sql`CURRENT_TIMESTAMP`)).execute();
965
+ await db.schema.alterTable("_dineway_sections").addColumn("category_id", "text", (col) => col.references("_dineway_section_categories.id").onDelete("set null")).execute();
966
+ await db.schema.createIndex("idx_sections_category").on("_dineway_sections").columns(["category_id"]).execute();
967
+ }
968
+
969
+ //#endregion
970
+ //#region src/database/migrations/022_marketplace_plugin_state.ts
971
+ var _022_marketplace_plugin_state_exports = /* @__PURE__ */ __exportAll({
972
+ down: () => down$12,
973
+ up: () => up$12
974
+ });
975
+ /**
976
+ * Migration: Add marketplace fields to _plugin_state
977
+ *
978
+ * Adds `source` and `marketplace_version` columns to track
979
+ * whether a plugin was installed from config or marketplace,
980
+ * and which marketplace version is installed.
981
+ */
982
+ async function up$12(db) {
983
+ await sql`
984
+ ALTER TABLE _plugin_state
985
+ ADD COLUMN source TEXT NOT NULL DEFAULT 'config'
986
+ `.execute(db);
987
+ await sql`
988
+ ALTER TABLE _plugin_state
989
+ ADD COLUMN marketplace_version TEXT
990
+ `.execute(db);
991
+ await sql`
992
+ CREATE INDEX idx_plugin_state_source
993
+ ON _plugin_state (source)
994
+ WHERE source = 'marketplace'
995
+ `.execute(db);
996
+ }
997
+ async function down$12(db) {
998
+ await sql`
999
+ DROP INDEX IF EXISTS idx_plugin_state_source
1000
+ `.execute(db);
1001
+ await sql`
1002
+ ALTER TABLE _plugin_state
1003
+ DROP COLUMN marketplace_version
1004
+ `.execute(db);
1005
+ await sql`
1006
+ ALTER TABLE _plugin_state
1007
+ DROP COLUMN source
1008
+ `.execute(db);
1009
+ }
1010
+
1011
+ //#endregion
1012
+ //#region src/database/migrations/023_plugin_metadata.ts
1013
+ var _023_plugin_metadata_exports = /* @__PURE__ */ __exportAll({
1014
+ down: () => down$11,
1015
+ up: () => up$11
1016
+ });
1017
+ /**
1018
+ * Migration: Add display metadata to _plugin_state
1019
+ *
1020
+ * Stores display_name and description for marketplace plugins
1021
+ * so the admin UI can show meaningful info without re-fetching
1022
+ * from the marketplace on every page load.
1023
+ */
1024
+ async function up$11(db) {
1025
+ await sql`
1026
+ ALTER TABLE _plugin_state
1027
+ ADD COLUMN display_name TEXT
1028
+ `.execute(db);
1029
+ await sql`
1030
+ ALTER TABLE _plugin_state
1031
+ ADD COLUMN description TEXT
1032
+ `.execute(db);
1033
+ }
1034
+ async function down$11(db) {
1035
+ await sql`
1036
+ ALTER TABLE _plugin_state
1037
+ DROP COLUMN description
1038
+ `.execute(db);
1039
+ await sql`
1040
+ ALTER TABLE _plugin_state
1041
+ DROP COLUMN display_name
1042
+ `.execute(db);
1043
+ }
1044
+
1045
+ //#endregion
1046
+ //#region src/database/migrations/024_media_placeholders.ts
1047
+ var _024_media_placeholders_exports = /* @__PURE__ */ __exportAll({
1048
+ down: () => down$10,
1049
+ up: () => up$10
1050
+ });
1051
+ /**
1052
+ * Migration: Add placeholder columns to media table
1053
+ *
1054
+ * Stores blurhash and dominant_color for LQIP (Low Quality Image Placeholder)
1055
+ * support. Generated at upload time from image pixel data.
1056
+ */
1057
+ async function up$10(db) {
1058
+ await sql`
1059
+ ALTER TABLE media
1060
+ ADD COLUMN blurhash TEXT
1061
+ `.execute(db);
1062
+ await sql`
1063
+ ALTER TABLE media
1064
+ ADD COLUMN dominant_color TEXT
1065
+ `.execute(db);
1066
+ }
1067
+ async function down$10(db) {
1068
+ await sql`
1069
+ ALTER TABLE media
1070
+ DROP COLUMN dominant_color
1071
+ `.execute(db);
1072
+ await sql`
1073
+ ALTER TABLE media
1074
+ DROP COLUMN blurhash
1075
+ `.execute(db);
1076
+ }
1077
+
1078
+ //#endregion
1079
+ //#region src/database/migrations/025_oauth_clients.ts
1080
+ var _025_oauth_clients_exports = /* @__PURE__ */ __exportAll({
1081
+ down: () => down$9,
1082
+ up: () => up$9
1083
+ });
1084
+ /**
1085
+ * Migration: Create OAuth clients table
1086
+ *
1087
+ * Implements the oauth_clients registry so that the authorization endpoint
1088
+ * can validate client_id and enforce a per-client redirect URI allowlist.
1089
+ *
1090
+ * Each client has a set of pre-registered redirect URIs (JSON array).
1091
+ * The authorize endpoint rejects any redirect_uri not in the client's list.
1092
+ */
1093
+ async function up$9(db) {
1094
+ await db.schema.createTable("_dineway_oauth_clients").addColumn("id", "text", (col) => col.primaryKey()).addColumn("name", "text", (col) => col.notNull()).addColumn("redirect_uris", "text", (col) => col.notNull()).addColumn("scopes", "text").addColumn("created_at", "text", (col) => col.defaultTo(currentTimestamp(db))).addColumn("updated_at", "text", (col) => col.defaultTo(currentTimestamp(db))).execute();
1095
+ }
1096
+ async function down$9(db) {
1097
+ await db.schema.dropTable("_dineway_oauth_clients").execute();
1098
+ }
1099
+
1100
+ //#endregion
1101
+ //#region src/database/migrations/026_cron_tasks.ts
1102
+ var _026_cron_tasks_exports = /* @__PURE__ */ __exportAll({
1103
+ down: () => down$8,
1104
+ up: () => up$8
1105
+ });
1106
+ /**
1107
+ * Migration: Create cron tasks table for plugin scheduled tasks.
1108
+ *
1109
+ * Each plugin can register cron tasks (recurring or one-shot) which are
1110
+ * stored here and executed by the platform-specific scheduler.
1111
+ *
1112
+ * The `next_run_at` + `status` + `enabled` index drives the "find overdue
1113
+ * tasks" query used by CronExecutor.tick().
1114
+ */
1115
+ async function up$8(db) {
1116
+ await db.schema.createTable("_dineway_cron_tasks").addColumn("id", "text", (col) => col.primaryKey()).addColumn("plugin_id", "text", (col) => col.notNull()).addColumn("task_name", "text", (col) => col.notNull()).addColumn("schedule", "text", (col) => col.notNull()).addColumn("is_oneshot", "integer", (col) => col.notNull().defaultTo(0)).addColumn("data", "text").addColumn("next_run_at", "text", (col) => col.notNull()).addColumn("last_run_at", "text").addColumn("status", "text", (col) => col.notNull().defaultTo("idle")).addColumn("locked_at", "text").addColumn("enabled", "integer", (col) => col.notNull().defaultTo(1)).addColumn("created_at", "text", (col) => col.defaultTo(currentTimestamp(db))).addUniqueConstraint("uq_cron_tasks_plugin_task", ["plugin_id", "task_name"]).execute();
1117
+ await db.schema.createIndex("idx_cron_tasks_due").on("_dineway_cron_tasks").columns([
1118
+ "enabled",
1119
+ "status",
1120
+ "next_run_at"
1121
+ ]).execute();
1122
+ await db.schema.createIndex("idx_cron_tasks_plugin").on("_dineway_cron_tasks").column("plugin_id").execute();
1123
+ }
1124
+ async function down$8(db) {
1125
+ await db.schema.dropTable("_dineway_cron_tasks").execute();
1126
+ }
1127
+
1128
+ //#endregion
1129
+ //#region src/database/migrations/027_comments.ts
1130
+ var _027_comments_exports = /* @__PURE__ */ __exportAll({
1131
+ down: () => down$7,
1132
+ up: () => up$7
1133
+ });
1134
+ async function up$7(db) {
1135
+ await db.schema.createTable("_dineway_comments").addColumn("id", "text", (col) => col.primaryKey()).addColumn("collection", "text", (col) => col.notNull()).addColumn("content_id", "text", (col) => col.notNull()).addColumn("parent_id", "text", (col) => col.references("_dineway_comments.id").onDelete("cascade")).addColumn("author_name", "text", (col) => col.notNull()).addColumn("author_email", "text", (col) => col.notNull()).addColumn("author_url", "text").addColumn("author_user_id", "text", (col) => col.references("users.id").onDelete("set null")).addColumn("body", "text", (col) => col.notNull()).addColumn("status", "text", (col) => col.notNull().defaultTo("pending")).addColumn("ip_hash", "text").addColumn("user_agent", "text").addColumn("moderation_metadata", "text").addColumn("created_at", "text", (col) => col.defaultTo(currentTimestamp(db))).addColumn("updated_at", "text", (col) => col.defaultTo(currentTimestamp(db))).execute();
1136
+ await db.schema.createIndex("idx_comments_content").on("_dineway_comments").columns([
1137
+ "collection",
1138
+ "content_id",
1139
+ "status"
1140
+ ]).execute();
1141
+ await db.schema.createIndex("idx_comments_parent").on("_dineway_comments").column("parent_id").execute();
1142
+ await db.schema.createIndex("idx_comments_status").on("_dineway_comments").columns(["status", "created_at"]).execute();
1143
+ await db.schema.createIndex("idx_comments_author_email").on("_dineway_comments").column("author_email").execute();
1144
+ await db.schema.createIndex("idx_comments_author_user").on("_dineway_comments").column("author_user_id").execute();
1145
+ await db.schema.alterTable("_dineway_collections").addColumn("comments_enabled", "integer", (col) => col.defaultTo(0)).execute();
1146
+ await db.schema.alterTable("_dineway_collections").addColumn("comments_moderation", "text", (col) => col.defaultTo("first_time")).execute();
1147
+ await db.schema.alterTable("_dineway_collections").addColumn("comments_closed_after_days", "integer", (col) => col.defaultTo(90)).execute();
1148
+ await db.schema.alterTable("_dineway_collections").addColumn("comments_auto_approve_users", "integer", (col) => col.defaultTo(1)).execute();
1149
+ }
1150
+ async function down$7(db) {
1151
+ await db.schema.dropTable("_dineway_comments").execute();
1152
+ }
1153
+
1154
+ //#endregion
1155
+ //#region src/database/migrations/028_drop_author_url.ts
1156
+ var _028_drop_author_url_exports = /* @__PURE__ */ __exportAll({
1157
+ down: () => down$6,
1158
+ up: () => up$6
1159
+ });
1160
+ async function up$6(db) {
1161
+ await sql`ALTER TABLE _dineway_comments DROP COLUMN author_url`.execute(db);
1162
+ }
1163
+ async function down$6(db) {
1164
+ await db.schema.alterTable("_dineway_comments").addColumn("author_url", "text").execute();
1165
+ }
1166
+
1167
+ //#endregion
1168
+ //#region src/database/migrations/029_redirects.ts
1169
+ var _029_redirects_exports = /* @__PURE__ */ __exportAll({
1170
+ down: () => down$5,
1171
+ up: () => up$5
1172
+ });
1173
+ async function up$5(db) {
1174
+ await db.schema.createTable("_dineway_redirects").addColumn("id", "text", (col) => col.primaryKey()).addColumn("source", "text", (col) => col.notNull()).addColumn("destination", "text", (col) => col.notNull()).addColumn("type", "integer", (col) => col.notNull().defaultTo(301)).addColumn("is_pattern", "integer", (col) => col.notNull().defaultTo(0)).addColumn("enabled", "integer", (col) => col.notNull().defaultTo(1)).addColumn("hits", "integer", (col) => col.notNull().defaultTo(0)).addColumn("last_hit_at", "text").addColumn("group_name", "text").addColumn("auto", "integer", (col) => col.notNull().defaultTo(0)).addColumn("created_at", "text", (col) => col.defaultTo(currentTimestamp(db))).addColumn("updated_at", "text", (col) => col.defaultTo(currentTimestamp(db))).execute();
1175
+ await db.schema.createIndex("idx_redirects_source").on("_dineway_redirects").column("source").execute();
1176
+ await db.schema.createIndex("idx_redirects_enabled").on("_dineway_redirects").column("enabled").execute();
1177
+ await db.schema.createIndex("idx_redirects_group").on("_dineway_redirects").column("group_name").execute();
1178
+ await db.schema.createTable("_dineway_404_log").addColumn("id", "text", (col) => col.primaryKey()).addColumn("path", "text", (col) => col.notNull()).addColumn("referrer", "text").addColumn("user_agent", "text").addColumn("ip", "text").addColumn("created_at", "text", (col) => col.defaultTo(currentTimestamp(db))).execute();
1179
+ await db.schema.createIndex("idx_404_log_path").on("_dineway_404_log").column("path").execute();
1180
+ await db.schema.createIndex("idx_404_log_created").on("_dineway_404_log").column("created_at").execute();
1181
+ }
1182
+ async function down$5(db) {
1183
+ await db.schema.dropTable("_dineway_404_log").execute();
1184
+ await db.schema.dropTable("_dineway_redirects").execute();
1185
+ }
1186
+
1187
+ //#endregion
1188
+ //#region src/database/migrations/030_widen_scheduled_index.ts
1189
+ var _030_widen_scheduled_index_exports = /* @__PURE__ */ __exportAll({
1190
+ down: () => down$4,
1191
+ up: () => up$4
1192
+ });
1193
+ /**
1194
+ * Migration: Widen scheduled publishing index
1195
+ *
1196
+ * The original partial index (013) only covered status='scheduled'.
1197
+ * Published posts can now have scheduled draft changes, so widen the
1198
+ * index to cover all rows where scheduled_at IS NOT NULL.
1199
+ */
1200
+ async function up$4(db) {
1201
+ const tableNames = await listTablesLike(db, "ec_%");
1202
+ for (const tableName of tableNames) {
1203
+ const table = { name: tableName };
1204
+ await sql`
1205
+ DROP INDEX IF EXISTS ${sql.ref(`idx_${table.name}_scheduled`)}
1206
+ `.execute(db);
1207
+ await sql`
1208
+ CREATE INDEX ${sql.ref(`idx_${table.name}_scheduled`)}
1209
+ ON ${sql.ref(table.name)} (scheduled_at)
1210
+ WHERE scheduled_at IS NOT NULL
1211
+ `.execute(db);
1212
+ }
1213
+ }
1214
+ async function down$4(db) {
1215
+ const tableNames = await listTablesLike(db, "ec_%");
1216
+ for (const tableName of tableNames) {
1217
+ const table = { name: tableName };
1218
+ await sql`
1219
+ DROP INDEX IF EXISTS ${sql.ref(`idx_${table.name}_scheduled`)}
1220
+ `.execute(db);
1221
+ await sql`
1222
+ CREATE INDEX ${sql.ref(`idx_${table.name}_scheduled`)}
1223
+ ON ${sql.ref(table.name)} (scheduled_at)
1224
+ WHERE scheduled_at IS NOT NULL AND status = 'scheduled'
1225
+ `.execute(db);
1226
+ }
1227
+ }
1228
+
1229
+ //#endregion
1230
+ //#region src/database/migrations/031_bylines.ts
1231
+ var _031_bylines_exports = /* @__PURE__ */ __exportAll({
1232
+ down: () => down$3,
1233
+ up: () => up$3
1234
+ });
1235
+ async function up$3(db) {
1236
+ await db.schema.createTable("_dineway_bylines").addColumn("id", "text", (col) => col.primaryKey()).addColumn("slug", "text", (col) => col.notNull().unique()).addColumn("display_name", "text", (col) => col.notNull()).addColumn("bio", "text").addColumn("avatar_media_id", "text", (col) => col.references("media.id").onDelete("set null")).addColumn("website_url", "text").addColumn("user_id", "text", (col) => col.references("users.id").onDelete("set null")).addColumn("is_guest", "integer", (col) => col.notNull().defaultTo(0)).addColumn("created_at", "text", (col) => col.defaultTo(currentTimestamp(db))).addColumn("updated_at", "text", (col) => col.defaultTo(currentTimestamp(db))).execute();
1237
+ await sql`
1238
+ CREATE UNIQUE INDEX ${sql.ref("idx_bylines_user_id_unique")}
1239
+ ON ${sql.ref("_dineway_bylines")} (user_id)
1240
+ WHERE user_id IS NOT NULL
1241
+ `.execute(db);
1242
+ await db.schema.createIndex("idx_bylines_slug").on("_dineway_bylines").column("slug").execute();
1243
+ await db.schema.createIndex("idx_bylines_display_name").on("_dineway_bylines").column("display_name").execute();
1244
+ await db.schema.createTable("_dineway_content_bylines").addColumn("id", "text", (col) => col.primaryKey()).addColumn("collection_slug", "text", (col) => col.notNull()).addColumn("content_id", "text", (col) => col.notNull()).addColumn("byline_id", "text", (col) => col.notNull().references("_dineway_bylines.id").onDelete("cascade")).addColumn("sort_order", "integer", (col) => col.notNull().defaultTo(0)).addColumn("role_label", "text").addColumn("created_at", "text", (col) => col.defaultTo(currentTimestamp(db))).addUniqueConstraint("content_bylines_unique", [
1245
+ "collection_slug",
1246
+ "content_id",
1247
+ "byline_id"
1248
+ ]).execute();
1249
+ await db.schema.createIndex("idx_content_bylines_content").on("_dineway_content_bylines").columns([
1250
+ "collection_slug",
1251
+ "content_id",
1252
+ "sort_order"
1253
+ ]).execute();
1254
+ await db.schema.createIndex("idx_content_bylines_byline").on("_dineway_content_bylines").column("byline_id").execute();
1255
+ const tableNames = await listTablesLike(db, "ec_%");
1256
+ for (const tableName of tableNames) {
1257
+ await sql`
1258
+ ALTER TABLE ${sql.ref(tableName)}
1259
+ ADD COLUMN primary_byline_id TEXT
1260
+ `.execute(db);
1261
+ await sql`
1262
+ CREATE INDEX ${sql.ref(`idx_${tableName}_primary_byline`)}
1263
+ ON ${sql.ref(tableName)} (primary_byline_id)
1264
+ `.execute(db);
1265
+ }
1266
+ }
1267
+ async function down$3(db) {
1268
+ const tableNames = await listTablesLike(db, "ec_%");
1269
+ for (const tableName of tableNames) {
1270
+ await sql`
1271
+ DROP INDEX IF EXISTS ${sql.ref(`idx_${tableName}_primary_byline`)}
1272
+ `.execute(db);
1273
+ await sql`
1274
+ ALTER TABLE ${sql.ref(tableName)}
1275
+ DROP COLUMN primary_byline_id
1276
+ `.execute(db);
1277
+ }
1278
+ await db.schema.dropTable("_dineway_content_bylines").execute();
1279
+ await db.schema.dropTable("_dineway_bylines").execute();
1280
+ }
1281
+
1282
+ //#endregion
1283
+ //#region src/database/migrations/032_rate_limits.ts
1284
+ var _032_rate_limits_exports = /* @__PURE__ */ __exportAll({
1285
+ down: () => down$2,
1286
+ up: () => up$2
1287
+ });
1288
+ /**
1289
+ * Migration: Rate limits table + device code polling tracking.
1290
+ *
1291
+ * 1. Create _dineway_rate_limits for database-backed rate limiting
1292
+ * of unauthenticated endpoints (device code, magic link, passkey).
1293
+ *
1294
+ * 2. Add last_polled_at column to _dineway_device_codes for
1295
+ * RFC 8628 slow_down enforcement.
1296
+ */
1297
+ async function up$2(db) {
1298
+ await db.schema.createTable("_dineway_rate_limits").addColumn("key", "text", (col) => col.notNull()).addColumn("window", "text", (col) => col.notNull()).addColumn("count", "integer", (col) => col.notNull().defaultTo(1)).addPrimaryKeyConstraint("pk_rate_limits", ["key", "window"]).execute();
1299
+ await db.schema.createIndex("idx_rate_limits_window").on("_dineway_rate_limits").column("window").execute();
1300
+ await db.schema.alterTable("_dineway_device_codes").addColumn("last_polled_at", "text").execute();
1301
+ }
1302
+ async function down$2(db) {
1303
+ await db.schema.dropTable("_dineway_rate_limits").execute();
1304
+ await db.schema.alterTable("_dineway_device_codes").dropColumn("last_polled_at").execute();
1305
+ }
1306
+
1307
+ //#endregion
1308
+ //#region src/database/migrations/033_optimize_content_indexes.ts
1309
+ var _033_optimize_content_indexes_exports = /* @__PURE__ */ __exportAll({
1310
+ down: () => down$1,
1311
+ up: () => up$1
1312
+ });
1313
+ /**
1314
+ * Migration: Optimize content table indexes for SQLite-compatible performance
1315
+ *
1316
+ * Addresses GitHub issue #131: Full table scans causing excessive row reads.
1317
+ *
1318
+ * Changes:
1319
+ * 1. Replaces single-column indexes with composite indexes on ec_* tables
1320
+ * 2. Adds partial indexes for _dineway_comments status counting
1321
+ *
1322
+ * Impact: Reduces row reads by 90%+ for admin panel operations on SQLite-compatible adapters.
1323
+ */
1324
+ async function up$1(db) {
1325
+ const tableNames = await listTablesLike(db, "ec_%");
1326
+ for (const tableName of tableNames) {
1327
+ const table = { name: tableName };
1328
+ await sql`DROP INDEX IF EXISTS ${sql.ref(`idx_${table.name}_status`)}`.execute(db);
1329
+ await sql`DROP INDEX IF EXISTS ${sql.ref(`idx_${table.name}_created`)}`.execute(db);
1330
+ await sql`DROP INDEX IF EXISTS ${sql.ref(`idx_${table.name}_deleted`)}`.execute(db);
1331
+ await sql`DROP INDEX IF EXISTS ${sql.ref(`idx_${table.name}_updated`)}`.execute(db);
1332
+ await sql`
1333
+ CREATE INDEX IF NOT EXISTS ${sql.ref(`idx_${table.name}_deleted_updated_id`)}
1334
+ ON ${sql.ref(table.name)} (deleted_at, updated_at DESC, id DESC)
1335
+ `.execute(db);
1336
+ await sql`
1337
+ CREATE INDEX IF NOT EXISTS ${sql.ref(`idx_${table.name}_deleted_status`)}
1338
+ ON ${sql.ref(table.name)} (deleted_at, status)
1339
+ `.execute(db);
1340
+ await sql`
1341
+ CREATE INDEX IF NOT EXISTS ${sql.ref(`idx_${table.name}_deleted_created_id`)}
1342
+ ON ${sql.ref(table.name)} (deleted_at, created_at DESC, id DESC)
1343
+ `.execute(db);
1344
+ }
1345
+ await sql`
1346
+ CREATE INDEX IF NOT EXISTS idx_comments_pending
1347
+ ON _dineway_comments (id)
1348
+ WHERE status = 'pending'
1349
+ `.execute(db);
1350
+ await sql`
1351
+ CREATE INDEX IF NOT EXISTS idx_comments_approved
1352
+ ON _dineway_comments (id)
1353
+ WHERE status = 'approved'
1354
+ `.execute(db);
1355
+ await sql`
1356
+ CREATE INDEX IF NOT EXISTS idx_comments_spam
1357
+ ON _dineway_comments (id)
1358
+ WHERE status = 'spam'
1359
+ `.execute(db);
1360
+ await sql`
1361
+ CREATE INDEX IF NOT EXISTS idx_comments_trash
1362
+ ON _dineway_comments (id)
1363
+ WHERE status = 'trash'
1364
+ `.execute(db);
1365
+ }
1366
+ async function down$1(db) {
1367
+ const tableNames = await listTablesLike(db, "ec_%");
1368
+ for (const tableName of tableNames) {
1369
+ const table = { name: tableName };
1370
+ await sql`DROP INDEX IF EXISTS ${sql.ref(`idx_${table.name}_deleted_updated_id`)}`.execute(db);
1371
+ await sql`DROP INDEX IF EXISTS ${sql.ref(`idx_${table.name}_deleted_status`)}`.execute(db);
1372
+ await sql`DROP INDEX IF EXISTS ${sql.ref(`idx_${table.name}_deleted_created_id`)}`.execute(db);
1373
+ await sql`
1374
+ CREATE INDEX ${sql.ref(`idx_${table.name}_status`)}
1375
+ ON ${sql.ref(table.name)} (status)
1376
+ `.execute(db);
1377
+ await sql`
1378
+ CREATE INDEX ${sql.ref(`idx_${table.name}_created`)}
1379
+ ON ${sql.ref(table.name)} (created_at)
1380
+ `.execute(db);
1381
+ await sql`
1382
+ CREATE INDEX ${sql.ref(`idx_${table.name}_deleted`)}
1383
+ ON ${sql.ref(table.name)} (deleted_at)
1384
+ `.execute(db);
1385
+ await sql`
1386
+ CREATE INDEX ${sql.ref(`idx_${table.name}_updated`)}
1387
+ ON ${sql.ref(table.name)} (updated_at)
1388
+ `.execute(db);
1389
+ }
1390
+ await sql`DROP INDEX IF EXISTS idx_comments_pending`.execute(db);
1391
+ await sql`DROP INDEX IF EXISTS idx_comments_approved`.execute(db);
1392
+ await sql`DROP INDEX IF EXISTS idx_comments_spam`.execute(db);
1393
+ await sql`DROP INDEX IF EXISTS idx_comments_trash`.execute(db);
1394
+ }
1395
+
1396
+ //#endregion
1397
+ //#region src/database/migrations/034_published_at_index.ts
1398
+ var _034_published_at_index_exports = /* @__PURE__ */ __exportAll({
1399
+ down: () => down,
1400
+ up: () => up
1401
+ });
1402
+ async function up(db) {
1403
+ const tableNames = await listTablesLike(db, "ec_%");
1404
+ for (const tableName of tableNames) {
1405
+ const table = { name: tableName };
1406
+ await sql`
1407
+ CREATE INDEX ${sql.ref(`idx_${table.name}_deleted_published_id`)}
1408
+ ON ${sql.ref(table.name)} (deleted_at, published_at DESC, id DESC)
1409
+ `.execute(db);
1410
+ }
1411
+ }
1412
+ async function down(db) {
1413
+ const tableNames = await listTablesLike(db, "ec_%");
1414
+ for (const tableName of tableNames) {
1415
+ const table = { name: tableName };
1416
+ await sql`DROP INDEX IF EXISTS ${sql.ref(`idx_${table.name}_deleted_published_id`)}`.execute(db);
1417
+ }
1418
+ }
1419
+
1420
+ //#endregion
1421
+ //#region src/database/migrations/runner.ts
1422
+ const MIGRATIONS = Object.freeze({
1423
+ "001_initial": _001_initial_exports,
1424
+ "002_media_status": _002_media_status_exports,
1425
+ "003_schema_registry": _003_schema_registry_exports,
1426
+ "004_plugins": _004_plugins_exports,
1427
+ "005_menus": _005_menus_exports,
1428
+ "006_taxonomy_defs": _006_taxonomy_defs_exports,
1429
+ "007_widgets": _007_widgets_exports,
1430
+ "008_auth": _008_auth_exports,
1431
+ "009_user_disabled": _009_user_disabled_exports,
1432
+ "011_sections": _011_sections_exports,
1433
+ "012_search": _012_search_exports,
1434
+ "013_scheduled_publishing": _013_scheduled_publishing_exports,
1435
+ "014_draft_revisions": _014_draft_revisions_exports,
1436
+ "015_indexes": _015_indexes_exports,
1437
+ "016_api_tokens": _016_api_tokens_exports,
1438
+ "017_authorization_codes": _017_authorization_codes_exports,
1439
+ "018_seo": _018_seo_exports,
1440
+ "019_i18n": _019_i18n_exports,
1441
+ "020_collection_url_pattern": _020_collection_url_pattern_exports,
1442
+ "021_remove_section_categories": _021_remove_section_categories_exports,
1443
+ "022_marketplace_plugin_state": _022_marketplace_plugin_state_exports,
1444
+ "023_plugin_metadata": _023_plugin_metadata_exports,
1445
+ "024_media_placeholders": _024_media_placeholders_exports,
1446
+ "025_oauth_clients": _025_oauth_clients_exports,
1447
+ "026_cron_tasks": _026_cron_tasks_exports,
1448
+ "027_comments": _027_comments_exports,
1449
+ "028_drop_author_url": _028_drop_author_url_exports,
1450
+ "029_redirects": _029_redirects_exports,
1451
+ "030_widen_scheduled_index": _030_widen_scheduled_index_exports,
1452
+ "031_bylines": _031_bylines_exports,
1453
+ "032_rate_limits": _032_rate_limits_exports,
1454
+ "033_optimize_content_indexes": _033_optimize_content_indexes_exports,
1455
+ "034_published_at_index": _034_published_at_index_exports
1456
+ });
1457
+ /** Total number of registered migrations. Exported for use in tests. */
1458
+ const MIGRATION_COUNT = Object.keys(MIGRATIONS).length;
1459
+ /**
1460
+ * Migration provider that uses statically imported migrations.
1461
+ * This approach works well with bundlers and avoids filesystem access.
1462
+ */
1463
+ var StaticMigrationProvider = class {
1464
+ async getMigrations() {
1465
+ return MIGRATIONS;
1466
+ }
1467
+ };
1468
+ /** Custom migration table name */
1469
+ const MIGRATION_TABLE = "_dineway_migrations";
1470
+ const MIGRATION_LOCK_TABLE = "_dineway_migrations_lock";
1471
+ /**
1472
+ * Get migration status
1473
+ */
1474
+ async function getMigrationStatus(db) {
1475
+ const migrations = await new Migrator({
1476
+ db,
1477
+ provider: new StaticMigrationProvider(),
1478
+ migrationTableName: MIGRATION_TABLE,
1479
+ migrationLockTableName: MIGRATION_LOCK_TABLE
1480
+ }).getMigrations();
1481
+ const applied = [];
1482
+ const pending = [];
1483
+ for (const migration of migrations) if (migration.executedAt) applied.push(migration.name);
1484
+ else pending.push(migration.name);
1485
+ return {
1486
+ applied,
1487
+ pending
1488
+ };
1489
+ }
1490
+ /**
1491
+ * Run all pending migrations
1492
+ */
1493
+ async function runMigrations(db) {
1494
+ const { error, results } = await new Migrator({
1495
+ db,
1496
+ provider: new StaticMigrationProvider(),
1497
+ migrationTableName: MIGRATION_TABLE,
1498
+ migrationLockTableName: MIGRATION_LOCK_TABLE
1499
+ }).migrateToLatest();
1500
+ const applied = results?.filter((r) => r.status === "Success").map((r) => r.migrationName) ?? [];
1501
+ if (error) {
1502
+ let msg = error instanceof Error ? error.message : JSON.stringify(error);
1503
+ if (!msg && error instanceof Error && error.cause) msg = error.cause instanceof Error ? error.cause.message : JSON.stringify(error.cause);
1504
+ const failedMigration = results?.find((r) => r.status === "Error");
1505
+ if (failedMigration) msg = `${msg || "unknown error"} (migration: ${failedMigration.migrationName})`;
1506
+ throw new Error(`Migration failed: ${msg}`);
1507
+ }
1508
+ return { applied };
1509
+ }
1510
+ /**
1511
+ * Rollback the last migration
1512
+ */
1513
+ async function rollbackMigration(db) {
1514
+ const { error, results } = await new Migrator({
1515
+ db,
1516
+ provider: new StaticMigrationProvider(),
1517
+ migrationTableName: MIGRATION_TABLE,
1518
+ migrationLockTableName: MIGRATION_LOCK_TABLE
1519
+ }).migrateDown();
1520
+ const rolledBack = results?.[0]?.status === "Success" ? results[0].migrationName : null;
1521
+ if (error) {
1522
+ const msg = error instanceof Error ? error.message : JSON.stringify(error);
1523
+ throw new Error(`Rollback failed: ${msg}`);
1524
+ }
1525
+ return { rolledBack };
1526
+ }
1527
+
1528
+ //#endregion
1529
+ export { rollbackMigration as n, runMigrations as r, getMigrationStatus as t };