emdash 0.4.0 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{adapters-C2BzVy0p.d.mts → adapters-Di31kZ28.d.mts} +16 -1
- package/dist/adapters-Di31kZ28.d.mts.map +1 -0
- package/dist/{apply-Cma_PiF6.mjs → apply-B4MsLM-w.mjs} +27 -12
- package/dist/apply-B4MsLM-w.mjs.map +1 -0
- package/dist/astro/index.d.mts +6 -6
- package/dist/astro/index.d.mts.map +1 -1
- package/dist/astro/index.mjs +208 -34
- package/dist/astro/index.mjs.map +1 -1
- package/dist/astro/middleware/auth.d.mts +5 -5
- package/dist/astro/middleware/auth.d.mts.map +1 -1
- package/dist/astro/middleware/auth.mjs +34 -9
- package/dist/astro/middleware/auth.mjs.map +1 -1
- package/dist/astro/middleware/redirect.mjs +1 -1
- package/dist/astro/middleware/request-context.d.mts.map +1 -1
- package/dist/astro/middleware/request-context.mjs +5 -3
- package/dist/astro/middleware/request-context.mjs.map +1 -1
- package/dist/astro/middleware/setup.mjs +1 -1
- package/dist/astro/middleware.d.mts.map +1 -1
- package/dist/astro/middleware.mjs +460 -180
- package/dist/astro/middleware.mjs.map +1 -1
- package/dist/astro/types.d.mts +8 -8
- package/dist/{byline-WuOq9MFJ.mjs → byline-C4OVd8b3.mjs} +3 -19
- package/dist/byline-C4OVd8b3.mjs.map +1 -0
- package/dist/{bylines-C_Wsnz4L.mjs → bylines-hPTW79hw.mjs} +20 -33
- package/dist/bylines-hPTW79hw.mjs.map +1 -0
- package/dist/{cache-E3Dts-yT.mjs → cache-BkKBuIvS.mjs} +1 -1
- package/dist/{cache-E3Dts-yT.mjs.map → cache-BkKBuIvS.mjs.map} +1 -1
- package/dist/chunks-HGz06Soa.mjs +19 -0
- package/dist/chunks-HGz06Soa.mjs.map +1 -0
- package/dist/cli/index.mjs +9 -8
- package/dist/cli/index.mjs.map +1 -1
- package/dist/client/cf-access.d.mts +1 -1
- package/dist/client/index.d.mts +1 -1
- package/dist/client/index.mjs +1 -1
- package/dist/{config-DkxPrM9l.mjs → config-BXwuX8Bx.mjs} +1 -1
- package/dist/{config-DkxPrM9l.mjs.map → config-BXwuX8Bx.mjs.map} +1 -1
- package/dist/{connection-B4zVnQIa.mjs → connection-2igzM-AT.mjs} +19 -2
- package/dist/connection-2igzM-AT.mjs.map +1 -0
- package/dist/database/instrumentation.d.mts +45 -0
- package/dist/database/instrumentation.d.mts.map +1 -0
- package/dist/database/instrumentation.mjs +61 -0
- package/dist/database/instrumentation.mjs.map +1 -0
- package/dist/db/index.d.mts +3 -3
- package/dist/db/index.mjs.map +1 -1
- package/dist/db/libsql.d.mts +1 -1
- package/dist/db/postgres.d.mts +1 -1
- package/dist/db/sqlite.d.mts +1 -1
- package/dist/db-errors-D0UT85nC.mjs +41 -0
- package/dist/db-errors-D0UT85nC.mjs.map +1 -0
- package/dist/{default-PUx9RK6u.mjs → default-CME5YdZ3.mjs} +1 -1
- package/dist/{default-PUx9RK6u.mjs.map → default-CME5YdZ3.mjs.map} +1 -1
- package/dist/{error-HBeQbVhV.mjs → error-CiYn9yDu.mjs} +1 -1
- package/dist/{error-HBeQbVhV.mjs.map → error-CiYn9yDu.mjs.map} +1 -1
- package/dist/{index-CRg3PWfZ.d.mts → index-BYv0mB9g.d.mts} +135 -19
- package/dist/index-BYv0mB9g.d.mts.map +1 -0
- package/dist/index.d.mts +11 -11
- package/dist/index.mjs +20 -18
- package/dist/{load-BhSSm-TS.mjs → load-CBcmDIot.mjs} +1 -1
- package/dist/{load-BhSSm-TS.mjs.map → load-CBcmDIot.mjs.map} +1 -1
- package/dist/{loader-BYzwzORf.mjs → loader-DeiBJEMe.mjs} +18 -12
- package/dist/loader-DeiBJEMe.mjs.map +1 -0
- package/dist/{manifest-schema-BsXINkQD.mjs → manifest-schema-V30qsMft.mjs} +1 -1
- package/dist/{manifest-schema-BsXINkQD.mjs.map → manifest-schema-V30qsMft.mjs.map} +1 -1
- package/dist/media/index.d.mts +1 -1
- package/dist/media/index.mjs +1 -1
- package/dist/media/local-runtime.d.mts +7 -7
- package/dist/{mode-CyPLdO3C.mjs → mode-CpNnGkPz.mjs} +1 -1
- package/dist/{mode-CyPLdO3C.mjs.map → mode-CpNnGkPz.mjs.map} +1 -1
- package/dist/page/index.d.mts +11 -2
- package/dist/page/index.d.mts.map +1 -1
- package/dist/page/index.mjs +23 -1
- package/dist/page/index.mjs.map +1 -1
- package/dist/{placeholder-DntBEQo7.mjs → placeholder-C-fk5hYI.mjs} +1 -1
- package/dist/{placeholder-DntBEQo7.mjs.map → placeholder-C-fk5hYI.mjs.map} +1 -1
- package/dist/{placeholder-BBCtpTES.d.mts → placeholder-tzpqGWII.d.mts} +1 -1
- package/dist/{placeholder-BBCtpTES.d.mts.map → placeholder-tzpqGWII.d.mts.map} +1 -1
- package/dist/plugins/adapt-sandbox-entry.d.mts +5 -5
- package/dist/plugins/adapt-sandbox-entry.mjs +1 -1
- package/dist/{query-B6Vu0d2i.mjs → query-Bk_3vKvU.mjs} +78 -11
- package/dist/query-Bk_3vKvU.mjs.map +1 -0
- package/dist/{registry-BgnP3ysR.mjs → registry-Ci3WxVAr.mjs} +133 -97
- package/dist/registry-Ci3WxVAr.mjs.map +1 -0
- package/dist/request-cache-DiR961CV.mjs +79 -0
- package/dist/request-cache-DiR961CV.mjs.map +1 -0
- package/dist/request-context.d.mts +19 -16
- package/dist/request-context.d.mts.map +1 -1
- package/dist/request-context.mjs.map +1 -1
- package/dist/{runner-DYv3rX8P.d.mts → runner-Fl2NcUUz.d.mts} +2 -2
- package/dist/{runner-DYv3rX8P.d.mts.map → runner-Fl2NcUUz.d.mts.map} +1 -1
- package/dist/runtime.d.mts +6 -6
- package/dist/runtime.mjs +1 -1
- package/dist/{search-B5p9D36n.mjs → search-DI4bM2w9.mjs} +110 -209
- package/dist/search-DI4bM2w9.mjs.map +1 -0
- package/dist/seed/index.d.mts +2 -2
- package/dist/seed/index.mjs +8 -7
- package/dist/seo/index.d.mts +1 -1
- package/dist/storage/local.d.mts +1 -1
- package/dist/storage/local.mjs +1 -1
- package/dist/storage/s3.d.mts +1 -1
- package/dist/storage/s3.mjs +1 -1
- package/dist/taxonomies-DbrKzDju.mjs +308 -0
- package/dist/taxonomies-DbrKzDju.mjs.map +1 -0
- package/dist/{tokens-DKHiCYCB.mjs → tokens-BFPFx3CA.mjs} +1 -1
- package/dist/{tokens-DKHiCYCB.mjs.map → tokens-BFPFx3CA.mjs.map} +1 -1
- package/dist/{transport-BtcQ-Z7T.mjs → transport-BykRfpyy.mjs} +1 -1
- package/dist/{transport-BtcQ-Z7T.mjs.map → transport-BykRfpyy.mjs.map} +1 -1
- package/dist/{transport-CKQA_G44.d.mts → transport-H4Iwx7tC.d.mts} +1 -1
- package/dist/{transport-CKQA_G44.d.mts.map → transport-H4Iwx7tC.d.mts.map} +1 -1
- package/dist/{types-BmkQR1En.d.mts → types-6CUZRrZP.d.mts} +1 -1
- package/dist/{types-BmkQR1En.d.mts.map → types-6CUZRrZP.d.mts.map} +1 -1
- package/dist/{types-B6BzlZxx.d.mts → types-8xrvl_68.d.mts} +1 -1
- package/dist/{types-B6BzlZxx.d.mts.map → types-8xrvl_68.d.mts.map} +1 -1
- package/dist/{types-Dz9_WMS6.mjs → types-BH2L167P.mjs} +1 -1
- package/dist/{types-Dz9_WMS6.mjs.map → types-BH2L167P.mjs.map} +1 -1
- package/dist/{types-DNZpaCBk.d.mts → types-CFWjXmus.d.mts} +1 -1
- package/dist/{types-DNZpaCBk.d.mts.map → types-CFWjXmus.d.mts.map} +1 -1
- package/dist/{types-gLYVCXCQ.d.mts → types-CnZYHyLW.d.mts} +55 -5
- package/dist/types-CnZYHyLW.d.mts.map +1 -0
- package/dist/{types-xxCWI3j0.mjs → types-DDS4MxsT.mjs} +11 -3
- package/dist/types-DDS4MxsT.mjs.map +1 -0
- package/dist/{types-BYWYxLcp.d.mts → types-DgrIP0tF.d.mts} +9 -2
- package/dist/types-DgrIP0tF.d.mts.map +1 -0
- package/dist/{validate-CcNRWH6I.d.mts → validate-CaLH1Ia2.d.mts} +5 -52
- package/dist/validate-CaLH1Ia2.d.mts.map +1 -0
- package/dist/{validate-DuZDIxfy.mjs → validate-CqsNItbt.mjs} +2 -2
- package/dist/{validate-DuZDIxfy.mjs.map → validate-CqsNItbt.mjs.map} +1 -1
- package/dist/version-Uaf2ynPX.mjs +7 -0
- package/dist/{version-DlTDRdpv.mjs.map → version-Uaf2ynPX.mjs.map} +1 -1
- package/package.json +10 -5
- package/src/after.ts +62 -0
- package/src/api/handlers/oauth-authorization.ts +2 -32
- package/src/api/handlers/oauth-clients.ts +40 -4
- package/src/api/handlers/taxonomies.ts +13 -0
- package/src/api/oauth/redirect-uri.ts +34 -0
- package/src/api/openapi/document.ts +126 -118
- package/src/api/schemas/auth.ts +7 -0
- package/src/api/schemas/media.ts +26 -15
- package/src/api/schemas/schema.ts +1 -0
- package/src/astro/integration/font-provider.ts +176 -0
- package/src/astro/integration/index.ts +42 -0
- package/src/astro/integration/routes.ts +17 -1
- package/src/astro/integration/runtime.ts +63 -0
- package/src/astro/integration/virtual-modules.ts +41 -39
- package/src/astro/integration/vite-config.ts +16 -5
- package/src/astro/middleware/auth.ts +39 -6
- package/src/astro/middleware/request-context.ts +15 -3
- package/src/astro/middleware.ts +340 -263
- package/src/astro/routes/admin.astro +10 -5
- package/src/astro/routes/api/auth/invite/register-options.ts +78 -0
- package/src/astro/routes/api/auth/passkey/verify.ts +5 -1
- package/src/astro/routes/api/content/[collection]/[id]/terms/[taxonomy].ts +5 -0
- package/src/astro/routes/api/import/wordpress/execute.ts +1 -1
- package/src/astro/routes/api/import/wordpress-plugin/execute.ts +1 -1
- package/src/astro/routes/api/media/upload-url.ts +10 -2
- package/src/astro/routes/api/media.ts +10 -7
- package/src/astro/routes/api/oauth/register.ts +178 -0
- package/src/astro/routes/api/oauth/token.ts +15 -0
- package/src/astro/routes/api/openapi.json.ts +15 -5
- package/src/astro/routes/api/schema/collections/[slug]/fields/[fieldSlug].ts +2 -0
- package/src/astro/routes/api/schema/collections/[slug]/fields/index.ts +1 -0
- package/src/astro/routes/api/schema/collections/[slug]/fields/reorder.ts +1 -0
- package/src/astro/routes/api/search/index.ts +5 -0
- package/src/astro/routes/api/search/suggest.ts +3 -0
- package/src/astro/routes/api/taxonomies/index.ts +1 -0
- package/src/astro/routes/api/well-known/oauth-authorization-server.ts +6 -4
- package/src/bylines/index.ts +22 -45
- package/src/components/EmDashHead.astro +23 -7
- package/src/components/Table.astro +73 -41
- package/src/components/index.ts +2 -12
- package/src/components/marks.ts +20 -0
- package/src/database/connection.ts +23 -1
- package/src/database/instrumentation.ts +98 -0
- package/src/db/adapters.ts +15 -0
- package/src/emdash-runtime.ts +309 -91
- package/src/index.ts +6 -0
- package/src/loader.ts +19 -24
- package/src/menus/index.ts +6 -3
- package/src/page/index.ts +1 -1
- package/src/page/seo-contributions.ts +36 -0
- package/src/plugins/context.ts +1 -0
- package/src/plugins/email-console.ts +9 -2
- package/src/plugins/types.ts +8 -0
- package/src/query.ts +104 -7
- package/src/request-cache.ts +106 -0
- package/src/request-context.ts +19 -0
- package/src/schema/query.ts +5 -2
- package/src/schema/registry.ts +243 -166
- package/src/schema/types.ts +13 -2
- package/src/schema/zod-generator.ts +4 -0
- package/src/search/fts-manager.ts +19 -5
- package/src/search/query.ts +4 -3
- package/src/seed/apply.ts +15 -1
- package/src/settings/index.ts +24 -5
- package/src/taxonomies/index.ts +324 -124
- package/src/utils/db-errors.ts +46 -0
- package/src/virtual-modules.d.ts +31 -10
- package/src/widgets/index.ts +54 -25
- package/dist/adapters-C2BzVy0p.d.mts.map +0 -1
- package/dist/apply-Cma_PiF6.mjs.map +0 -1
- package/dist/byline-WuOq9MFJ.mjs.map +0 -1
- package/dist/bylines-C_Wsnz4L.mjs.map +0 -1
- package/dist/connection-B4zVnQIa.mjs.map +0 -1
- package/dist/index-CRg3PWfZ.d.mts.map +0 -1
- package/dist/loader-BYzwzORf.mjs.map +0 -1
- package/dist/query-B6Vu0d2i.mjs.map +0 -1
- package/dist/registry-BgnP3ysR.mjs.map +0 -1
- package/dist/search-B5p9D36n.mjs.map +0 -1
- package/dist/types-BYWYxLcp.d.mts.map +0 -1
- package/dist/types-gLYVCXCQ.d.mts.map +0 -1
- package/dist/types-xxCWI3j0.mjs.map +0 -1
- package/dist/validate-CcNRWH6I.d.mts.map +0 -1
- package/dist/version-DlTDRdpv.mjs +0 -7
package/src/schema/registry.ts
CHANGED
|
@@ -188,53 +188,69 @@ export class SchemaRegistry {
|
|
|
188
188
|
? supportsArray.includes("seo")
|
|
189
189
|
: existing.hasSeo;
|
|
190
190
|
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
?
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
?
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
191
|
+
return withTransaction(this.db, async (trx) => {
|
|
192
|
+
await trx
|
|
193
|
+
.updateTable("_emdash_collections")
|
|
194
|
+
.set({
|
|
195
|
+
label: input.label ?? existing.label,
|
|
196
|
+
label_singular: input.labelSingular ?? existing.labelSingular ?? null,
|
|
197
|
+
description: input.description ?? existing.description ?? null,
|
|
198
|
+
icon: input.icon ?? existing.icon ?? null,
|
|
199
|
+
supports: input.supports
|
|
200
|
+
? JSON.stringify(input.supports)
|
|
201
|
+
: JSON.stringify(existing.supports),
|
|
202
|
+
url_pattern:
|
|
203
|
+
input.urlPattern !== undefined
|
|
204
|
+
? (input.urlPattern ?? null)
|
|
205
|
+
: (existing.urlPattern ?? null),
|
|
206
|
+
has_seo: hasSeo ? 1 : 0,
|
|
207
|
+
comments_enabled:
|
|
208
|
+
input.commentsEnabled !== undefined
|
|
209
|
+
? input.commentsEnabled
|
|
210
|
+
? 1
|
|
211
|
+
: 0
|
|
212
|
+
: existing.commentsEnabled
|
|
213
|
+
? 1
|
|
214
|
+
: 0,
|
|
215
|
+
comments_moderation: input.commentsModeration ?? existing.commentsModeration,
|
|
216
|
+
comments_closed_after_days:
|
|
217
|
+
input.commentsClosedAfterDays !== undefined
|
|
218
|
+
? input.commentsClosedAfterDays
|
|
219
|
+
: existing.commentsClosedAfterDays,
|
|
220
|
+
comments_auto_approve_users:
|
|
221
|
+
input.commentsAutoApproveUsers !== undefined
|
|
222
|
+
? input.commentsAutoApproveUsers
|
|
223
|
+
? 1
|
|
224
|
+
: 0
|
|
225
|
+
: existing.commentsAutoApproveUsers
|
|
226
|
+
? 1
|
|
227
|
+
: 0,
|
|
228
|
+
updated_at: now,
|
|
229
|
+
})
|
|
230
|
+
.where("slug", "=", slug)
|
|
231
|
+
.execute();
|
|
231
232
|
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
233
|
+
const row = await trx
|
|
234
|
+
.selectFrom("_emdash_collections")
|
|
235
|
+
.where("slug", "=", slug)
|
|
236
|
+
.selectAll()
|
|
237
|
+
.executeTakeFirst();
|
|
238
|
+
|
|
239
|
+
if (!row) {
|
|
240
|
+
throw new SchemaError("Failed to update collection", "UPDATE_FAILED");
|
|
241
|
+
}
|
|
236
242
|
|
|
237
|
-
|
|
243
|
+
// Sync FTS state when the supports array changes (e.g. search toggled on/off)
|
|
244
|
+
if (input.supports !== undefined) {
|
|
245
|
+
const hadSearch = existing.supports.includes("search");
|
|
246
|
+
const hasSearch = (JSON.parse(row.supports ?? "[]") as string[]).includes("search");
|
|
247
|
+
if (hadSearch !== hasSearch) {
|
|
248
|
+
await this.syncSearchState(slug, trx);
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
return this.mapCollectionRow(row);
|
|
253
|
+
});
|
|
238
254
|
}
|
|
239
255
|
|
|
240
256
|
/**
|
|
@@ -257,11 +273,18 @@ export class SchemaRegistry {
|
|
|
257
273
|
}
|
|
258
274
|
}
|
|
259
275
|
|
|
260
|
-
|
|
261
|
-
|
|
276
|
+
await withTransaction(this.db, async (trx) => {
|
|
277
|
+
// Drop FTS table and triggers before dropping the content table
|
|
278
|
+
const ftsManager = new FTSManager(trx);
|
|
279
|
+
await ftsManager.dropFtsTable(slug);
|
|
280
|
+
|
|
281
|
+
// Drop the content table
|
|
282
|
+
const tableName = this.getTableName(slug);
|
|
283
|
+
await sql`DROP TABLE IF EXISTS ${sql.ref(tableName)}`.execute(trx);
|
|
262
284
|
|
|
263
|
-
|
|
264
|
-
|
|
285
|
+
// Delete the collection record (fields will cascade)
|
|
286
|
+
await trx.deleteFrom("_emdash_collections").where("id", "=", existing.id).execute();
|
|
287
|
+
});
|
|
265
288
|
}
|
|
266
289
|
|
|
267
290
|
// ============================================
|
|
@@ -336,40 +359,63 @@ export class SchemaRegistry {
|
|
|
336
359
|
|
|
337
360
|
const sortOrder = input.sortOrder ?? (maxSort?.max ?? -1) + 1;
|
|
338
361
|
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
362
|
+
return withTransaction(this.db, async (trx) => {
|
|
363
|
+
// Insert field record
|
|
364
|
+
await trx
|
|
365
|
+
.insertInto("_emdash_fields")
|
|
366
|
+
.values({
|
|
367
|
+
id,
|
|
368
|
+
collection_id: collection.id,
|
|
369
|
+
slug: input.slug,
|
|
370
|
+
label: input.label,
|
|
371
|
+
type: input.type,
|
|
372
|
+
column_type: columnType,
|
|
373
|
+
required: input.required ? 1 : 0,
|
|
374
|
+
unique: input.unique ? 1 : 0,
|
|
375
|
+
default_value:
|
|
376
|
+
input.defaultValue !== undefined ? JSON.stringify(input.defaultValue) : null,
|
|
377
|
+
validation: input.validation ? JSON.stringify(input.validation) : null,
|
|
378
|
+
widget: input.widget ?? null,
|
|
379
|
+
options: input.options ? JSON.stringify(input.options) : null,
|
|
380
|
+
sort_order: sortOrder,
|
|
381
|
+
searchable: input.searchable ? 1 : 0,
|
|
382
|
+
translatable: input.translatable === false ? 0 : 1,
|
|
383
|
+
})
|
|
384
|
+
.execute();
|
|
360
385
|
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
386
|
+
// Add column to content table — pass trx to stay on the same connection
|
|
387
|
+
await this.addColumn(
|
|
388
|
+
collectionSlug,
|
|
389
|
+
input.slug,
|
|
390
|
+
input.type,
|
|
391
|
+
{
|
|
392
|
+
required: input.required,
|
|
393
|
+
defaultValue: input.defaultValue,
|
|
394
|
+
},
|
|
395
|
+
trx,
|
|
396
|
+
);
|
|
366
397
|
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
398
|
+
// Read the created field via trx (not this.db) to avoid connection mutex deadlock
|
|
399
|
+
const fieldRow = await trx
|
|
400
|
+
.selectFrom("_emdash_fields")
|
|
401
|
+
.where("collection_id", "=", collection.id)
|
|
402
|
+
.where("slug", "=", input.slug)
|
|
403
|
+
.selectAll()
|
|
404
|
+
.executeTakeFirst();
|
|
405
|
+
|
|
406
|
+
if (!fieldRow) {
|
|
407
|
+
throw new SchemaError("Failed to create field", "CREATE_FAILED");
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
const field = this.mapFieldRow(fieldRow);
|
|
411
|
+
|
|
412
|
+
// Sync search state if this field is searchable; support checks are handled by syncSearchState()
|
|
413
|
+
if (input.searchable) {
|
|
414
|
+
await this.syncSearchState(collectionSlug, trx);
|
|
415
|
+
}
|
|
371
416
|
|
|
372
|
-
|
|
417
|
+
return field;
|
|
418
|
+
});
|
|
373
419
|
}
|
|
374
420
|
|
|
375
421
|
/**
|
|
@@ -388,84 +434,106 @@ export class SchemaRegistry {
|
|
|
388
434
|
);
|
|
389
435
|
}
|
|
390
436
|
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
input.
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
:
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
? JSON.stringify(
|
|
437
|
+
return withTransaction(this.db, async (trx) => {
|
|
438
|
+
await trx
|
|
439
|
+
.updateTable("_emdash_fields")
|
|
440
|
+
.set({
|
|
441
|
+
label: input.label ?? field.label,
|
|
442
|
+
required:
|
|
443
|
+
input.required !== undefined ? (input.required ? 1 : 0) : field.required ? 1 : 0,
|
|
444
|
+
unique: input.unique !== undefined ? (input.unique ? 1 : 0) : field.unique ? 1 : 0,
|
|
445
|
+
searchable:
|
|
446
|
+
input.searchable !== undefined ? (input.searchable ? 1 : 0) : field.searchable ? 1 : 0,
|
|
447
|
+
translatable:
|
|
448
|
+
input.translatable !== undefined
|
|
449
|
+
? input.translatable
|
|
450
|
+
? 1
|
|
451
|
+
: 0
|
|
452
|
+
: field.translatable
|
|
453
|
+
? 1
|
|
454
|
+
: 0,
|
|
455
|
+
default_value:
|
|
456
|
+
input.defaultValue !== undefined
|
|
457
|
+
? JSON.stringify(input.defaultValue)
|
|
458
|
+
: field.defaultValue !== undefined
|
|
459
|
+
? JSON.stringify(field.defaultValue)
|
|
460
|
+
: null,
|
|
461
|
+
validation: input.validation
|
|
462
|
+
? JSON.stringify(input.validation)
|
|
463
|
+
: field.validation
|
|
464
|
+
? JSON.stringify(field.validation)
|
|
412
465
|
: null,
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
: null,
|
|
424
|
-
sort_order: input.sortOrder ?? field.sortOrder,
|
|
425
|
-
})
|
|
426
|
-
.where("id", "=", field.id)
|
|
427
|
-
.execute();
|
|
466
|
+
widget: input.widget ?? field.widget ?? null,
|
|
467
|
+
options: input.options
|
|
468
|
+
? JSON.stringify(input.options)
|
|
469
|
+
: field.options
|
|
470
|
+
? JSON.stringify(field.options)
|
|
471
|
+
: null,
|
|
472
|
+
sort_order: input.sortOrder ?? field.sortOrder,
|
|
473
|
+
})
|
|
474
|
+
.where("id", "=", field.id)
|
|
475
|
+
.execute();
|
|
428
476
|
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
477
|
+
// Read the updated field via trx (not this.db) to avoid connection mutex deadlock
|
|
478
|
+
const updatedRow = await trx
|
|
479
|
+
.selectFrom("_emdash_fields")
|
|
480
|
+
.where("collection_id", "=", field.collectionId)
|
|
481
|
+
.where("slug", "=", fieldSlug)
|
|
482
|
+
.selectAll()
|
|
483
|
+
.executeTakeFirst();
|
|
433
484
|
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
485
|
+
if (!updatedRow) {
|
|
486
|
+
throw new SchemaError("Failed to update field", "UPDATE_FAILED");
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
const updated = this.mapFieldRow(updatedRow);
|
|
490
|
+
|
|
491
|
+
// If searchable changed, sync FTS state for this collection
|
|
492
|
+
const searchableChanged =
|
|
493
|
+
input.searchable !== undefined && input.searchable !== field.searchable;
|
|
494
|
+
if (searchableChanged) {
|
|
495
|
+
await this.syncSearchState(collectionSlug, trx);
|
|
496
|
+
}
|
|
440
497
|
|
|
441
|
-
|
|
498
|
+
return updated;
|
|
499
|
+
});
|
|
442
500
|
}
|
|
443
501
|
|
|
444
502
|
/**
|
|
445
|
-
*
|
|
503
|
+
* Synchronize an existing FTS index with the collection's current state.
|
|
504
|
+
*
|
|
505
|
+
* Only rebuilds or disables — never first-time enables. First-time FTS
|
|
506
|
+
* enablement is handled by the seed's explicit enableSearch call (which
|
|
507
|
+
* is try-caught) or the admin UI toggle.
|
|
508
|
+
*
|
|
509
|
+
* - FTS active + still has search support and searchable fields → rebuild
|
|
510
|
+
* - FTS active + lost search support or no searchable fields → disable
|
|
511
|
+
* - FTS not active → no-op
|
|
446
512
|
*
|
|
447
|
-
*
|
|
448
|
-
*
|
|
513
|
+
* Pass `db` when calling from within a transaction so FTS operations
|
|
514
|
+
* participate in the same transaction and are rolled back on failure.
|
|
449
515
|
*/
|
|
450
|
-
private async
|
|
451
|
-
const
|
|
516
|
+
private async syncSearchState(collectionSlug: string, db?: Kysely<Database>): Promise<void> {
|
|
517
|
+
const conn = db ?? this.db;
|
|
518
|
+
const ftsManager = new FTSManager(conn);
|
|
452
519
|
|
|
453
|
-
//
|
|
454
|
-
const
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
520
|
+
// Query via conn (not this.db) to avoid connection mutex deadlock when called inside a transaction
|
|
521
|
+
const row = await conn
|
|
522
|
+
.selectFrom("_emdash_collections")
|
|
523
|
+
.where("slug", "=", collectionSlug)
|
|
524
|
+
.select("supports")
|
|
525
|
+
.executeTakeFirst();
|
|
526
|
+
if (!row) return;
|
|
459
527
|
|
|
460
|
-
|
|
528
|
+
const wantsSearch = (JSON.parse(row.supports ?? "[]") as string[]).includes("search");
|
|
461
529
|
const searchableFields = await ftsManager.getSearchableFields(collectionSlug);
|
|
530
|
+
const config = await ftsManager.getSearchConfig(collectionSlug);
|
|
531
|
+
const ftsActive = config?.enabled === true;
|
|
462
532
|
|
|
463
|
-
if (searchableFields.length
|
|
464
|
-
|
|
533
|
+
if (wantsSearch && searchableFields.length > 0 && ftsActive) {
|
|
534
|
+
await ftsManager.rebuildIndex(collectionSlug, searchableFields, config?.weights);
|
|
535
|
+
} else if (ftsActive && (!wantsSearch || searchableFields.length === 0)) {
|
|
465
536
|
await ftsManager.disableSearch(collectionSlug);
|
|
466
|
-
} else {
|
|
467
|
-
// Rebuild the index with updated fields
|
|
468
|
-
await ftsManager.rebuildIndex(collectionSlug, searchableFields, config.weights);
|
|
469
537
|
}
|
|
470
538
|
}
|
|
471
539
|
|
|
@@ -481,11 +549,22 @@ export class SchemaRegistry {
|
|
|
481
549
|
);
|
|
482
550
|
}
|
|
483
551
|
|
|
484
|
-
|
|
485
|
-
|
|
552
|
+
await withTransaction(this.db, async (trx) => {
|
|
553
|
+
// Delete the field record first so syncSearchState sees the updated field list.
|
|
554
|
+
// This ordering matters for searchable fields: SQLite prevents dropping a column
|
|
555
|
+
// that is still referenced by a trigger. syncSearchState drops and recreates the
|
|
556
|
+
// FTS triggers based on the remaining searchable fields, clearing the dependency
|
|
557
|
+
// before we attempt the ALTER TABLE DROP COLUMN below.
|
|
558
|
+
await trx.deleteFrom("_emdash_fields").where("id", "=", field.id).execute();
|
|
559
|
+
|
|
560
|
+
// If the deleted field was searchable, sync FTS state (removes old triggers)
|
|
561
|
+
if (field.searchable) {
|
|
562
|
+
await this.syncSearchState(collectionSlug, trx);
|
|
563
|
+
}
|
|
486
564
|
|
|
487
|
-
|
|
488
|
-
|
|
565
|
+
// Drop column from content table — safe now because FTS triggers are gone
|
|
566
|
+
await this.dropColumn(collectionSlug, fieldSlug, trx);
|
|
567
|
+
});
|
|
489
568
|
}
|
|
490
569
|
|
|
491
570
|
/**
|
|
@@ -603,14 +682,6 @@ export class SchemaRegistry {
|
|
|
603
682
|
`.execute(conn);
|
|
604
683
|
}
|
|
605
684
|
|
|
606
|
-
/**
|
|
607
|
-
* Drop a content table
|
|
608
|
-
*/
|
|
609
|
-
private async dropContentTable(slug: string): Promise<void> {
|
|
610
|
-
const tableName = this.getTableName(slug);
|
|
611
|
-
await sql`DROP TABLE IF EXISTS ${sql.ref(tableName)}`.execute(this.db);
|
|
612
|
-
}
|
|
613
|
-
|
|
614
685
|
/**
|
|
615
686
|
* Add a column to a content table
|
|
616
687
|
*/
|
|
@@ -619,7 +690,9 @@ export class SchemaRegistry {
|
|
|
619
690
|
fieldSlug: string,
|
|
620
691
|
fieldType: FieldType,
|
|
621
692
|
options?: { required?: boolean; defaultValue?: unknown },
|
|
693
|
+
db?: Kysely<Database>,
|
|
622
694
|
): Promise<void> {
|
|
695
|
+
const conn = db ?? this.db;
|
|
623
696
|
const tableName = this.getTableName(collectionSlug);
|
|
624
697
|
const columnType = FIELD_TYPE_TO_COLUMN[fieldType];
|
|
625
698
|
const columnName = this.getColumnName(fieldSlug);
|
|
@@ -629,35 +702,39 @@ export class SchemaRegistry {
|
|
|
629
702
|
if (options?.required && options?.defaultValue !== undefined) {
|
|
630
703
|
const defaultVal = this.formatDefaultValue(options.defaultValue, fieldType);
|
|
631
704
|
await sql`
|
|
632
|
-
ALTER TABLE ${sql.ref(tableName)}
|
|
705
|
+
ALTER TABLE ${sql.ref(tableName)}
|
|
633
706
|
ADD COLUMN ${sql.ref(columnName)} ${sql.raw(columnType)} NOT NULL DEFAULT ${sql.raw(defaultVal)}
|
|
634
|
-
`.execute(
|
|
707
|
+
`.execute(conn);
|
|
635
708
|
} else if (options?.required) {
|
|
636
709
|
// For required fields without default, use empty string/0 as default
|
|
637
710
|
const defaultVal = this.getEmptyDefault(fieldType);
|
|
638
711
|
await sql`
|
|
639
|
-
ALTER TABLE ${sql.ref(tableName)}
|
|
712
|
+
ALTER TABLE ${sql.ref(tableName)}
|
|
640
713
|
ADD COLUMN ${sql.ref(columnName)} ${sql.raw(columnType)} NOT NULL DEFAULT ${sql.raw(defaultVal)}
|
|
641
|
-
`.execute(
|
|
714
|
+
`.execute(conn);
|
|
642
715
|
} else {
|
|
643
716
|
await sql`
|
|
644
|
-
ALTER TABLE ${sql.ref(tableName)}
|
|
717
|
+
ALTER TABLE ${sql.ref(tableName)}
|
|
645
718
|
ADD COLUMN ${sql.ref(columnName)} ${sql.raw(columnType)}
|
|
646
|
-
`.execute(
|
|
719
|
+
`.execute(conn);
|
|
647
720
|
}
|
|
648
721
|
}
|
|
649
722
|
|
|
650
723
|
/**
|
|
651
724
|
* Drop a column from a content table
|
|
652
725
|
*/
|
|
653
|
-
private async dropColumn(
|
|
726
|
+
private async dropColumn(
|
|
727
|
+
collectionSlug: string,
|
|
728
|
+
fieldSlug: string,
|
|
729
|
+
db?: Kysely<Database>,
|
|
730
|
+
): Promise<void> {
|
|
654
731
|
const tableName = this.getTableName(collectionSlug);
|
|
655
732
|
const columnName = this.getColumnName(fieldSlug);
|
|
656
733
|
|
|
657
734
|
await sql`
|
|
658
|
-
ALTER TABLE ${sql.ref(tableName)}
|
|
735
|
+
ALTER TABLE ${sql.ref(tableName)}
|
|
659
736
|
DROP COLUMN ${sql.ref(columnName)}
|
|
660
|
-
`.execute(this.db);
|
|
737
|
+
`.execute(db ?? this.db);
|
|
661
738
|
}
|
|
662
739
|
|
|
663
740
|
// ============================================
|
|
@@ -671,7 +748,7 @@ export class SchemaRegistry {
|
|
|
671
748
|
const tableName = this.getTableName(slug);
|
|
672
749
|
try {
|
|
673
750
|
const result = await sql<{ count: number }>`
|
|
674
|
-
SELECT COUNT(*) as count FROM ${sql.ref(tableName)}
|
|
751
|
+
SELECT COUNT(*) as count FROM ${sql.ref(tableName)}
|
|
675
752
|
WHERE deleted_at IS NULL
|
|
676
753
|
`.execute(this.db);
|
|
677
754
|
return (result.rows[0]?.count ?? 0) > 0;
|
package/src/schema/types.ts
CHANGED
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
export type FieldType =
|
|
12
12
|
| "string"
|
|
13
13
|
| "text"
|
|
14
|
+
| "url"
|
|
14
15
|
| "number"
|
|
15
16
|
| "integer"
|
|
16
17
|
| "boolean"
|
|
@@ -31,6 +32,7 @@ export type FieldType =
|
|
|
31
32
|
export const FIELD_TYPES: readonly FieldType[] = [
|
|
32
33
|
"string",
|
|
33
34
|
"text",
|
|
35
|
+
"url",
|
|
34
36
|
"number",
|
|
35
37
|
"integer",
|
|
36
38
|
"boolean",
|
|
@@ -69,6 +71,7 @@ export const FIELD_TYPE_TO_COLUMN: Record<FieldType, ColumnType> = {
|
|
|
69
71
|
reference: "TEXT",
|
|
70
72
|
json: "JSON",
|
|
71
73
|
slug: "TEXT",
|
|
74
|
+
url: "TEXT",
|
|
72
75
|
repeater: "JSON",
|
|
73
76
|
};
|
|
74
77
|
|
|
@@ -99,7 +102,7 @@ export type CollectionSource =
|
|
|
99
102
|
/** Sub-field definition for repeater fields */
|
|
100
103
|
export interface RepeaterSubField {
|
|
101
104
|
slug: string;
|
|
102
|
-
type: "string" | "text" | "number" | "integer" | "boolean" | "datetime" | "select";
|
|
105
|
+
type: "string" | "text" | "url" | "number" | "integer" | "boolean" | "datetime" | "select";
|
|
103
106
|
label: string;
|
|
104
107
|
required?: boolean;
|
|
105
108
|
options?: string[]; // For select sub-fields
|
|
@@ -109,6 +112,7 @@ export interface RepeaterSubField {
|
|
|
109
112
|
export const REPEATER_SUB_FIELD_TYPES = [
|
|
110
113
|
"string",
|
|
111
114
|
"text",
|
|
115
|
+
"url",
|
|
112
116
|
"number",
|
|
113
117
|
"integer",
|
|
114
118
|
"boolean",
|
|
@@ -270,7 +274,10 @@ export interface CollectionWithFields extends Collection {
|
|
|
270
274
|
}
|
|
271
275
|
|
|
272
276
|
/**
|
|
273
|
-
* Reserved field slugs that cannot be used
|
|
277
|
+
* Reserved field slugs that cannot be used.
|
|
278
|
+
*
|
|
279
|
+
* Includes names reserved for runtime hydration (`terms`, `bylines`, `byline`)
|
|
280
|
+
* so user-defined fields never shadow the auto-hydrated values on entry.data.
|
|
274
281
|
*/
|
|
275
282
|
export const RESERVED_FIELD_SLUGS = [
|
|
276
283
|
"id",
|
|
@@ -286,6 +293,10 @@ export const RESERVED_FIELD_SLUGS = [
|
|
|
286
293
|
"version",
|
|
287
294
|
"live_revision_id",
|
|
288
295
|
"draft_revision_id",
|
|
296
|
+
// Runtime-hydrated fields
|
|
297
|
+
"terms",
|
|
298
|
+
"bylines",
|
|
299
|
+
"byline",
|
|
289
300
|
];
|
|
290
301
|
|
|
291
302
|
/**
|
|
@@ -53,6 +53,9 @@ export function generateFieldSchema(field: Field): ZodTypeAny {
|
|
|
53
53
|
*/
|
|
54
54
|
function getBaseSchema(type: FieldType, field: Field): ZodTypeAny {
|
|
55
55
|
switch (type) {
|
|
56
|
+
case "url":
|
|
57
|
+
return z.string().url();
|
|
58
|
+
|
|
56
59
|
case "string":
|
|
57
60
|
case "text":
|
|
58
61
|
case "slug":
|
|
@@ -330,6 +333,7 @@ function fieldTypeToTypeScript(field: Field): string {
|
|
|
330
333
|
case "string":
|
|
331
334
|
case "text":
|
|
332
335
|
case "slug":
|
|
336
|
+
case "url":
|
|
333
337
|
case "datetime":
|
|
334
338
|
return "string";
|
|
335
339
|
|
|
@@ -113,7 +113,6 @@ export class FTSManager {
|
|
|
113
113
|
const contentTable = this.getContentTableName(collectionSlug);
|
|
114
114
|
const fieldList = searchableFields.join(", ");
|
|
115
115
|
const newFieldList = searchableFields.map((f) => `NEW.${f}`).join(", ");
|
|
116
|
-
|
|
117
116
|
// Insert trigger - only index non-deleted content
|
|
118
117
|
await sql
|
|
119
118
|
.raw(`
|
|
@@ -342,7 +341,8 @@ export class FTSManager {
|
|
|
342
341
|
async disableSearch(collectionSlug: string): Promise<void> {
|
|
343
342
|
if (!isSqlite(this.db)) return;
|
|
344
343
|
await this.dropFtsTable(collectionSlug);
|
|
345
|
-
await this.
|
|
344
|
+
const existing = await this.getSearchConfig(collectionSlug);
|
|
345
|
+
await this.setSearchConfig(collectionSlug, { enabled: false, weights: existing?.weights });
|
|
346
346
|
}
|
|
347
347
|
|
|
348
348
|
/**
|
|
@@ -354,6 +354,7 @@ export class FTSManager {
|
|
|
354
354
|
if (!isSqlite(this.db)) return null;
|
|
355
355
|
this.validateInputs(collectionSlug);
|
|
356
356
|
const ftsTable = this.getFtsTableName(collectionSlug);
|
|
357
|
+
const ftsDocsizeTable = `${ftsTable}_docsize`;
|
|
357
358
|
|
|
358
359
|
// Check if table exists
|
|
359
360
|
if (!(await this.ftsTableExists(collectionSlug))) {
|
|
@@ -362,7 +363,7 @@ export class FTSManager {
|
|
|
362
363
|
|
|
363
364
|
// Count indexed rows
|
|
364
365
|
const result = await sql<{ count: number }>`
|
|
365
|
-
SELECT COUNT(*) as count FROM "${sql.raw(
|
|
366
|
+
SELECT COUNT(*) as count FROM "${sql.raw(ftsDocsizeTable)}"
|
|
366
367
|
`.execute(this.db);
|
|
367
368
|
|
|
368
369
|
return {
|
|
@@ -381,10 +382,19 @@ export class FTSManager {
|
|
|
381
382
|
if (!isSqlite(this.db)) return false;
|
|
382
383
|
this.validateInputs(collectionSlug);
|
|
383
384
|
const ftsTable = this.getFtsTableName(collectionSlug);
|
|
385
|
+
const ftsDocsizeTable = `${ftsTable}_docsize`;
|
|
384
386
|
const contentTable = this.getContentTableName(collectionSlug);
|
|
387
|
+
const fields = await this.getSearchableFields(collectionSlug);
|
|
388
|
+
const config = await this.getSearchConfig(collectionSlug);
|
|
385
389
|
|
|
386
390
|
if (!(await this.ftsTableExists(collectionSlug))) {
|
|
387
|
-
|
|
391
|
+
if (!config?.enabled || fields.length === 0) {
|
|
392
|
+
return false;
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
console.warn(`FTS index for "${collectionSlug}" is missing. Rebuilding.`);
|
|
396
|
+
await this.rebuildIndex(collectionSlug, fields, config.weights);
|
|
397
|
+
return true;
|
|
388
398
|
}
|
|
389
399
|
|
|
390
400
|
// Check 1: Row count mismatch
|
|
@@ -393,8 +403,12 @@ export class FTSManager {
|
|
|
393
403
|
WHERE deleted_at IS NULL
|
|
394
404
|
`.execute(this.db);
|
|
395
405
|
|
|
406
|
+
// For external-content FTS tables, COUNT(*) on the virtual table is
|
|
407
|
+
// answered from the backing content table, including soft-deleted rows.
|
|
408
|
+
// The docsize shadow table tracks the rows actually present in the
|
|
409
|
+
// full-text index, which is what we need for repair decisions.
|
|
396
410
|
const ftsCount = await sql<{ count: number }>`
|
|
397
|
-
SELECT COUNT(*) as count FROM "${sql.raw(
|
|
411
|
+
SELECT COUNT(*) as count FROM "${sql.raw(ftsDocsizeTable)}"
|
|
398
412
|
`.execute(this.db);
|
|
399
413
|
|
|
400
414
|
const contentRows = contentCount.rows[0]?.count ?? 0;
|