@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.
- package/LICENSE +662 -0
- package/README.md +71 -0
- package/dist/client/rank.d.ts +42 -0
- package/dist/client/rank.d.ts.map +1 -0
- package/dist/client/rank.js +97 -0
- package/dist/client/rank.js.map +1 -0
- package/dist/contract/capabilities.d.ts +35 -0
- package/dist/contract/capabilities.d.ts.map +1 -0
- package/dist/contract/capabilities.js +35 -0
- package/dist/contract/capabilities.js.map +1 -0
- package/dist/contract/events.d.ts +58 -0
- package/dist/contract/events.d.ts.map +1 -0
- package/dist/contract/events.js +18 -0
- package/dist/contract/events.js.map +1 -0
- package/dist/contract/index.d.ts +6 -0
- package/dist/contract/index.d.ts.map +1 -0
- package/dist/contract/index.js +6 -0
- package/dist/contract/index.js.map +1 -0
- package/dist/contract/models.d.ts +95 -0
- package/dist/contract/models.d.ts.map +1 -0
- package/dist/contract/models.js +82 -0
- package/dist/contract/models.js.map +1 -0
- package/dist/contract/permissions.d.ts +51 -0
- package/dist/contract/permissions.d.ts.map +1 -0
- package/dist/contract/permissions.js +59 -0
- package/dist/contract/permissions.js.map +1 -0
- package/dist/contract/router.d.ts +713 -0
- package/dist/contract/router.d.ts.map +1 -0
- package/dist/contract/router.js +109 -0
- package/dist/contract/router.js.map +1 -0
- package/dist/server/_impl.d.ts +815 -0
- package/dist/server/_impl.d.ts.map +1 -0
- package/dist/server/_impl.js +189 -0
- package/dist/server/_impl.js.map +1 -0
- package/dist/server/index.d.ts +3 -0
- package/dist/server/index.d.ts.map +1 -0
- package/dist/server/index.js +87 -0
- package/dist/server/index.js.map +1 -0
- package/dist/server/schema.d.ts +540 -0
- package/dist/server/schema.d.ts.map +1 -0
- package/dist/server/schema.js +86 -0
- package/dist/server/schema.js.map +1 -0
- package/dist/server/services/access.d.ts +78 -0
- package/dist/server/services/access.d.ts.map +1 -0
- package/dist/server/services/access.js +96 -0
- package/dist/server/services/access.js.map +1 -0
- package/dist/server/services/index.d.ts +15 -0
- package/dist/server/services/index.d.ts.map +1 -0
- package/dist/server/services/index.js +22 -0
- package/dist/server/services/index.js.map +1 -0
- package/dist/server/services/pages.d.ts +190 -0
- package/dist/server/services/pages.d.ts.map +1 -0
- package/dist/server/services/pages.js +242 -0
- package/dist/server/services/pages.js.map +1 -0
- package/dist/server/services/spaces.d.ts +95 -0
- package/dist/server/services/spaces.d.ts.map +1 -0
- package/dist/server/services/spaces.js +100 -0
- package/dist/server/services/spaces.js.map +1 -0
- package/migrations/0000_init.sql +43 -0
- package/migrations/0001_rls.sql +17 -0
- package/migrations/meta/0000_snapshot.json +358 -0
- package/migrations/meta/_journal.json +20 -0
- package/package.json +78 -0
- package/src/client/index.ts +67 -0
- package/src/client/rank.test.ts +92 -0
- package/src/client/rank.ts +100 -0
- package/src/contract/capabilities.ts +36 -0
- package/src/contract/events.ts +22 -0
- package/src/contract/index.ts +5 -0
- package/src/contract/models.ts +93 -0
- package/src/contract/permissions.ts +59 -0
- 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));
|