@kernhq/module-quire 0.2.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.
Files changed (72) hide show
  1. package/LICENSE +662 -0
  2. package/README.md +71 -0
  3. package/dist/client/rank.d.ts +42 -0
  4. package/dist/client/rank.d.ts.map +1 -0
  5. package/dist/client/rank.js +97 -0
  6. package/dist/client/rank.js.map +1 -0
  7. package/dist/contract/capabilities.d.ts +35 -0
  8. package/dist/contract/capabilities.d.ts.map +1 -0
  9. package/dist/contract/capabilities.js +35 -0
  10. package/dist/contract/capabilities.js.map +1 -0
  11. package/dist/contract/events.d.ts +58 -0
  12. package/dist/contract/events.d.ts.map +1 -0
  13. package/dist/contract/events.js +18 -0
  14. package/dist/contract/events.js.map +1 -0
  15. package/dist/contract/index.d.ts +6 -0
  16. package/dist/contract/index.d.ts.map +1 -0
  17. package/dist/contract/index.js +6 -0
  18. package/dist/contract/index.js.map +1 -0
  19. package/dist/contract/models.d.ts +95 -0
  20. package/dist/contract/models.d.ts.map +1 -0
  21. package/dist/contract/models.js +82 -0
  22. package/dist/contract/models.js.map +1 -0
  23. package/dist/contract/permissions.d.ts +51 -0
  24. package/dist/contract/permissions.d.ts.map +1 -0
  25. package/dist/contract/permissions.js +59 -0
  26. package/dist/contract/permissions.js.map +1 -0
  27. package/dist/contract/router.d.ts +713 -0
  28. package/dist/contract/router.d.ts.map +1 -0
  29. package/dist/contract/router.js +109 -0
  30. package/dist/contract/router.js.map +1 -0
  31. package/dist/server/_impl.d.ts +815 -0
  32. package/dist/server/_impl.d.ts.map +1 -0
  33. package/dist/server/_impl.js +189 -0
  34. package/dist/server/_impl.js.map +1 -0
  35. package/dist/server/index.d.ts +3 -0
  36. package/dist/server/index.d.ts.map +1 -0
  37. package/dist/server/index.js +87 -0
  38. package/dist/server/index.js.map +1 -0
  39. package/dist/server/schema.d.ts +540 -0
  40. package/dist/server/schema.d.ts.map +1 -0
  41. package/dist/server/schema.js +86 -0
  42. package/dist/server/schema.js.map +1 -0
  43. package/dist/server/services/access.d.ts +78 -0
  44. package/dist/server/services/access.d.ts.map +1 -0
  45. package/dist/server/services/access.js +96 -0
  46. package/dist/server/services/access.js.map +1 -0
  47. package/dist/server/services/index.d.ts +15 -0
  48. package/dist/server/services/index.d.ts.map +1 -0
  49. package/dist/server/services/index.js +22 -0
  50. package/dist/server/services/index.js.map +1 -0
  51. package/dist/server/services/pages.d.ts +190 -0
  52. package/dist/server/services/pages.d.ts.map +1 -0
  53. package/dist/server/services/pages.js +242 -0
  54. package/dist/server/services/pages.js.map +1 -0
  55. package/dist/server/services/spaces.d.ts +95 -0
  56. package/dist/server/services/spaces.d.ts.map +1 -0
  57. package/dist/server/services/spaces.js +100 -0
  58. package/dist/server/services/spaces.js.map +1 -0
  59. package/migrations/0000_init.sql +43 -0
  60. package/migrations/0001_rls.sql +17 -0
  61. package/migrations/meta/0000_snapshot.json +358 -0
  62. package/migrations/meta/_journal.json +20 -0
  63. package/package.json +78 -0
  64. package/src/client/index.ts +67 -0
  65. package/src/client/rank.test.ts +92 -0
  66. package/src/client/rank.ts +100 -0
  67. package/src/contract/capabilities.ts +36 -0
  68. package/src/contract/events.ts +22 -0
  69. package/src/contract/index.ts +5 -0
  70. package/src/contract/models.ts +93 -0
  71. package/src/contract/permissions.ts +59 -0
  72. package/src/contract/router.ts +121 -0
