@plakboek/db 0.2.0 → 0.4.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/README.md +10 -3
- package/dist/index.js +446 -8
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -69,9 +69,16 @@ gives up waiting for the lock before the first call finishes.
|
|
|
69
69
|
|
|
70
70
|
`MIGRATIONS` starts with `0001_auth_core`, which creates the authentication
|
|
71
71
|
tables (`"user"`, `session`, `account`, `verification`, `two_factor`) and
|
|
72
|
-
the append-only `audit_log`.
|
|
73
|
-
|
|
74
|
-
|
|
72
|
+
the append-only `audit_log`. `0002_content_engine` follows it, creating the
|
|
73
|
+
content engine tables (`content_types`, `content_type_fields`,
|
|
74
|
+
`content_entries`, `entry_revisions` and their history, seed, reference and
|
|
75
|
+
settings tables) used by `@plakboek/content`. `0003_page_block_engine`
|
|
76
|
+
creates the page and block-tree tables (`pages`, `page_blocks`,
|
|
77
|
+
`block_revisions`, `page_publications`, `page_url_history`,
|
|
78
|
+
`page_engine_settings`) used by `@plakboek/pages`. See
|
|
79
|
+
[`MIGRATIONS.md`](./MIGRATIONS.md) for how later migrations are added, the
|
|
80
|
+
transactional-vs-existence-guarded-statement decision, and what each thrown
|
|
81
|
+
error class means.
|
|
75
82
|
|
|
76
83
|
## Errors
|
|
77
84
|
|
package/dist/index.js
CHANGED
|
@@ -142,14 +142,18 @@ async function withMigrationLock(client, fn, options) {
|
|
|
142
142
|
* ../../MIGRATIONS.md for the full authoring convention.
|
|
143
143
|
*
|
|
144
144
|
* `0001_auth_core` is the first shipped entry: the better-auth core tables
|
|
145
|
-
* and the audit log.
|
|
146
|
-
*
|
|
147
|
-
*
|
|
145
|
+
* and the audit log. `0002_content_engine` is the second: the content-type,
|
|
146
|
+
* field and entry tables `@plakboek/content` builds on. `0003_page_block_engine`
|
|
147
|
+
* is the third: the page and block-tree tables `@plakboek/pages` builds on.
|
|
148
|
+
* The runner's failure modes (ordering, checksums, partial application,
|
|
149
|
+
* locking) are exercised through fixture registries in this package's own
|
|
150
|
+
* tests (tests/fixtures/migrations.ts).
|
|
148
151
|
*/
|
|
149
|
-
const MIGRATIONS = Object.freeze([
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
152
|
+
const MIGRATIONS = Object.freeze([
|
|
153
|
+
{
|
|
154
|
+
name: "0001_auth_core",
|
|
155
|
+
transactional: true,
|
|
156
|
+
sql: `
|
|
153
157
|
CREATE TABLE IF NOT EXISTS "user" (
|
|
154
158
|
"id" text PRIMARY KEY NOT NULL,
|
|
155
159
|
"name" text NOT NULL,
|
|
@@ -255,7 +259,441 @@ CREATE INDEX IF NOT EXISTS "audit_log_actor_idx"
|
|
|
255
259
|
CREATE INDEX IF NOT EXISTS "audit_log_created_at_idx"
|
|
256
260
|
ON "audit_log" USING btree ("created_at");
|
|
257
261
|
`
|
|
258
|
-
}
|
|
262
|
+
},
|
|
263
|
+
{
|
|
264
|
+
name: "0002_content_engine",
|
|
265
|
+
transactional: true,
|
|
266
|
+
sql: `
|
|
267
|
+
CREATE TABLE IF NOT EXISTS "content_types" (
|
|
268
|
+
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
269
|
+
"key" text NOT NULL,
|
|
270
|
+
"slug" text NOT NULL,
|
|
271
|
+
"label_singular" text NOT NULL,
|
|
272
|
+
"label_plural" text NOT NULL,
|
|
273
|
+
"description" text,
|
|
274
|
+
"routable" boolean NOT NULL DEFAULT false,
|
|
275
|
+
"url_pattern" text,
|
|
276
|
+
"singleton" boolean NOT NULL DEFAULT false,
|
|
277
|
+
"drafts" boolean NOT NULL DEFAULT false,
|
|
278
|
+
"revisions" boolean NOT NULL DEFAULT false,
|
|
279
|
+
"revision_mode" text,
|
|
280
|
+
"edit_locking" boolean NOT NULL DEFAULT false,
|
|
281
|
+
"seo" boolean NOT NULL DEFAULT false,
|
|
282
|
+
"title_field_key" text,
|
|
283
|
+
"created_at" timestamp with time zone NOT NULL,
|
|
284
|
+
"updated_at" timestamp with time zone NOT NULL,
|
|
285
|
+
CONSTRAINT "content_types_key_unique" UNIQUE ("key"),
|
|
286
|
+
CONSTRAINT "content_types_slug_unique" UNIQUE ("slug"),
|
|
287
|
+
CONSTRAINT "content_types_revision_mode_check"
|
|
288
|
+
CHECK ("revision_mode" IN ('on_publish', 'on_every_save')),
|
|
289
|
+
CONSTRAINT "content_types_revision_mode_presence_check"
|
|
290
|
+
CHECK (
|
|
291
|
+
("revisions" AND "revision_mode" IS NOT NULL)
|
|
292
|
+
OR (NOT "revisions" AND "revision_mode" IS NULL)
|
|
293
|
+
),
|
|
294
|
+
CONSTRAINT "content_types_singleton_not_routable_check"
|
|
295
|
+
CHECK (NOT ("singleton" AND "routable"))
|
|
296
|
+
);
|
|
297
|
+
|
|
298
|
+
CREATE TABLE IF NOT EXISTS "content_type_fields" (
|
|
299
|
+
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
300
|
+
"content_type_id" uuid NOT NULL,
|
|
301
|
+
"key" text NOT NULL,
|
|
302
|
+
"label" text NOT NULL,
|
|
303
|
+
"field_type" text NOT NULL,
|
|
304
|
+
"translatable" boolean NOT NULL DEFAULT true,
|
|
305
|
+
"required" boolean NOT NULL DEFAULT false,
|
|
306
|
+
"options" jsonb NOT NULL DEFAULT '{}'::jsonb,
|
|
307
|
+
"widget" text NOT NULL,
|
|
308
|
+
"widget_options" jsonb NOT NULL DEFAULT '{}'::jsonb,
|
|
309
|
+
"default_value" jsonb,
|
|
310
|
+
"sort_order" integer NOT NULL,
|
|
311
|
+
"created_at" timestamp with time zone NOT NULL,
|
|
312
|
+
"updated_at" timestamp with time zone NOT NULL,
|
|
313
|
+
CONSTRAINT "content_type_fields_content_type_id_fk" FOREIGN KEY ("content_type_id")
|
|
314
|
+
REFERENCES "content_types" ("id") ON DELETE CASCADE,
|
|
315
|
+
CONSTRAINT "content_type_fields_type_key_unique" UNIQUE ("content_type_id", "key"),
|
|
316
|
+
CONSTRAINT "content_type_fields_field_type_check" CHECK (
|
|
317
|
+
"field_type" IN (
|
|
318
|
+
'short_text', 'long_text', 'rich_text', 'number', 'integer', 'boolean',
|
|
319
|
+
'date_time', 'select', 'multi_select', 'image', 'file', 'reference',
|
|
320
|
+
'json', 'slug', 'url', 'repeater'
|
|
321
|
+
)
|
|
322
|
+
),
|
|
323
|
+
CONSTRAINT "content_type_fields_key_check"
|
|
324
|
+
CHECK ("key" ~ '^[a-z][A-Za-z0-9]{0,63}$')
|
|
325
|
+
);
|
|
326
|
+
|
|
327
|
+
CREATE SEQUENCE IF NOT EXISTS "content_entry_public_id_seq";
|
|
328
|
+
|
|
329
|
+
CREATE TABLE IF NOT EXISTS "content_entries" (
|
|
330
|
+
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
331
|
+
"content_type_id" uuid NOT NULL,
|
|
332
|
+
"translation_group" uuid NOT NULL,
|
|
333
|
+
"public_id" bigint NOT NULL,
|
|
334
|
+
"locale" text NOT NULL,
|
|
335
|
+
"slug" text,
|
|
336
|
+
"slug_source" text,
|
|
337
|
+
"status" text NOT NULL DEFAULT 'draft',
|
|
338
|
+
"data" jsonb NOT NULL DEFAULT '{}'::jsonb,
|
|
339
|
+
"seo" jsonb,
|
|
340
|
+
"version" integer NOT NULL DEFAULT 1,
|
|
341
|
+
"draft_revision_id" uuid,
|
|
342
|
+
"live_revision_id" uuid,
|
|
343
|
+
"resolved_path" text,
|
|
344
|
+
"locked_by" text,
|
|
345
|
+
"locked_at" timestamp with time zone,
|
|
346
|
+
"created_by" text,
|
|
347
|
+
"updated_by" text,
|
|
348
|
+
"created_at" timestamp with time zone NOT NULL,
|
|
349
|
+
"updated_at" timestamp with time zone NOT NULL,
|
|
350
|
+
"published_at" timestamp with time zone,
|
|
351
|
+
"first_published_at" timestamp with time zone,
|
|
352
|
+
"scheduled_at" timestamp with time zone,
|
|
353
|
+
"trashed_at" timestamp with time zone,
|
|
354
|
+
CONSTRAINT "content_entries_content_type_id_fk" FOREIGN KEY ("content_type_id")
|
|
355
|
+
REFERENCES "content_types" ("id") ON DELETE RESTRICT,
|
|
356
|
+
CONSTRAINT "content_entries_locked_by_user_id_fk" FOREIGN KEY ("locked_by")
|
|
357
|
+
REFERENCES "user" ("id") ON DELETE SET NULL,
|
|
358
|
+
CONSTRAINT "content_entries_created_by_user_id_fk" FOREIGN KEY ("created_by")
|
|
359
|
+
REFERENCES "user" ("id") ON DELETE SET NULL,
|
|
360
|
+
CONSTRAINT "content_entries_updated_by_user_id_fk" FOREIGN KEY ("updated_by")
|
|
361
|
+
REFERENCES "user" ("id") ON DELETE SET NULL,
|
|
362
|
+
CONSTRAINT "content_entries_type_locale_slug_unique"
|
|
363
|
+
UNIQUE ("content_type_id", "locale", "slug"),
|
|
364
|
+
CONSTRAINT "content_entries_group_locale_unique"
|
|
365
|
+
UNIQUE ("translation_group", "locale"),
|
|
366
|
+
CONSTRAINT "content_entries_status_check"
|
|
367
|
+
CHECK ("status" IN ('draft', 'published', 'scheduled', 'trashed')),
|
|
368
|
+
CONSTRAINT "content_entries_slug_source_check"
|
|
369
|
+
CHECK ("slug_source" IN ('generated', 'manual')),
|
|
370
|
+
CONSTRAINT "content_entries_data_object_check"
|
|
371
|
+
CHECK (jsonb_typeof("data") = 'object'),
|
|
372
|
+
CONSTRAINT "content_entries_scheduled_at_check"
|
|
373
|
+
CHECK ("status" <> 'scheduled' OR "scheduled_at" IS NOT NULL),
|
|
374
|
+
CONSTRAINT "content_entries_resolved_path_check"
|
|
375
|
+
CHECK ("resolved_path" IS NULL OR "status" = 'published'),
|
|
376
|
+
CONSTRAINT "content_entries_lock_pair_check"
|
|
377
|
+
CHECK (("locked_by" IS NULL) = ("locked_at" IS NULL))
|
|
378
|
+
);
|
|
379
|
+
|
|
380
|
+
CREATE TABLE IF NOT EXISTS "entry_revisions" (
|
|
381
|
+
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
382
|
+
"entry_id" uuid NOT NULL,
|
|
383
|
+
"locale" text NOT NULL,
|
|
384
|
+
"kind" text NOT NULL,
|
|
385
|
+
"data" jsonb NOT NULL,
|
|
386
|
+
"field_ids" jsonb NOT NULL,
|
|
387
|
+
"seo" jsonb,
|
|
388
|
+
"slug" text,
|
|
389
|
+
"author_id" text,
|
|
390
|
+
"created_at" timestamp with time zone NOT NULL,
|
|
391
|
+
CONSTRAINT "entry_revisions_entry_id_fk" FOREIGN KEY ("entry_id")
|
|
392
|
+
REFERENCES "content_entries" ("id") ON DELETE CASCADE,
|
|
393
|
+
CONSTRAINT "entry_revisions_author_id_user_id_fk" FOREIGN KEY ("author_id")
|
|
394
|
+
REFERENCES "user" ("id") ON DELETE SET NULL,
|
|
395
|
+
CONSTRAINT "entry_revisions_kind_check" CHECK ("kind" IN ('save', 'publish'))
|
|
396
|
+
);
|
|
397
|
+
|
|
398
|
+
DO $$
|
|
399
|
+
BEGIN
|
|
400
|
+
IF NOT EXISTS (
|
|
401
|
+
SELECT 1 FROM pg_constraint WHERE conname = 'content_entries_draft_revision_id_fk'
|
|
402
|
+
) THEN
|
|
403
|
+
ALTER TABLE "content_entries"
|
|
404
|
+
ADD CONSTRAINT "content_entries_draft_revision_id_fk"
|
|
405
|
+
FOREIGN KEY ("draft_revision_id") REFERENCES "entry_revisions" ("id") ON DELETE SET NULL;
|
|
406
|
+
END IF;
|
|
407
|
+
END $$;
|
|
408
|
+
|
|
409
|
+
DO $$
|
|
410
|
+
BEGIN
|
|
411
|
+
IF NOT EXISTS (
|
|
412
|
+
SELECT 1 FROM pg_constraint WHERE conname = 'content_entries_live_revision_id_fk'
|
|
413
|
+
) THEN
|
|
414
|
+
ALTER TABLE "content_entries"
|
|
415
|
+
ADD CONSTRAINT "content_entries_live_revision_id_fk"
|
|
416
|
+
FOREIGN KEY ("live_revision_id") REFERENCES "entry_revisions" ("id") ON DELETE SET NULL;
|
|
417
|
+
END IF;
|
|
418
|
+
END $$;
|
|
419
|
+
|
|
420
|
+
CREATE TABLE IF NOT EXISTS "content_type_key_history" (
|
|
421
|
+
"id" bigserial PRIMARY KEY,
|
|
422
|
+
"content_type_id" uuid NOT NULL,
|
|
423
|
+
"old_key" text,
|
|
424
|
+
"new_key" text NOT NULL,
|
|
425
|
+
"changed_by" text,
|
|
426
|
+
"changed_at" timestamp with time zone NOT NULL,
|
|
427
|
+
CONSTRAINT "content_type_key_history_content_type_id_fk" FOREIGN KEY ("content_type_id")
|
|
428
|
+
REFERENCES "content_types" ("id") ON DELETE CASCADE,
|
|
429
|
+
CONSTRAINT "content_type_key_history_changed_by_user_id_fk" FOREIGN KEY ("changed_by")
|
|
430
|
+
REFERENCES "user" ("id") ON DELETE SET NULL
|
|
431
|
+
);
|
|
432
|
+
|
|
433
|
+
CREATE TABLE IF NOT EXISTS "content_field_key_history" (
|
|
434
|
+
"id" bigserial PRIMARY KEY,
|
|
435
|
+
"content_type_id" uuid NOT NULL,
|
|
436
|
+
"field_id" uuid NOT NULL,
|
|
437
|
+
"old_key" text,
|
|
438
|
+
"new_key" text NOT NULL,
|
|
439
|
+
"changed_by" text,
|
|
440
|
+
"changed_at" timestamp with time zone NOT NULL,
|
|
441
|
+
CONSTRAINT "content_field_key_history_content_type_id_fk" FOREIGN KEY ("content_type_id")
|
|
442
|
+
REFERENCES "content_types" ("id") ON DELETE CASCADE,
|
|
443
|
+
CONSTRAINT "content_field_key_history_changed_by_user_id_fk" FOREIGN KEY ("changed_by")
|
|
444
|
+
REFERENCES "user" ("id") ON DELETE SET NULL
|
|
445
|
+
);
|
|
446
|
+
|
|
447
|
+
CREATE TABLE IF NOT EXISTS "content_entry_url_history" (
|
|
448
|
+
"id" bigserial PRIMARY KEY,
|
|
449
|
+
"entry_id" uuid,
|
|
450
|
+
"content_type_id" uuid NOT NULL,
|
|
451
|
+
"translation_group" uuid NOT NULL,
|
|
452
|
+
"locale" text NOT NULL,
|
|
453
|
+
"old_path" text NOT NULL,
|
|
454
|
+
"reason" text NOT NULL,
|
|
455
|
+
"changed_at" timestamp with time zone NOT NULL,
|
|
456
|
+
CONSTRAINT "content_entry_url_history_entry_id_fk" FOREIGN KEY ("entry_id")
|
|
457
|
+
REFERENCES "content_entries" ("id") ON DELETE SET NULL,
|
|
458
|
+
CONSTRAINT "content_entry_url_history_content_type_id_fk" FOREIGN KEY ("content_type_id")
|
|
459
|
+
REFERENCES "content_types" ("id") ON DELETE CASCADE,
|
|
460
|
+
CONSTRAINT "content_entry_url_history_reason_check" CHECK (
|
|
461
|
+
"reason" IN ('slug_changed', 'pattern_changed', 'unpublished', 'trashed', 'deleted')
|
|
462
|
+
)
|
|
463
|
+
);
|
|
464
|
+
|
|
465
|
+
CREATE TABLE IF NOT EXISTS "content_type_seed_applications" (
|
|
466
|
+
"seed_id" text PRIMARY KEY,
|
|
467
|
+
"kind" text NOT NULL,
|
|
468
|
+
"target_id" uuid NOT NULL,
|
|
469
|
+
"applied_at" timestamp with time zone NOT NULL,
|
|
470
|
+
CONSTRAINT "content_type_seed_applications_kind_check" CHECK ("kind" IN ('type', 'field'))
|
|
471
|
+
);
|
|
472
|
+
|
|
473
|
+
CREATE TABLE IF NOT EXISTS "content_entry_references" (
|
|
474
|
+
"source_entry_id" uuid NOT NULL,
|
|
475
|
+
"field_id" uuid NOT NULL,
|
|
476
|
+
"target_translation_group" uuid NOT NULL,
|
|
477
|
+
PRIMARY KEY ("source_entry_id", "field_id", "target_translation_group"),
|
|
478
|
+
CONSTRAINT "content_entry_references_source_entry_id_fk" FOREIGN KEY ("source_entry_id")
|
|
479
|
+
REFERENCES "content_entries" ("id") ON DELETE CASCADE
|
|
480
|
+
);
|
|
481
|
+
|
|
482
|
+
CREATE TABLE IF NOT EXISTS "content_engine_settings" (
|
|
483
|
+
"id" smallint PRIMARY KEY DEFAULT 1,
|
|
484
|
+
"revision_cap" integer NOT NULL DEFAULT 0,
|
|
485
|
+
"updated_at" timestamp with time zone NOT NULL DEFAULT now(),
|
|
486
|
+
CONSTRAINT "content_engine_settings_id_check" CHECK ("id" = 1),
|
|
487
|
+
CONSTRAINT "content_engine_settings_revision_cap_check" CHECK ("revision_cap" >= 0)
|
|
488
|
+
);
|
|
489
|
+
|
|
490
|
+
INSERT INTO "content_engine_settings" ("id") VALUES (1) ON CONFLICT ("id") DO NOTHING;
|
|
491
|
+
|
|
492
|
+
CREATE INDEX IF NOT EXISTS "content_entries_type_locale_status_idx"
|
|
493
|
+
ON "content_entries" USING btree ("content_type_id", "locale", "status");
|
|
494
|
+
CREATE INDEX IF NOT EXISTS "content_entries_translation_group_idx"
|
|
495
|
+
ON "content_entries" USING btree ("translation_group");
|
|
496
|
+
CREATE INDEX IF NOT EXISTS "content_entries_public_id_idx"
|
|
497
|
+
ON "content_entries" USING btree ("public_id");
|
|
498
|
+
CREATE UNIQUE INDEX IF NOT EXISTS "content_entries_locale_resolved_path_unique"
|
|
499
|
+
ON "content_entries" USING btree ("locale", "resolved_path")
|
|
500
|
+
WHERE "resolved_path" IS NOT NULL;
|
|
501
|
+
CREATE INDEX IF NOT EXISTS "entry_revisions_entry_kind_created_idx"
|
|
502
|
+
ON "entry_revisions" USING btree ("entry_id", "kind", "created_at");
|
|
503
|
+
CREATE INDEX IF NOT EXISTS "content_entry_url_history_locale_path_idx"
|
|
504
|
+
ON "content_entry_url_history" USING btree ("locale", "old_path");
|
|
505
|
+
CREATE INDEX IF NOT EXISTS "content_entry_references_target_idx"
|
|
506
|
+
ON "content_entry_references" USING btree ("target_translation_group");
|
|
507
|
+
CREATE INDEX IF NOT EXISTS "content_field_key_history_field_idx"
|
|
508
|
+
ON "content_field_key_history" USING btree ("field_id", "id");
|
|
509
|
+
`
|
|
510
|
+
},
|
|
511
|
+
{
|
|
512
|
+
name: "0003_page_block_engine",
|
|
513
|
+
transactional: true,
|
|
514
|
+
sql: `
|
|
515
|
+
CREATE TABLE IF NOT EXISTS "pages" (
|
|
516
|
+
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
517
|
+
"translation_group" uuid NOT NULL,
|
|
518
|
+
"locale" text NOT NULL,
|
|
519
|
+
"parent_page_id" uuid REFERENCES "pages" ("id") ON DELETE RESTRICT,
|
|
520
|
+
"slug" text NOT NULL,
|
|
521
|
+
"slug_source" text NOT NULL DEFAULT 'generated',
|
|
522
|
+
"path" text NOT NULL,
|
|
523
|
+
"resolved_path" text,
|
|
524
|
+
"title" text NOT NULL,
|
|
525
|
+
"status" text NOT NULL DEFAULT 'draft',
|
|
526
|
+
"seo" jsonb,
|
|
527
|
+
"version" integer NOT NULL DEFAULT 1,
|
|
528
|
+
"live_publication_id" uuid,
|
|
529
|
+
"locked_by" text,
|
|
530
|
+
"locked_at" timestamp with time zone,
|
|
531
|
+
"created_by" text,
|
|
532
|
+
"updated_by" text,
|
|
533
|
+
"created_at" timestamp with time zone NOT NULL,
|
|
534
|
+
"updated_at" timestamp with time zone NOT NULL,
|
|
535
|
+
"published_at" timestamp with time zone,
|
|
536
|
+
"first_published_at" timestamp with time zone,
|
|
537
|
+
"scheduled_at" timestamp with time zone,
|
|
538
|
+
"trashed_at" timestamp with time zone,
|
|
539
|
+
CONSTRAINT "pages_locked_by_user_id_fk" FOREIGN KEY ("locked_by")
|
|
540
|
+
REFERENCES "user" ("id") ON DELETE SET NULL,
|
|
541
|
+
CONSTRAINT "pages_created_by_user_id_fk" FOREIGN KEY ("created_by")
|
|
542
|
+
REFERENCES "user" ("id") ON DELETE SET NULL,
|
|
543
|
+
CONSTRAINT "pages_updated_by_user_id_fk" FOREIGN KEY ("updated_by")
|
|
544
|
+
REFERENCES "user" ("id") ON DELETE SET NULL,
|
|
545
|
+
CONSTRAINT "pages_locale_path_unique" UNIQUE ("locale", "path"),
|
|
546
|
+
CONSTRAINT "pages_group_locale_unique" UNIQUE ("translation_group", "locale"),
|
|
547
|
+
CONSTRAINT "pages_status_check"
|
|
548
|
+
CHECK ("status" IN ('draft', 'published', 'scheduled', 'trashed')),
|
|
549
|
+
CONSTRAINT "pages_slug_source_check"
|
|
550
|
+
CHECK ("slug_source" IN ('generated', 'manual')),
|
|
551
|
+
CONSTRAINT "pages_scheduled_at_check"
|
|
552
|
+
CHECK ("status" <> 'scheduled' OR "scheduled_at" IS NOT NULL),
|
|
553
|
+
CONSTRAINT "pages_resolved_path_check"
|
|
554
|
+
CHECK ("resolved_path" IS NULL OR "status" = 'published'),
|
|
555
|
+
CONSTRAINT "pages_lock_pair_check"
|
|
556
|
+
CHECK (("locked_by" IS NULL) = ("locked_at" IS NULL)),
|
|
557
|
+
CONSTRAINT "pages_path_check"
|
|
558
|
+
CHECK ("path" <> '' AND "path" NOT LIKE '/%' AND "path" NOT LIKE '%/')
|
|
559
|
+
);
|
|
560
|
+
|
|
561
|
+
CREATE TABLE IF NOT EXISTS "page_blocks" (
|
|
562
|
+
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
563
|
+
"owner_type" text NOT NULL,
|
|
564
|
+
"owner_id" uuid NOT NULL,
|
|
565
|
+
"locale" text NOT NULL,
|
|
566
|
+
"parent_block_id" uuid REFERENCES "page_blocks" ("id") ON DELETE CASCADE,
|
|
567
|
+
"block_type" text NOT NULL,
|
|
568
|
+
"props" jsonb NOT NULL DEFAULT '{}'::jsonb,
|
|
569
|
+
"schema_version" integer NOT NULL,
|
|
570
|
+
"depth" integer NOT NULL,
|
|
571
|
+
"sort_order" integer NOT NULL,
|
|
572
|
+
"version" integer NOT NULL DEFAULT 1,
|
|
573
|
+
"created_by" text,
|
|
574
|
+
"updated_by" text,
|
|
575
|
+
"created_at" timestamp with time zone NOT NULL,
|
|
576
|
+
"updated_at" timestamp with time zone NOT NULL,
|
|
577
|
+
CONSTRAINT "page_blocks_created_by_user_id_fk" FOREIGN KEY ("created_by")
|
|
578
|
+
REFERENCES "user" ("id") ON DELETE SET NULL,
|
|
579
|
+
CONSTRAINT "page_blocks_updated_by_user_id_fk" FOREIGN KEY ("updated_by")
|
|
580
|
+
REFERENCES "user" ("id") ON DELETE SET NULL,
|
|
581
|
+
CONSTRAINT "page_blocks_owner_type_check" CHECK ("owner_type" IN ('page')),
|
|
582
|
+
CONSTRAINT "page_blocks_props_object_check"
|
|
583
|
+
CHECK (jsonb_typeof("props") = 'object'),
|
|
584
|
+
CONSTRAINT "page_blocks_schema_version_check" CHECK ("schema_version" >= 1),
|
|
585
|
+
CONSTRAINT "page_blocks_depth_check" CHECK ("depth" >= 0)
|
|
586
|
+
);
|
|
587
|
+
|
|
588
|
+
CREATE TABLE IF NOT EXISTS "block_revisions" (
|
|
589
|
+
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
590
|
+
"block_id" uuid REFERENCES "page_blocks" ("id") ON DELETE SET NULL,
|
|
591
|
+
"owner_type" text NOT NULL,
|
|
592
|
+
"owner_id" uuid NOT NULL,
|
|
593
|
+
"locale" text NOT NULL,
|
|
594
|
+
"revision_batch_id" uuid NOT NULL,
|
|
595
|
+
"change_type" text NOT NULL,
|
|
596
|
+
"kind" text NOT NULL,
|
|
597
|
+
"block_type" text NOT NULL,
|
|
598
|
+
"parent_block_id" uuid,
|
|
599
|
+
"sort_order" integer NOT NULL,
|
|
600
|
+
"depth" integer NOT NULL,
|
|
601
|
+
"props" jsonb NOT NULL,
|
|
602
|
+
"schema_version" integer NOT NULL,
|
|
603
|
+
"author_id" text,
|
|
604
|
+
"created_at" timestamp with time zone NOT NULL,
|
|
605
|
+
CONSTRAINT "block_revisions_author_id_user_id_fk" FOREIGN KEY ("author_id")
|
|
606
|
+
REFERENCES "user" ("id") ON DELETE SET NULL,
|
|
607
|
+
CONSTRAINT "block_revisions_change_type_check"
|
|
608
|
+
CHECK ("change_type" IN ('create', 'update', 'move', 'delete')),
|
|
609
|
+
CONSTRAINT "block_revisions_kind_check" CHECK ("kind" IN ('save', 'publish')),
|
|
610
|
+
CONSTRAINT "block_revisions_props_object_check"
|
|
611
|
+
CHECK (jsonb_typeof("props") = 'object')
|
|
612
|
+
);
|
|
613
|
+
|
|
614
|
+
CREATE TABLE IF NOT EXISTS "page_publications" (
|
|
615
|
+
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
616
|
+
"page_id" uuid NOT NULL,
|
|
617
|
+
"locale" text NOT NULL,
|
|
618
|
+
"is_draft" boolean NOT NULL DEFAULT false,
|
|
619
|
+
"snapshot" jsonb NOT NULL,
|
|
620
|
+
"revision_manifest" jsonb NOT NULL,
|
|
621
|
+
"manifest_hash" text NOT NULL,
|
|
622
|
+
"published_by" text,
|
|
623
|
+
"published_at" timestamp with time zone NOT NULL,
|
|
624
|
+
CONSTRAINT "page_publications_page_id_fk" FOREIGN KEY ("page_id")
|
|
625
|
+
REFERENCES "pages" ("id") ON DELETE CASCADE,
|
|
626
|
+
CONSTRAINT "page_publications_published_by_user_id_fk" FOREIGN KEY ("published_by")
|
|
627
|
+
REFERENCES "user" ("id") ON DELETE SET NULL,
|
|
628
|
+
CONSTRAINT "page_publications_snapshot_object_check"
|
|
629
|
+
CHECK (jsonb_typeof("snapshot") = 'object'),
|
|
630
|
+
CONSTRAINT "page_publications_manifest_object_check"
|
|
631
|
+
CHECK (jsonb_typeof("revision_manifest") = 'object')
|
|
632
|
+
);
|
|
633
|
+
|
|
634
|
+
DO $$
|
|
635
|
+
BEGIN
|
|
636
|
+
IF NOT EXISTS (
|
|
637
|
+
SELECT 1 FROM pg_constraint WHERE conname = 'pages_live_publication_id_fk'
|
|
638
|
+
) THEN
|
|
639
|
+
ALTER TABLE "pages"
|
|
640
|
+
ADD CONSTRAINT "pages_live_publication_id_fk"
|
|
641
|
+
FOREIGN KEY ("live_publication_id") REFERENCES "page_publications" ("id") ON DELETE SET NULL;
|
|
642
|
+
END IF;
|
|
643
|
+
END $$;
|
|
644
|
+
|
|
645
|
+
CREATE TABLE IF NOT EXISTS "page_url_history" (
|
|
646
|
+
"id" bigserial PRIMARY KEY,
|
|
647
|
+
"page_id" uuid,
|
|
648
|
+
"translation_group" uuid NOT NULL,
|
|
649
|
+
"locale" text NOT NULL,
|
|
650
|
+
"old_path" text NOT NULL,
|
|
651
|
+
"reason" text NOT NULL,
|
|
652
|
+
"changed_at" timestamp with time zone NOT NULL,
|
|
653
|
+
CONSTRAINT "page_url_history_page_id_fk" FOREIGN KEY ("page_id")
|
|
654
|
+
REFERENCES "pages" ("id") ON DELETE SET NULL,
|
|
655
|
+
CONSTRAINT "page_url_history_reason_check" CHECK (
|
|
656
|
+
"reason" IN ('slug_changed', 'moved', 'pattern_changed', 'unpublished', 'trashed', 'deleted')
|
|
657
|
+
)
|
|
658
|
+
);
|
|
659
|
+
|
|
660
|
+
CREATE TABLE IF NOT EXISTS "page_engine_settings" (
|
|
661
|
+
"id" smallint PRIMARY KEY DEFAULT 1,
|
|
662
|
+
"page_edit_locking" boolean NOT NULL DEFAULT true,
|
|
663
|
+
"url_pattern" text NOT NULL DEFAULT '{locale}/{path}',
|
|
664
|
+
"updated_at" timestamp with time zone NOT NULL DEFAULT now(),
|
|
665
|
+
CONSTRAINT "page_engine_settings_id_check" CHECK ("id" = 1)
|
|
666
|
+
);
|
|
667
|
+
|
|
668
|
+
INSERT INTO "page_engine_settings" ("id") VALUES (1) ON CONFLICT ("id") DO NOTHING;
|
|
669
|
+
|
|
670
|
+
CREATE INDEX IF NOT EXISTS "pages_parent_idx" ON "pages" USING btree ("parent_page_id");
|
|
671
|
+
CREATE INDEX IF NOT EXISTS "pages_locale_status_idx" ON "pages" USING btree ("locale", "status");
|
|
672
|
+
CREATE INDEX IF NOT EXISTS "pages_translation_group_idx" ON "pages" USING btree ("translation_group");
|
|
673
|
+
CREATE INDEX IF NOT EXISTS "pages_locale_path_prefix_idx"
|
|
674
|
+
ON "pages" USING btree ("locale", "path" text_pattern_ops);
|
|
675
|
+
CREATE UNIQUE INDEX IF NOT EXISTS "pages_locale_resolved_path_unique"
|
|
676
|
+
ON "pages" USING btree ("locale", "resolved_path")
|
|
677
|
+
WHERE "resolved_path" IS NOT NULL;
|
|
678
|
+
CREATE INDEX IF NOT EXISTS "page_blocks_owner_idx"
|
|
679
|
+
ON "page_blocks" USING btree ("owner_type", "owner_id", "locale");
|
|
680
|
+
CREATE INDEX IF NOT EXISTS "page_blocks_parent_sort_idx"
|
|
681
|
+
ON "page_blocks" USING btree ("parent_block_id", "sort_order");
|
|
682
|
+
CREATE INDEX IF NOT EXISTS "page_blocks_type_version_idx"
|
|
683
|
+
ON "page_blocks" USING btree ("block_type", "schema_version");
|
|
684
|
+
CREATE INDEX IF NOT EXISTS "block_revisions_owner_created_idx"
|
|
685
|
+
ON "block_revisions" USING btree ("owner_type", "owner_id", "locale", "created_at" DESC);
|
|
686
|
+
CREATE INDEX IF NOT EXISTS "block_revisions_batch_idx"
|
|
687
|
+
ON "block_revisions" USING btree ("revision_batch_id");
|
|
688
|
+
CREATE INDEX IF NOT EXISTS "block_revisions_block_idx"
|
|
689
|
+
ON "block_revisions" USING btree ("block_id", "created_at" DESC);
|
|
690
|
+
CREATE INDEX IF NOT EXISTS "page_publications_page_draft_published_idx"
|
|
691
|
+
ON "page_publications" USING btree ("page_id", "is_draft", "published_at" DESC);
|
|
692
|
+
CREATE INDEX IF NOT EXISTS "page_url_history_locale_path_idx"
|
|
693
|
+
ON "page_url_history" USING btree ("locale", "old_path");
|
|
694
|
+
`
|
|
695
|
+
}
|
|
696
|
+
]);
|
|
259
697
|
//#endregion
|
|
260
698
|
//#region src/migrate.ts
|
|
261
699
|
/**
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@plakboek/db",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "Postgres connection factory and migration-lock primitives for Plakboek CMS installations",
|
|
5
5
|
"homepage": "https://github.com/flovan/plakboek/tree/main/packages/db#readme",
|
|
6
6
|
"bugs": {
|