@hollware/server 0.3.0 → 0.5.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/{ceremonies-D7tsWU3S.mjs → ceremonies-BM9G03lZ.mjs} +9 -16
- package/dist/index.d.mts +151 -2
- package/dist/index.mjs +1142 -193
- package/dist/{store-Sq0pwUQv.mjs → store-DiS8UFI2.mjs} +85 -54
- package/migrations/0006_people_across_sites.sql +54 -0
- package/migrations/0007_read_tokens.sql +4 -0
- package/migrations/0008_search.sql +47 -0
- package/migrations/0009_embeddings.sql +19 -0
- package/migrations/meta/0006_snapshot.json +2102 -0
- package/migrations/meta/0007_snapshot.json +2110 -0
- package/migrations/meta/0008_snapshot.json +2110 -0
- package/migrations/meta/0009_snapshot.json +2257 -0
- package/migrations/meta/_journal.json +28 -0
- package/package.json +2 -2
|
@@ -135,6 +135,11 @@ const tokens = sqliteTable("tokens", {
|
|
|
135
135
|
userId: text("user_id").notNull(),
|
|
136
136
|
label: text("label").notNull(),
|
|
137
137
|
hash: text("hash").notNull(),
|
|
138
|
+
/**
|
|
139
|
+
* `personal` (`hw_pat_`) or `read` (`hw_read_`), which reads the published content of the
|
|
140
|
+
* site over the read API and nothing else. Its CHECK is the column's own (migration 0007).
|
|
141
|
+
*/
|
|
142
|
+
kind: text("kind", { enum: ["personal", "read"] }).notNull().default("personal"),
|
|
138
143
|
/** A JSON array of scopes. */
|
|
139
144
|
scopes: text("scopes").notNull(),
|
|
140
145
|
expiresAt: text("expires_at"),
|
|
@@ -413,10 +418,12 @@ const deliveries = sqliteTable("deliveries", {
|
|
|
413
418
|
}),
|
|
414
419
|
check("deliveries_attempts", sql`${t.attempts} >= 0`)
|
|
415
420
|
]);
|
|
416
|
-
/**
|
|
421
|
+
/**
|
|
422
|
+
* The passkeys of a person (ADR-026): WebAuthn credentials, by their id, with their public key. A
|
|
423
|
+
* person is one across the sites of a deployment (v0.4), and so are their passkeys and sessions.
|
|
424
|
+
*/
|
|
417
425
|
const passkeys = sqliteTable("passkeys", {
|
|
418
426
|
id: id("pky"),
|
|
419
|
-
siteId: siteId(),
|
|
420
427
|
userId: text("user_id").notNull(),
|
|
421
428
|
/** The credential id, base64url, as the authenticator gives it. */
|
|
422
429
|
credentialId: text("credential_id").notNull(),
|
|
@@ -430,14 +437,8 @@ const passkeys = sqliteTable("passkeys", {
|
|
|
430
437
|
lastUsedAt: text("last_used_at"),
|
|
431
438
|
...timestamps()
|
|
432
439
|
}, (t) => [
|
|
433
|
-
unique("passkeys_id_site").on(t.id, t.siteId),
|
|
434
440
|
unique("passkeys_credential").on(t.credentialId),
|
|
435
441
|
index("passkeys_user").on(t.userId),
|
|
436
|
-
foreignKey({
|
|
437
|
-
name: "passkeys_site",
|
|
438
|
-
columns: [t.siteId],
|
|
439
|
-
foreignColumns: [sites.id]
|
|
440
|
-
}),
|
|
441
442
|
foreignKey({
|
|
442
443
|
name: "passkeys_user_fk",
|
|
443
444
|
columns: [t.userId],
|
|
@@ -479,7 +480,6 @@ const invitations = sqliteTable("invitations", {
|
|
|
479
480
|
/** WebAuthn challenges, each used once and briefly (ADR-026). */
|
|
480
481
|
const authChallenges = sqliteTable("auth_challenges", {
|
|
481
482
|
id: id("chl"),
|
|
482
|
-
siteId: siteId(),
|
|
483
483
|
challenge: text("challenge").notNull(),
|
|
484
484
|
/** `register` or `authenticate`. */
|
|
485
485
|
purpose: text("purpose").notNull(),
|
|
@@ -489,37 +489,19 @@ const authChallenges = sqliteTable("auth_challenges", {
|
|
|
489
489
|
userId: text("user_id"),
|
|
490
490
|
expiresAt: text("expires_at").notNull(),
|
|
491
491
|
...timestamps()
|
|
492
|
-
}, (t) => [
|
|
493
|
-
unique("auth_challenges_id_site").on(t.id, t.siteId),
|
|
494
|
-
foreignKey({
|
|
495
|
-
name: "auth_challenges_site",
|
|
496
|
-
columns: [t.siteId],
|
|
497
|
-
foreignColumns: [sites.id]
|
|
498
|
-
}),
|
|
499
|
-
check("auth_challenges_purpose", sql`${t.purpose} IN ('register', 'authenticate')`)
|
|
500
|
-
]);
|
|
492
|
+
}, (t) => [check("auth_challenges_purpose", sql`${t.purpose} IN ('register', 'authenticate')`)]);
|
|
501
493
|
/** Signed-in people (ADR-026): the session cookie that `/authorize` reads, by its hash. */
|
|
502
494
|
const sessions = sqliteTable("sessions", {
|
|
503
495
|
id: id("ses"),
|
|
504
|
-
siteId: siteId(),
|
|
505
496
|
userId: text("user_id").notNull(),
|
|
506
497
|
hash: text("hash").notNull(),
|
|
507
498
|
expiresAt: text("expires_at").notNull(),
|
|
508
499
|
...timestamps()
|
|
509
|
-
}, (t) => [
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
columns: [t.siteId],
|
|
515
|
-
foreignColumns: [sites.id]
|
|
516
|
-
}),
|
|
517
|
-
foreignKey({
|
|
518
|
-
name: "sessions_user",
|
|
519
|
-
columns: [t.userId],
|
|
520
|
-
foreignColumns: [users.id]
|
|
521
|
-
})
|
|
522
|
-
]);
|
|
500
|
+
}, (t) => [unique("sessions_hash").on(t.hash), foreignKey({
|
|
501
|
+
name: "sessions_user",
|
|
502
|
+
columns: [t.userId],
|
|
503
|
+
foreignColumns: [users.id]
|
|
504
|
+
})]);
|
|
523
505
|
/**
|
|
524
506
|
* Reviews (v0.3): a person who may write but not publish asks for a revision to be published, and
|
|
525
507
|
* a person who may publish approves it, which publishes it, or asks for changes with a note. An
|
|
@@ -576,6 +558,47 @@ const reviews = sqliteTable("reviews", {
|
|
|
576
558
|
check("reviews_decided", sql`(${t.status} IN ('pending', 'cancelled')) OR (${t.decidedBy} IS NOT NULL AND ${t.decidedAt} IS NOT NULL)`),
|
|
577
559
|
check("reviews_json", sql`json_valid(${t.acknowledged}) AND json_type(${t.acknowledged}) = 'array' AND (${t.requestedMeta} IS NULL OR json_valid(${t.requestedMeta}))`)
|
|
578
560
|
]);
|
|
561
|
+
/**
|
|
562
|
+
* The vectors of an entry, as derived data (v0.5): the published revision they were made from,
|
|
563
|
+
* with which model and which version of the chunker, and how many pieces there are. The vectors
|
|
564
|
+
* live in Vectorize under `<entry id>:<piece>`; this row says which exist and whether they are
|
|
565
|
+
* current. One row per entry: a new publication replaces it.
|
|
566
|
+
*/
|
|
567
|
+
const entryEmbeddings = sqliteTable("entry_embeddings", {
|
|
568
|
+
id: id("emb"),
|
|
569
|
+
siteId: siteId(),
|
|
570
|
+
entryId: text("entry_id").notNull(),
|
|
571
|
+
revisionId: text("revision_id").notNull(),
|
|
572
|
+
model: text("model").notNull(),
|
|
573
|
+
chunkerVersion: text("chunker_version").notNull(),
|
|
574
|
+
/** How many pieces, so how many vectors: `<entry id>:0` to `<entry id>:<pieces - 1>`. */
|
|
575
|
+
pieces: integer("pieces").notNull(),
|
|
576
|
+
/**
|
|
577
|
+
* The entries closest to this one by their summaries, in its collection and locale, closest
|
|
578
|
+
* first, as JSON: `[{ "id": "ent_…", "score": 0.91 }]`. Found when its vectors were made.
|
|
579
|
+
*/
|
|
580
|
+
related: text("related").notNull().default("[]"),
|
|
581
|
+
...timestamps()
|
|
582
|
+
}, (t) => [
|
|
583
|
+
unique("entry_embeddings_id_site").on(t.id, t.siteId),
|
|
584
|
+
unique("entry_embeddings_entry").on(t.siteId, t.entryId),
|
|
585
|
+
foreignKey({
|
|
586
|
+
name: "entry_embeddings_site",
|
|
587
|
+
columns: [t.siteId],
|
|
588
|
+
foreignColumns: [sites.id]
|
|
589
|
+
}),
|
|
590
|
+
foreignKey({
|
|
591
|
+
name: "entry_embeddings_entry_fk",
|
|
592
|
+
columns: [t.entryId, t.siteId],
|
|
593
|
+
foreignColumns: [entries.id, entries.siteId]
|
|
594
|
+
}).onDelete("cascade"),
|
|
595
|
+
foreignKey({
|
|
596
|
+
name: "entry_embeddings_revision_fk",
|
|
597
|
+
columns: [t.revisionId, t.siteId],
|
|
598
|
+
foreignColumns: [revisions.id, revisions.siteId]
|
|
599
|
+
}),
|
|
600
|
+
check("entry_embeddings_pieces", sql`${t.pieces} >= 1`)
|
|
601
|
+
]);
|
|
579
602
|
//#endregion
|
|
580
603
|
//#region src/identity/secrets.ts
|
|
581
604
|
/**
|
|
@@ -607,7 +630,7 @@ function userHandle(userId) {
|
|
|
607
630
|
//#region src/identity/store.ts
|
|
608
631
|
function roleOf(value) {
|
|
609
632
|
if (value === "owner" || value === "editor" || value === "author") return value;
|
|
610
|
-
throw new Error(`
|
|
633
|
+
throw new Error(`A role of ${value} is no role the schema allows`);
|
|
611
634
|
}
|
|
612
635
|
/** Creates a link that lets one person in, and gives its secret, which is never stored. */
|
|
613
636
|
async function createInvitation(db, input) {
|
|
@@ -633,24 +656,25 @@ async function createInvitation(db, input) {
|
|
|
633
656
|
};
|
|
634
657
|
}
|
|
635
658
|
/** The invitation of a secret, while it is unused and has not expired. */
|
|
636
|
-
async function findInvitation(db,
|
|
659
|
+
async function findInvitation(db, secret, now) {
|
|
637
660
|
const [row] = await db.select({
|
|
638
661
|
id: invitations.id,
|
|
662
|
+
site: invitations.siteId,
|
|
639
663
|
role: invitations.role,
|
|
640
664
|
userId: invitations.userId
|
|
641
|
-
}).from(invitations).where(and(eq(invitations.
|
|
665
|
+
}).from(invitations).where(and(eq(invitations.hash, await hashSecret(secret)), isNull(invitations.usedAt), gt(invitations.expiresAt, now.toISOString()))).limit(1);
|
|
642
666
|
return row === void 0 ? null : {
|
|
643
667
|
...row,
|
|
644
668
|
role: roleOf(row.role)
|
|
645
669
|
};
|
|
646
670
|
}
|
|
647
671
|
/** Uses an invitation, once: `false` when it was used already or has expired. */
|
|
648
|
-
async function redeemInvitation(db,
|
|
672
|
+
async function redeemInvitation(db, id, now) {
|
|
649
673
|
const at = now.toISOString();
|
|
650
674
|
return (await db.update(invitations).set({
|
|
651
675
|
usedAt: at,
|
|
652
676
|
updatedAt: at
|
|
653
|
-
}).where(and(eq(invitations.
|
|
677
|
+
}).where(and(eq(invitations.id, id), isNull(invitations.usedAt), gt(invitations.expiresAt, at))).returning({ id: invitations.id })).length === 1;
|
|
654
678
|
}
|
|
655
679
|
/** Keeps a WebAuthn challenge for five minutes. */
|
|
656
680
|
async function saveChallenge(db, input) {
|
|
@@ -658,7 +682,6 @@ async function saveChallenge(db, input) {
|
|
|
658
682
|
const now = input.now.toISOString();
|
|
659
683
|
await db.insert(authChallenges).values({
|
|
660
684
|
id,
|
|
661
|
-
siteId: input.site,
|
|
662
685
|
challenge: input.challenge,
|
|
663
686
|
purpose: input.purpose,
|
|
664
687
|
invitationId: input.invitationId,
|
|
@@ -671,16 +694,16 @@ async function saveChallenge(db, input) {
|
|
|
671
694
|
}
|
|
672
695
|
/** Takes a challenge, once, if it has the purpose asked and has not expired. */
|
|
673
696
|
async function takeChallenge(db, input) {
|
|
674
|
-
const [row] = await db.delete(authChallenges).where(
|
|
697
|
+
const [row] = await db.delete(authChallenges).where(eq(authChallenges.id, input.id)).returning();
|
|
675
698
|
if (row === void 0 || row.purpose !== input.purpose) return null;
|
|
676
699
|
if (row.expiresAt <= input.now.toISOString()) return null;
|
|
677
700
|
return row;
|
|
678
701
|
}
|
|
679
|
-
async function passkeysOf(db,
|
|
680
|
-
return db.select().from(passkeys).where(
|
|
702
|
+
async function passkeysOf(db, userId) {
|
|
703
|
+
return db.select().from(passkeys).where(eq(passkeys.userId, userId));
|
|
681
704
|
}
|
|
682
|
-
async function findPasskey(db,
|
|
683
|
-
const [row] = await db.select().from(passkeys).where(
|
|
705
|
+
async function findPasskey(db, credentialId) {
|
|
706
|
+
const [row] = await db.select().from(passkeys).where(eq(passkeys.credentialId, credentialId)).limit(1);
|
|
684
707
|
return row ?? null;
|
|
685
708
|
}
|
|
686
709
|
/** Records a sign-in with a passkey: its new counter, and when. */
|
|
@@ -714,7 +737,6 @@ async function addPasskey(db, input) {
|
|
|
714
737
|
const at = input.now.toISOString();
|
|
715
738
|
await db.insert(passkeys).values({
|
|
716
739
|
id: newId("pky"),
|
|
717
|
-
siteId: input.site,
|
|
718
740
|
userId: input.userId,
|
|
719
741
|
credentialId: input.credentialId,
|
|
720
742
|
publicKey: input.publicKey,
|
|
@@ -733,7 +755,6 @@ async function createSession(db, input) {
|
|
|
733
755
|
const expiresAt = new Date(input.now.getTime() + input.hours * 36e5).toISOString();
|
|
734
756
|
await db.insert(sessions).values({
|
|
735
757
|
id: newId("ses"),
|
|
736
|
-
siteId: input.site,
|
|
737
758
|
userId: input.userId,
|
|
738
759
|
hash: await hashSecret(secret),
|
|
739
760
|
expiresAt,
|
|
@@ -745,26 +766,36 @@ async function createSession(db, input) {
|
|
|
745
766
|
expiresAt
|
|
746
767
|
};
|
|
747
768
|
}
|
|
748
|
-
/** The person of a session cookie, while it lasts
|
|
749
|
-
async function findSession(db,
|
|
769
|
+
/** The person of a session cookie, while it lasts. */
|
|
770
|
+
async function findSession(db, secret, now) {
|
|
750
771
|
const [row] = await db.select({
|
|
751
772
|
userId: sessions.userId,
|
|
752
|
-
name: users.name
|
|
753
|
-
|
|
754
|
-
}).from(sessions).innerJoin(users, eq(users.id, sessions.userId)).innerJoin(members, and(eq(members.userId, sessions.userId), eq(members.siteId, sessions.siteId))).where(and(eq(sessions.siteId, site), eq(sessions.hash, await hashSecret(secret)), gt(sessions.expiresAt, now.toISOString()))).limit(1);
|
|
773
|
+
name: users.name
|
|
774
|
+
}).from(sessions).innerJoin(users, eq(users.id, sessions.userId)).where(and(eq(sessions.hash, await hashSecret(secret)), gt(sessions.expiresAt, now.toISOString()))).limit(1);
|
|
755
775
|
return row ?? null;
|
|
756
776
|
}
|
|
777
|
+
/** The sites a person is a member of, with their role in each, in the order they joined. */
|
|
778
|
+
async function membershipsOf(db, userId) {
|
|
779
|
+
return (await db.select({
|
|
780
|
+
site: members.siteId,
|
|
781
|
+
role: members.role
|
|
782
|
+
}).from(members).where(eq(members.userId, userId)).orderBy(members.createdAt, members.siteId)).map((row) => ({
|
|
783
|
+
site: row.site,
|
|
784
|
+
role: roleOf(row.role)
|
|
785
|
+
}));
|
|
786
|
+
}
|
|
757
787
|
/** An invitation by its id, used or not: what a registration redeems. */
|
|
758
|
-
async function invitationById(db,
|
|
788
|
+
async function invitationById(db, id) {
|
|
759
789
|
const [row] = await db.select({
|
|
760
790
|
id: invitations.id,
|
|
791
|
+
site: invitations.siteId,
|
|
761
792
|
role: invitations.role,
|
|
762
793
|
userId: invitations.userId
|
|
763
|
-
}).from(invitations).where(
|
|
794
|
+
}).from(invitations).where(eq(invitations.id, id)).limit(1);
|
|
764
795
|
return row === void 0 ? null : {
|
|
765
796
|
...row,
|
|
766
797
|
role: roleOf(row.role)
|
|
767
798
|
};
|
|
768
799
|
}
|
|
769
800
|
//#endregion
|
|
770
|
-
export {
|
|
801
|
+
export { ROLE_SCOPES as A, reviews as C, sites as D, schedules as E, isRole as M, tokens as O, publications as S, revisions as T, entries as _, findInvitation as a, members as b, invitationById as c, redeemInvitation as d, saveChallenge as f, deliveries as g, userHandle as h, createUser as i, effectiveScopes as j, users as k, membershipsOf as l, touchPasskey as m, createInvitation as n, findPasskey as o, takeChallenge as p, createSession as r, findSession as s, addPasskey as t, passkeysOf as u, entryEmbeddings as v, revisionTags as w, outbox as x, invitations as y };
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
-- Edited by hand (ADR-023): a person is one across the sites of a deployment (v0.4), so passkeys,
|
|
2
|
+
-- sessions and sign-in challenges lose site_id. Each table is rebuilt, which D1 allows here since
|
|
3
|
+
-- no other table points at them; the PRAGMA foreign_keys lines drizzle-kit writes are gone, as D1
|
|
4
|
+
-- ignores them, and the CHECKs name their columns without the temporary table's name.
|
|
5
|
+
CREATE TABLE `__new_auth_challenges` (
|
|
6
|
+
`id` text PRIMARY KEY NOT NULL,
|
|
7
|
+
`challenge` text NOT NULL,
|
|
8
|
+
`purpose` text NOT NULL,
|
|
9
|
+
`invitation_id` text,
|
|
10
|
+
`user_id` text,
|
|
11
|
+
`expires_at` text NOT NULL,
|
|
12
|
+
`created_at` text NOT NULL,
|
|
13
|
+
`updated_at` text NOT NULL,
|
|
14
|
+
CONSTRAINT "auth_challenges_purpose" CHECK("purpose" IN ('register', 'authenticate'))
|
|
15
|
+
);
|
|
16
|
+
--> statement-breakpoint
|
|
17
|
+
INSERT INTO `__new_auth_challenges`("id", "challenge", "purpose", "invitation_id", "user_id", "expires_at", "created_at", "updated_at") SELECT "id", "challenge", "purpose", "invitation_id", "user_id", "expires_at", "created_at", "updated_at" FROM `auth_challenges`;--> statement-breakpoint
|
|
18
|
+
DROP TABLE `auth_challenges`;--> statement-breakpoint
|
|
19
|
+
ALTER TABLE `__new_auth_challenges` RENAME TO `auth_challenges`;--> statement-breakpoint
|
|
20
|
+
CREATE TABLE `__new_passkeys` (
|
|
21
|
+
`id` text PRIMARY KEY NOT NULL,
|
|
22
|
+
`user_id` text NOT NULL,
|
|
23
|
+
`credential_id` text NOT NULL,
|
|
24
|
+
`public_key` text NOT NULL,
|
|
25
|
+
`counter` integer NOT NULL,
|
|
26
|
+
`transports` text NOT NULL,
|
|
27
|
+
`label` text NOT NULL,
|
|
28
|
+
`last_used_at` text,
|
|
29
|
+
`created_at` text NOT NULL,
|
|
30
|
+
`updated_at` text NOT NULL,
|
|
31
|
+
FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON UPDATE no action ON DELETE no action,
|
|
32
|
+
CONSTRAINT "passkeys_counter" CHECK("counter" >= 0),
|
|
33
|
+
CONSTRAINT "passkeys_json" CHECK(json_valid("transports") AND json_type("transports") = 'array')
|
|
34
|
+
);
|
|
35
|
+
--> statement-breakpoint
|
|
36
|
+
INSERT INTO `__new_passkeys`("id", "user_id", "credential_id", "public_key", "counter", "transports", "label", "last_used_at", "created_at", "updated_at") SELECT "id", "user_id", "credential_id", "public_key", "counter", "transports", "label", "last_used_at", "created_at", "updated_at" FROM `passkeys`;--> statement-breakpoint
|
|
37
|
+
DROP TABLE `passkeys`;--> statement-breakpoint
|
|
38
|
+
ALTER TABLE `__new_passkeys` RENAME TO `passkeys`;--> statement-breakpoint
|
|
39
|
+
CREATE INDEX `passkeys_user` ON `passkeys` (`user_id`);--> statement-breakpoint
|
|
40
|
+
CREATE UNIQUE INDEX `passkeys_credential` ON `passkeys` (`credential_id`);--> statement-breakpoint
|
|
41
|
+
CREATE TABLE `__new_sessions` (
|
|
42
|
+
`id` text PRIMARY KEY NOT NULL,
|
|
43
|
+
`user_id` text NOT NULL,
|
|
44
|
+
`hash` text NOT NULL,
|
|
45
|
+
`expires_at` text NOT NULL,
|
|
46
|
+
`created_at` text NOT NULL,
|
|
47
|
+
`updated_at` text NOT NULL,
|
|
48
|
+
FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON UPDATE no action ON DELETE no action
|
|
49
|
+
);
|
|
50
|
+
--> statement-breakpoint
|
|
51
|
+
INSERT INTO `__new_sessions`("id", "user_id", "hash", "expires_at", "created_at", "updated_at") SELECT "id", "user_id", "hash", "expires_at", "created_at", "updated_at" FROM `sessions`;--> statement-breakpoint
|
|
52
|
+
DROP TABLE `sessions`;--> statement-breakpoint
|
|
53
|
+
ALTER TABLE `__new_sessions` RENAME TO `sessions`;--> statement-breakpoint
|
|
54
|
+
CREATE UNIQUE INDEX `sessions_hash` ON `sessions` (`hash`);
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
-- Edited by hand (ADR-023). Read tokens (v0.4): a token is personal (hw_pat_) or a read token (hw_read_), which reads the
|
|
2
|
+
-- published content of its site over the read API and nothing else. Every token so far is
|
|
3
|
+
-- personal. The CHECK is the column's own, so the table is not rebuilt.
|
|
4
|
+
ALTER TABLE `tokens` ADD `kind` text DEFAULT 'personal' NOT NULL CHECK (`kind` IN ('personal', 'read'));
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
-- Written by hand (ADR-023), as drizzle-kit has no virtual tables. Full-text search (v0.5): an
|
|
2
|
+
-- FTS5 index of the published revision of each entry, its title, its tags and its text. The text
|
|
3
|
+
-- is taken from the stored Comark document, the strings at the text positions of its nodes, so
|
|
4
|
+
-- the Markdown's own marks are never indexed. `unicode61 remove_diacritics 2` matches
|
|
5
|
+
-- "migracion" with "migración".
|
|
6
|
+
--
|
|
7
|
+
-- The triggers keep it in the step that changes what is published, whichever write does it:
|
|
8
|
+
-- publish, a group, a schedule that comes due, unpublish. Only published content is in it.
|
|
9
|
+
CREATE VIRTUAL TABLE `entries_fts` USING fts5(
|
|
10
|
+
`title`,
|
|
11
|
+
`tags`,
|
|
12
|
+
`body`,
|
|
13
|
+
`entry_id` UNINDEXED,
|
|
14
|
+
`site_id` UNINDEXED,
|
|
15
|
+
`collection` UNINDEXED,
|
|
16
|
+
`locale` UNINDEXED,
|
|
17
|
+
tokenize = 'unicode61 remove_diacritics 2'
|
|
18
|
+
);
|
|
19
|
+
--> statement-breakpoint
|
|
20
|
+
CREATE TRIGGER `entries_fts_publication` AFTER UPDATE OF `published_rev` ON `entries`
|
|
21
|
+
BEGIN
|
|
22
|
+
DELETE FROM `entries_fts` WHERE `entry_id` = OLD.`id`;
|
|
23
|
+
INSERT INTO `entries_fts` (`title`, `tags`, `body`, `entry_id`, `site_id`, `collection`, `locale`)
|
|
24
|
+
SELECT
|
|
25
|
+
coalesce(json_extract(r.`frontmatter`, '$.title'), ''),
|
|
26
|
+
coalesce((SELECT group_concat(t.`tag`, ' ') FROM `revision_tags` t WHERE t.`revision_id` = r.`id`), ''),
|
|
27
|
+
coalesce((SELECT group_concat(n.`value`, ' ') FROM json_tree(r.`document`, '$.nodes') n
|
|
28
|
+
WHERE n.`type` = 'text' AND typeof(n.`key`) = 'integer' AND n.`key` >= 2), ''),
|
|
29
|
+
NEW.`id`, NEW.`site_id`, NEW.`collection`, NEW.`locale`
|
|
30
|
+
FROM `revisions` r
|
|
31
|
+
WHERE NEW.`published_rev` IS NOT NULL AND r.`id` = NEW.`published_rev`;
|
|
32
|
+
END;
|
|
33
|
+
--> statement-breakpoint
|
|
34
|
+
CREATE TRIGGER `entries_fts_removal` AFTER DELETE ON `entries`
|
|
35
|
+
BEGIN
|
|
36
|
+
DELETE FROM `entries_fts` WHERE `entry_id` = OLD.`id`;
|
|
37
|
+
END;
|
|
38
|
+
--> statement-breakpoint
|
|
39
|
+
-- What is published already.
|
|
40
|
+
INSERT INTO `entries_fts` (`title`, `tags`, `body`, `entry_id`, `site_id`, `collection`, `locale`)
|
|
41
|
+
SELECT
|
|
42
|
+
coalesce(json_extract(r.`frontmatter`, '$.title'), ''),
|
|
43
|
+
coalesce((SELECT group_concat(t.`tag`, ' ') FROM `revision_tags` t WHERE t.`revision_id` = r.`id`), ''),
|
|
44
|
+
coalesce((SELECT group_concat(n.`value`, ' ') FROM json_tree(r.`document`, '$.nodes') n
|
|
45
|
+
WHERE n.`type` = 'text' AND typeof(n.`key`) = 'integer' AND n.`key` >= 2), ''),
|
|
46
|
+
e.`id`, e.`site_id`, e.`collection`, e.`locale`
|
|
47
|
+
FROM `entries` e JOIN `revisions` r ON r.`id` = e.`published_rev`;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
CREATE TABLE `entry_embeddings` (
|
|
2
|
+
`id` text PRIMARY KEY NOT NULL,
|
|
3
|
+
`site_id` text NOT NULL,
|
|
4
|
+
`entry_id` text NOT NULL,
|
|
5
|
+
`revision_id` text NOT NULL,
|
|
6
|
+
`model` text NOT NULL,
|
|
7
|
+
`chunker_version` text NOT NULL,
|
|
8
|
+
`pieces` integer NOT NULL,
|
|
9
|
+
`related` text DEFAULT '[]' NOT NULL,
|
|
10
|
+
`created_at` text NOT NULL,
|
|
11
|
+
`updated_at` text NOT NULL,
|
|
12
|
+
FOREIGN KEY (`site_id`) REFERENCES `sites`(`id`) ON UPDATE no action ON DELETE no action,
|
|
13
|
+
FOREIGN KEY (`entry_id`,`site_id`) REFERENCES `entries`(`id`,`site_id`) ON UPDATE no action ON DELETE cascade,
|
|
14
|
+
FOREIGN KEY (`revision_id`,`site_id`) REFERENCES `revisions`(`id`,`site_id`) ON UPDATE no action ON DELETE no action,
|
|
15
|
+
CONSTRAINT "entry_embeddings_pieces" CHECK("entry_embeddings"."pieces" >= 1)
|
|
16
|
+
);
|
|
17
|
+
--> statement-breakpoint
|
|
18
|
+
CREATE UNIQUE INDEX `entry_embeddings_id_site` ON `entry_embeddings` (`id`,`site_id`);--> statement-breakpoint
|
|
19
|
+
CREATE UNIQUE INDEX `entry_embeddings_entry` ON `entry_embeddings` (`site_id`,`entry_id`);
|