@@ -0,0 +1,242 @@
1
+ import { formatCollabDocument } from '@kernhq/contracts';
2
+ import { KernError, uuidv7 } from '@kernhq/kernel';
3
+ import { and, asc, desc, eq, inArray, isNotNull, isNull, lt, sql } from 'drizzle-orm';
4
+ import { initialRank, rankBetween } from '../../client/rank.js';
5
+ import { MODULE_ID } from '../../contract/index.js';
6
+ import { pages } from '../schema.js';
7
+ /** The name this page's prose is synchronised under, on the collab service. */
8
+ export function documentNameOf(row) {
9
+ return formatCollabDocument({
10
+ workspaceId: row.workspaceId,
11
+ module: MODULE_ID,
12
+ type: 'page',
13
+ objectId: row.id,
14
+ });
15
+ }
16
+ export function toPage(row) {
17
+ return {
18
+ id: row.id,
19
+ workspaceId: row.workspaceId,
20
+ spaceId: row.spaceId,
21
+ parentId: row.parentId,
22
+ position: row.position,
23
+ kind: row.kind,
24
+ title: row.title,
25
+ icon: row.icon,
26
+ coverUrl: row.coverUrl,
27
+ publishedVersionId: row.publishedVersionId,
28
+ hasUnpublishedChanges: row.hasUnpublishedChanges,
29
+ createdBy: row.createdBy,
30
+ updatedBy: row.updatedBy,
31
+ createdAt: row.createdAt.toISOString(),
32
+ updatedAt: row.updatedAt.toISOString(),
33
+ archivedAt: row.archivedAt?.toISOString() ?? null,
34
+ deletedAt: row.deletedAt?.toISOString() ?? null,
35
+ };
36
+ }
37
+ export function quirePages(access) {
38
+ /** The position for a new or moved page landing behind `afterId` among `parentId`'s children. */
39
+ async function positionAfter(tx, workspaceId, spaceId, parentId, afterId, movingId) {
40
+ const siblings = await tx
41
+ .select({ id: pages.id, position: pages.position })
42
+ .from(pages)
43
+ .where(and(eq(pages.workspaceId, workspaceId), eq(pages.spaceId, spaceId), parentId === null ? isNull(pages.parentId) : eq(pages.parentId, parentId), isNull(pages.deletedAt)))
44
+ .orderBy(asc(pages.position));
45
+ const ordered = siblings.filter((s) => s.id !== movingId);
46
+ if (afterId === null)
47
+ return rankBetween(null, ordered[0]?.position ?? null);
48
+ const at = ordered.findIndex((s) => s.id === afterId);
49
+ if (at < 0)
50
+ throw KernError.badRequest('afterId is not a sibling at this position');
51
+ return rankBetween(ordered[at].position, ordered[at + 1]?.position ?? null);
52
+ }
53
+ return {
54
+ /**
55
+ * Every page in the space, flat, ordered so the caller can build the tree without sorting.
56
+ *
57
+ * One query for the whole space rather than one per expanded level: a wiki sidebar shows every
58
+ * level at once, and paging by level turns opening a space into a request per node.
59
+ */
60
+ async tree(tx, workspaceId, spaceId, includeArchived) {
61
+ const rows = await tx
62
+ .select({
63
+ id: pages.id,
64
+ parentId: pages.parentId,
65
+ position: pages.position,
66
+ kind: pages.kind,
67
+ title: pages.title,
68
+ icon: pages.icon,
69
+ archivedAt: pages.archivedAt,
70
+ })
71
+ .from(pages)
72
+ .where(and(eq(pages.workspaceId, workspaceId), eq(pages.spaceId, spaceId), isNull(pages.deletedAt), includeArchived ? undefined : isNull(pages.archivedAt)))
73
+ .orderBy(asc(pages.position));
74
+ const withChildren = new Set(rows.map((r) => r.parentId).filter((id) => id !== null));
75
+ return rows.map((r) => ({
76
+ id: r.id,
77
+ parentId: r.parentId,
78
+ position: r.position,
79
+ kind: r.kind,
80
+ title: r.title,
81
+ icon: r.icon,
82
+ hasChildren: withChildren.has(r.id),
83
+ archivedAt: r.archivedAt?.toISOString() ?? null,
84
+ }));
85
+ },
86
+ async get(tx, workspaceId, pageId) {
87
+ return toPage(await access.pageRow(tx, workspaceId, pageId));
88
+ },
89
+ async trash(tx, workspaceId, spaceId, limit, cursor) {
90
+ const rows = await tx
91
+ .select()
92
+ .from(pages)
93
+ .where(and(eq(pages.workspaceId, workspaceId), eq(pages.spaceId, spaceId), isNotNull(pages.deletedAt), cursor ? lt(pages.id, cursor) : undefined))
94
+ .orderBy(desc(pages.id))
95
+ .limit(limit + 1);
96
+ const items = rows.slice(0, limit).map(toPage);
97
+ return { items, nextCursor: rows.length > limit ? (items.at(-1)?.id ?? null) : null };
98
+ },
99
+ async create(tx, principal, workspaceId, input) {
100
+ if (input.parentId) {
101
+ const parent = await access.pageRow(tx, workspaceId, input.parentId);
102
+ if (parent.spaceId !== input.spaceId)
103
+ throw KernError.badRequest('The parent page is in a different space');
104
+ }
105
+ const position = await positionAfter(tx, workspaceId, input.spaceId, input.parentId, input.afterId);
106
+ const [row] = await tx
107
+ .insert(pages)
108
+ .values({
109
+ id: uuidv7(),
110
+ workspaceId,
111
+ spaceId: input.spaceId,
112
+ parentId: input.parentId,
113
+ position,
114
+ kind: input.kind,
115
+ title: input.title,
116
+ icon: input.icon,
117
+ createdBy: principal.userId,
118
+ updatedBy: principal.userId,
119
+ })
120
+ .returning();
121
+ return toPage(row);
122
+ },
123
+ async update(tx, principal, workspaceId, pageId, patch) {
124
+ const [row] = await tx
125
+ .update(pages)
126
+ .set({ ...patch, updatedBy: principal.userId, updatedAt: new Date() })
127
+ .where(and(eq(pages.workspaceId, workspaceId), eq(pages.id, pageId), isNull(pages.deletedAt)))
128
+ .returning();
129
+ if (!row)
130
+ throw KernError.notFound('Page');
131
+ return toPage(row);
132
+ },
133
+ /**
134
+ * Reparent and reorder in one step.
135
+ *
136
+ * The cycle guard is the part worth reading: dragging a page onto one of its own descendants
137
+ * would otherwise produce a subtree with no root, invisible in every query that walks down from
138
+ * the top and impossible to reach or delete.
139
+ */
140
+ async move(tx, principal, workspaceId, pageId, parentId, afterId) {
141
+ const row = await access.pageRow(tx, workspaceId, pageId);
142
+ if (parentId === pageId)
143
+ throw KernError.badRequest('A page cannot be its own parent');
144
+ if (parentId) {
145
+ const parent = await access.pageRow(tx, workspaceId, parentId);
146
+ if (parent.spaceId !== row.spaceId)
147
+ throw KernError.badRequest('A page can only move within its own space');
148
+ const parentScope = await access.scopeOf(tx, workspaceId, parentId);
149
+ if (parentScope.ancestorIds.includes(pageId))
150
+ throw KernError.badRequest('A page cannot move inside one of its own descendants');
151
+ }
152
+ const position = await positionAfter(tx, workspaceId, row.spaceId, parentId, afterId, pageId);
153
+ const [moved] = await tx
154
+ .update(pages)
155
+ .set({ parentId, position, updatedBy: principal.userId, updatedAt: new Date() })
156
+ .where(and(eq(pages.workspaceId, workspaceId), eq(pages.id, pageId)))
157
+ .returning();
158
+ return toPage(moved);
159
+ },
160
+ async archive(tx, principal, workspaceId, pageId, archived) {
161
+ const [row] = await tx
162
+ .update(pages)
163
+ .set({
164
+ archivedAt: archived ? new Date() : null,
165
+ updatedBy: principal.userId,
166
+ updatedAt: new Date(),
167
+ })
168
+ .where(and(eq(pages.workspaceId, workspaceId), eq(pages.id, pageId), isNull(pages.deletedAt)))
169
+ .returning();
170
+ if (!row)
171
+ throw KernError.notFound('Page');
172
+ return toPage(row);
173
+ },
174
+ /** Every page in the subtree rooted at `pageId`, including it, nearest first. */
175
+ async subtreeIds(tx, workspaceId, pageId) {
176
+ const res = await tx.execute(sql `
177
+ with recursive subtree as (
178
+ select id from mod_quire.pages
179
+ where workspace_id = ${workspaceId}::uuid and id = ${pageId}::uuid
180
+ union all
181
+ select p.id from mod_quire.pages p
182
+ join subtree on p.parent_id = subtree.id
183
+ where p.workspace_id = ${workspaceId}::uuid
184
+ ) cycle id set looped using path
185
+ select id from subtree
186
+ `);
187
+ return res.rows.map((r) => r.id);
188
+ },
189
+ /** Into the trash, with every descendant — a page whose parent is gone is unreachable. */
190
+ async trashPage(tx, workspaceId, pageId) {
191
+ const ids = await this.subtreeIds(tx, workspaceId, pageId);
192
+ if (ids.length === 0)
193
+ throw KernError.notFound('Page');
194
+ await tx
195
+ .update(pages)
196
+ .set({ deletedAt: new Date(), updatedAt: new Date() })
197
+ .where(and(eq(pages.workspaceId, workspaceId), inArray(pages.id, ids)));
198
+ return { ids };
199
+ },
200
+ /**
201
+ * Back out of the trash. Only the page asked for and its descendants come back, and only if its
202
+ * parent is still there — restoring under a deleted parent would put it somewhere unreachable.
203
+ */
204
+ async restore(tx, workspaceId, pageId) {
205
+ const row = await access.pageRow(tx, workspaceId, pageId, { includeTrashed: true });
206
+ if (!row.deletedAt)
207
+ return toPage(row);
208
+ if (row.parentId) {
209
+ const [parent] = await tx
210
+ .select({ deletedAt: pages.deletedAt })
211
+ .from(pages)
212
+ .where(and(eq(pages.workspaceId, workspaceId), eq(pages.id, row.parentId)))
213
+ .limit(1);
214
+ // Restoring to the top of the space beats restoring into nothing.
215
+ if (!parent || parent.deletedAt) {
216
+ const position = await positionAfter(tx, workspaceId, row.spaceId, null, null, pageId);
217
+ await tx
218
+ .update(pages)
219
+ .set({ parentId: null, position })
220
+ .where(and(eq(pages.workspaceId, workspaceId), eq(pages.id, pageId)));
221
+ }
222
+ }
223
+ const ids = await this.subtreeIds(tx, workspaceId, pageId);
224
+ await tx
225
+ .update(pages)
226
+ .set({ deletedAt: null, updatedAt: new Date() })
227
+ .where(and(eq(pages.workspaceId, workspaceId), inArray(pages.id, ids)));
228
+ return toPage(await access.pageRow(tx, workspaceId, pageId));
229
+ },
230
+ /** Gone. The caller is responsible for telling collab to forget the documents. */
231
+ async purge(tx, workspaceId, pageId) {
232
+ const ids = await this.subtreeIds(tx, workspaceId, pageId);
233
+ if (ids.length === 0)
234
+ throw KernError.notFound('Page');
235
+ await tx.delete(pages).where(and(eq(pages.workspaceId, workspaceId), inArray(pages.id, ids)));
236
+ return { ids };
237
+ },
238
+ /** The first page of a brand new space, so a space is never an empty screen. */
239
+ firstPosition: () => initialRank(),
240
+ };
241
+ }
242
+ //# sourceMappingURL=pages.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"pages.js","sourceRoot":"","sources":["../../../src/server/services/pages.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,95 @@
1
+ import type { Principal } from '@kernhq/contracts';
2
+ import { type Tx } from '@kernhq/kernel';
3
+ import type { Space } from '../../contract/index.js';
4
+ import { spaces } from '../schema.js';
5
+ import type { QuireAccess } from './access.js';
6
+ type SpaceRow = typeof spaces.$inferSelect;
7
+ export declare function toSpace(row: SpaceRow): Space;
8
+ export declare function quireSpaces(access: QuireAccess): {
9
+ /**
10
+ * Only the spaces this person may see.
11
+ *
12
+ * The filter is per row rather than a single workspace-level check, because visibility is the
13
+ * whole point of a space: a `private` one should not appear in a list to someone with no
14
+ * binding, and "you may not open it" is a different, worse answer than not showing it at all.
15
+ */
16
+ list(tx: Tx, principal: Principal, workspaceId: string, includeArchived: boolean): Promise<{
17
+ id: string;
18
+ workspaceId: string & import("zod").$brand<"WorkspaceId">;
19
+ key: string;
20
+ name: string;
21
+ description: string;
22
+ icon: string | null;
23
+ visibility: "open" | "restricted" | "private";
24
+ homepageId: string | null;
25
+ createdBy: (string & import("zod").$brand<"UserId">) | null;
26
+ createdAt: string;
27
+ updatedAt: string;
28
+ archivedAt: string | null;
29
+ }[]>;
30
+ get(tx: Tx, principal: Principal, workspaceId: string, spaceId: string): Promise<{
31
+ id: string;
32
+ workspaceId: string & import("zod").$brand<"WorkspaceId">;
33
+ key: string;
34
+ name: string;
35
+ description: string;
36
+ icon: string | null;
37
+ visibility: "open" | "restricted" | "private";
38
+ homepageId: string | null;
39
+ createdBy: (string & import("zod").$brand<"UserId">) | null;
40
+ createdAt: string;
41
+ updatedAt: string;
42
+ archivedAt: string | null;
43
+ }>;
44
+ create(tx: Tx, principal: Principal, workspaceId: string, input: {
45
+ key: string;
46
+ name: string;
47
+ description: string;
48
+ icon: string | null;
49
+ visibility: string;
50
+ }): Promise<{
51
+ id: string;
52
+ workspaceId: string & import("zod").$brand<"WorkspaceId">;
53
+ key: string;
54
+ name: string;
55
+ description: string;
56
+ icon: string | null;
57
+ visibility: "open" | "restricted" | "private";
58
+ homepageId: string | null;
59
+ createdBy: (string & import("zod").$brand<"UserId">) | null;
60
+ createdAt: string;
61
+ updatedAt: string;
62
+ archivedAt: string | null;
63
+ }>;
64
+ update(tx: Tx, workspaceId: string, spaceId: string, patch: Partial<Pick<SpaceRow, "name" | "description" | "icon" | "visibility" | "homepageId">>): Promise<{
65
+ id: string;
66
+ workspaceId: string & import("zod").$brand<"WorkspaceId">;
67
+ key: string;
68
+ name: string;
69
+ description: string;
70
+ icon: string | null;
71
+ visibility: "open" | "restricted" | "private";
72
+ homepageId: string | null;
73
+ createdBy: (string & import("zod").$brand<"UserId">) | null;
74
+ createdAt: string;
75
+ updatedAt: string;
76
+ archivedAt: string | null;
77
+ }>;
78
+ archive(tx: Tx, workspaceId: string, spaceId: string, archived: boolean): Promise<{
79
+ id: string;
80
+ workspaceId: string & import("zod").$brand<"WorkspaceId">;
81
+ key: string;
82
+ name: string;
83
+ description: string;
84
+ icon: string | null;
85
+ visibility: "open" | "restricted" | "private";
86
+ homepageId: string | null;
87
+ createdBy: (string & import("zod").$brand<"UserId">) | null;
88
+ createdAt: string;
89
+ updatedAt: string;
90
+ archivedAt: string | null;
91
+ }>;
92
+ };
93
+ export type QuireSpaces = ReturnType<typeof quireSpaces>;
94
+ export {};
95
+ //# sourceMappingURL=spaces.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"spaces.d.ts","sourceRoot":"","sources":["../../../src/server/services/spaces.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,mBAAmB,CAAA;AAClD,OAAO,EAAa,KAAK,EAAE,EAAU,MAAM,gBAAgB,CAAA;AAE3D,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,yBAAyB,CAAA;AACpD,OAAO,EAAS,MAAM,EAAE,MAAM,cAAc,CAAA;AAC5C,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,aAAa,CAAA;AAE9C,KAAK,QAAQ,GAAG,OAAO,MAAM,CAAC,YAAY,CAAA;AAE1C,wBAAgB,OAAO,CAAC,GAAG,EAAE,QAAQ,GAAG,KAAK,CAe5C;AAED,wBAAgB,WAAW,CAAC,MAAM,EAAE,WAAW;IAE3C;;;;;;OAMG;aACY,EAAE,aAAa,SAAS,eAAe,MAAM,mBAAmB,OAAO;;;;;;;;;;;;;;YAkBxE,EAAE,aAAa,SAAS,eAAe,MAAM,WAAW,MAAM;;;;;;;;;;;;;;eAOtE,EAAE,aACK,SAAS,eACP,MAAM,SACZ;QAAE,GAAG,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAC;QAAC,WAAW,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;QAAC,UAAU,EAAE,MAAM,CAAA;KAAE;;;;;;;;;;;;;;eA0B9F,EAAE,eACO,MAAM,WACV,MAAM,SACR,OAAO,CAAC,IAAI,CAAC,QAAQ,EAAE,MAAM,GAAG,aAAa,GAAG,MAAM,GAAG,YAAY,GAAG,YAAY,CAAC,CAAC;;;;;;;;;;;;;;gBA2B7E,EAAE,eAAe,MAAM,WAAW,MAAM,YAAY,OAAO;;;;;;;;;;;;;;EAUhF;AACD,MAAM,MAAM,WAAW,GAAG,UAAU,CAAC,OAAO,WAAW,CAAC,CAAA"}
@@ -0,0 +1,100 @@
1
+ import { KernError, uuidv7 } from '@kernhq/kernel';
2
+ import { and, asc, eq, isNull } from 'drizzle-orm';
3
+ import { pages, spaces } from '../schema.js';
4
+ export function toSpace(row) {
5
+ return {
6
+ id: row.id,
7
+ workspaceId: row.workspaceId,
8
+ key: row.key,
9
+ name: row.name,
10
+ description: row.description,
11
+ icon: row.icon,
12
+ visibility: row.visibility,
13
+ homepageId: row.homepageId,
14
+ createdBy: row.createdBy,
15
+ createdAt: row.createdAt.toISOString(),
16
+ updatedAt: row.updatedAt.toISOString(),
17
+ archivedAt: row.archivedAt?.toISOString() ?? null,
18
+ };
19
+ }
20
+ export function quireSpaces(access) {
21
+ return {
22
+ /**
23
+ * Only the spaces this person may see.
24
+ *
25
+ * The filter is per row rather than a single workspace-level check, because visibility is the
26
+ * whole point of a space: a `private` one should not appear in a list to someone with no
27
+ * binding, and "you may not open it" is a different, worse answer than not showing it at all.
28
+ */
29
+ async list(tx, principal, workspaceId, includeArchived) {
30
+ const rows = await tx
31
+ .select()
32
+ .from(spaces)
33
+ .where(includeArchived
34
+ ? eq(spaces.workspaceId, workspaceId)
35
+ : and(eq(spaces.workspaceId, workspaceId), isNull(spaces.archivedAt)))
36
+ .orderBy(asc(spaces.name));
37
+ const visible = await Promise.all(rows.map(async (r) => (await access.canSpace(principal, 'quire.space.view', workspaceId, r.id)) ? r : null));
38
+ return visible.filter((r) => r !== null).map(toSpace);
39
+ },
40
+ async get(tx, principal, workspaceId, spaceId) {
41
+ const row = await access.spaceRow(tx, workspaceId, spaceId);
42
+ await access.requireSpace(principal, 'quire.space.view', workspaceId, spaceId);
43
+ return toSpace(row);
44
+ },
45
+ async create(tx, principal, workspaceId, input) {
46
+ const [existing] = await tx
47
+ .select({ id: spaces.id })
48
+ .from(spaces)
49
+ .where(and(eq(spaces.workspaceId, workspaceId), eq(spaces.key, input.key)))
50
+ .limit(1);
51
+ if (existing)
52
+ throw KernError.conflict(`A space with the key "${input.key}" already exists`);
53
+ const [row] = await tx
54
+ .insert(spaces)
55
+ .values({
56
+ id: uuidv7(),
57
+ workspaceId,
58
+ key: input.key,
59
+ name: input.name,
60
+ description: input.description,
61
+ icon: input.icon,
62
+ visibility: input.visibility,
63
+ createdBy: principal.userId,
64
+ })
65
+ .returning();
66
+ return toSpace(row);
67
+ },
68
+ async update(tx, workspaceId, spaceId, patch) {
69
+ // A home page that is not in this space would make the space open to a page nobody expected.
70
+ if (patch.homepageId) {
71
+ const [home] = await tx
72
+ .select({ id: pages.id })
73
+ .from(pages)
74
+ .where(and(eq(pages.workspaceId, workspaceId), eq(pages.id, patch.homepageId), eq(pages.spaceId, spaceId), isNull(pages.deletedAt)))
75
+ .limit(1);
76
+ if (!home)
77
+ throw KernError.badRequest('The home page must be a page in this space');
78
+ }
79
+ const [row] = await tx
80
+ .update(spaces)
81
+ .set({ ...patch, updatedAt: new Date() })
82
+ .where(and(eq(spaces.workspaceId, workspaceId), eq(spaces.id, spaceId)))
83
+ .returning();
84
+ if (!row)
85
+ throw KernError.notFound('Space');
86
+ return toSpace(row);
87
+ },
88
+ async archive(tx, workspaceId, spaceId, archived) {
89
+ const [row] = await tx
90
+ .update(spaces)
91
+ .set({ archivedAt: archived ? new Date() : null, updatedAt: new Date() })
92
+ .where(and(eq(spaces.workspaceId, workspaceId), eq(spaces.id, spaceId)))
93
+ .returning();
94
+ if (!row)
95
+ throw KernError.notFound('Space');
96
+ return toSpace(row);
97
+ },
98
+ };
99
+ }
100
+ //# sourceMappingURL=spaces.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"spaces.js","sourceRoot":"","sources":["../../../src/server/services/spaces.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,43 @@
1
+ CREATE SCHEMA IF NOT EXISTS "mod_quire";
2
+ --> statement-breakpoint
3
+ CREATE TABLE "mod_quire"."pages" (
4
+ "id" uuid PRIMARY KEY DEFAULT uuidv7() NOT NULL,
5
+ "workspace_id" uuid NOT NULL,
6
+ "space_id" uuid NOT NULL,
7
+ "parent_id" uuid,
8
+ "position" text COLLATE "C" NOT NULL,
9
+ "kind" text DEFAULT 'page' NOT NULL,
10
+ "title" text DEFAULT '' NOT NULL,
11
+ "icon" text,
12
+ "cover_url" text,
13
+ "published_version_id" uuid,
14
+ "has_unpublished_changes" boolean DEFAULT false NOT NULL,
15
+ "text" text DEFAULT '' NOT NULL,
16
+ "created_by" uuid,
17
+ "updated_by" uuid,
18
+ "created_at" timestamp with time zone DEFAULT now() NOT NULL,
19
+ "updated_at" timestamp with time zone DEFAULT now() NOT NULL,
20
+ "archived_at" timestamp with time zone,
21
+ "deleted_at" timestamp with time zone
22
+ );
23
+ --> statement-breakpoint
24
+ CREATE TABLE "mod_quire"."spaces" (
25
+ "id" uuid PRIMARY KEY DEFAULT uuidv7() NOT NULL,
26
+ "workspace_id" uuid NOT NULL,
27
+ "key" text NOT NULL,
28
+ "name" text NOT NULL,
29
+ "description" text DEFAULT '' NOT NULL,
30
+ "icon" text,
31
+ "visibility" text DEFAULT 'open' NOT NULL,
32
+ "homepage_id" uuid,
33
+ "created_by" uuid,
34
+ "created_at" timestamp with time zone DEFAULT now() NOT NULL,
35
+ "updated_at" timestamp with time zone DEFAULT now() NOT NULL,
36
+ "archived_at" timestamp with time zone
37
+ );
38
+ --> statement-breakpoint
39
+ CREATE INDEX "pages_ws_space_idx" ON "mod_quire"."pages" USING btree ("workspace_id","space_id","position");--> statement-breakpoint
40
+ CREATE INDEX "pages_ws_parent_idx" ON "mod_quire"."pages" USING btree ("workspace_id","parent_id","position");--> statement-breakpoint
41
+ CREATE INDEX "pages_ws_updated_idx" ON "mod_quire"."pages" USING btree ("workspace_id","updated_at");--> statement-breakpoint
42
+ CREATE UNIQUE INDEX "spaces_ws_key_uq" ON "mod_quire"."spaces" USING btree ("workspace_id","key");--> statement-breakpoint
43
+ CREATE INDEX "spaces_ws_idx" ON "mod_quire"."spaces" USING btree ("workspace_id","created_at");
@@ -0,0 +1,17 @@
1
+ -- Row-level security, one block per table in TENANT_TABLES.
2
+ --
3
+ -- `force` matters: without it the table owner bypasses the policy, and the owner is the role the
4
+ -- service connects as. Note that a *superuser* bypasses RLS whatever this says, and the development
5
+ -- and CI database roles are superusers — so a test that does not connect as an unprivileged role
6
+ -- proves nothing about isolation.
7
+ alter table "mod_quire"."spaces" enable row level security;--> statement-breakpoint
8
+ alter table "mod_quire"."spaces" force row level security;--> statement-breakpoint
9
+ create policy "spaces_ws_isolation" on "mod_quire"."spaces"
10
+ using (workspace_id::text = current_setting('app.workspace_id', true))
11
+ with check (workspace_id::text = current_setting('app.workspace_id', true));--> statement-breakpoint
12
+
13
+ alter table "mod_quire"."pages" enable row level security;--> statement-breakpoint
14
+ alter table "mod_quire"."pages" force row level security;--> statement-breakpoint
15
+ create policy "pages_ws_isolation" on "mod_quire"."pages"
16
+ using (workspace_id::text = current_setting('app.workspace_id', true))
17
+ with check (workspace_id::text = current_setting('app.workspace_id', true));