cursedbelt-server 4.26.1 → 4.28.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/server/activity/d1.d.ts +70 -0
- package/dist/server/activity/d1.js +68 -0
- package/dist/server/activity/migrations.d.ts +1 -3
- package/dist/server/activity/migrations.js +7 -32
- package/dist/server/activity/query.d.ts +123 -0
- package/dist/server/activity/query.js +171 -0
- package/dist/server/activity/schema.d.ts +13 -0
- package/dist/server/activity/schema.js +43 -0
- package/dist/server/activity/shareSource.d.ts +1 -1
- package/dist/server/activity/shareSource.js +1 -1
- package/dist/server/activity/store.d.ts +4 -62
- package/dist/server/activity/store.js +14 -145
- package/dist/server/engagement/d1.d.ts +67 -0
- package/dist/server/engagement/d1.js +81 -0
- package/dist/server/engagement/manifestPlaces.d.ts +9 -0
- package/dist/server/engagement/manifestPlaces.js +17 -0
- package/dist/server/engagement/places.js +2 -18
- package/dist/server/notifications/d1.d.ts +80 -0
- package/dist/server/notifications/d1.js +226 -0
- package/dist/server/notifications/service.d.ts +2 -20
- package/dist/server/notifications/types.d.ts +27 -0
- package/dist/server/notifications/types.js +1 -0
- package/dist/server/sharing/contract.d.ts +94 -0
- package/dist/server/sharing/contract.js +3 -0
- package/dist/server/sharing/d1.d.ts +77 -0
- package/dist/server/sharing/d1.js +309 -0
- package/dist/server/sharing/index.d.ts +1 -0
- package/dist/server/sharing/migrations.d.ts +1 -4
- package/dist/server/sharing/migrations.js +7 -72
- package/dist/server/sharing/router.d.ts +8 -4
- package/dist/server/sharing/router.js +7 -5
- package/dist/server/sharing/schema.d.ts +19 -0
- package/dist/server/sharing/schema.js +88 -0
- package/dist/server/sharing/sql.js +1 -1
- package/dist/server/sharing/statements.d.ts +116 -0
- package/dist/server/sharing/statements.js +139 -0
- package/dist/server/sharing/store.d.ts +3 -28
- package/dist/server/sharing/store.js +32 -105
- package/docs/activity.md +7 -0
- package/docs/engagement.md +6 -0
- package/docs/notifications.md +9 -0
- package/package.json +25 -1
- package/src/server/activity/d1.spec.ts +169 -0
- package/src/server/activity/d1.ts +172 -0
- package/src/server/activity/migrations.ts +6 -40
- package/src/server/activity/query.ts +283 -0
- package/src/server/activity/schema.ts +45 -0
- package/src/server/activity/shareSource.ts +2 -2
- package/src/server/activity/store.ts +45 -257
- package/src/server/engagement/d1.spec.ts +102 -0
- package/src/server/engagement/d1.ts +141 -0
- package/src/server/engagement/manifestPlaces.ts +24 -0
- package/src/server/engagement/places.ts +2 -16
- package/src/server/notifications/d1.spec.ts +179 -0
- package/src/server/notifications/d1.ts +334 -0
- package/src/server/notifications/service.ts +7 -23
- package/src/server/notifications/types.ts +29 -0
- package/src/server/sharing/contract.ts +109 -0
- package/src/server/sharing/d1.spec.ts +242 -0
- package/src/server/sharing/d1.ts +430 -0
- package/src/server/sharing/index.ts +3 -0
- package/src/server/sharing/migrations.ts +12 -85
- package/src/server/sharing/router.ts +21 -11
- package/src/server/sharing/schema.ts +90 -0
- package/src/server/sharing/sql.ts +1 -1
- package/src/server/sharing/statements.ts +213 -0
- package/src/server/sharing/store.ts +46 -221
- package/src/server/telemetryIsWorkerSafe.spec.ts +64 -0
|
@@ -49,25 +49,25 @@ import {
|
|
|
49
49
|
strongerAccess,
|
|
50
50
|
} from "cursedbelt-core/sharing/model";
|
|
51
51
|
import { runMigrations } from "../migration/runner.js";
|
|
52
|
-
import {
|
|
53
|
-
|
|
54
|
-
SHARE_GRANTS_TABLE,
|
|
55
|
-
SHARE_GROUPS_TABLE,
|
|
56
|
-
SHARE_GROUP_MEMBERS_TABLE,
|
|
57
|
-
SHARE_MIGRATIONS,
|
|
58
|
-
} from "./migrations.js";
|
|
52
|
+
import type { GrantInput, RevokeInput, ShareEventQuery, ShareTarget } from "./contract.js";
|
|
53
|
+
import { SHARE_MIGRATIONS } from "./migrations.js";
|
|
59
54
|
import { sharedResourceSql } from "./sql.js";
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
}
|
|
55
|
+
import {
|
|
56
|
+
type EventRow,
|
|
57
|
+
eventLimit,
|
|
58
|
+
eventsWhere,
|
|
59
|
+
type GrantRow,
|
|
60
|
+
type GroupMemberRow,
|
|
61
|
+
type GroupRow,
|
|
62
|
+
SHARE_SQL,
|
|
63
|
+
toEvent,
|
|
64
|
+
toGrant,
|
|
65
|
+
} from "./statements.js";
|
|
66
|
+
|
|
67
|
+
// The contract has no driver in it, so `./d1.ts` (which runs on `workerd`) shares it without
|
|
68
|
+
// reaching this file. Re-exported so `cursedbelt-server/sharing` is unchanged.
|
|
69
|
+
export { group, user } from "./contract.js";
|
|
70
|
+
export type { GrantInput, RevokeInput, ShareEventQuery, ShareTarget } from "./contract.js";
|
|
71
71
|
|
|
72
72
|
export interface ShareStoreOptions {
|
|
73
73
|
db: Database;
|
|
@@ -93,79 +93,6 @@ export interface ShareStoreOptions {
|
|
|
93
93
|
now?: () => string;
|
|
94
94
|
}
|
|
95
95
|
|
|
96
|
-
interface GrantRow {
|
|
97
|
-
app: string;
|
|
98
|
-
kind: string;
|
|
99
|
-
resource_id: string;
|
|
100
|
-
subject_type: "user" | "group";
|
|
101
|
-
subject_id: string;
|
|
102
|
-
level: ShareLevel;
|
|
103
|
-
granted_by: string;
|
|
104
|
-
granted_at: string;
|
|
105
|
-
note: string;
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
interface EventRow {
|
|
109
|
-
event_id: number;
|
|
110
|
-
at: string;
|
|
111
|
-
action: ShareAction;
|
|
112
|
-
app: string | null;
|
|
113
|
-
kind: string | null;
|
|
114
|
-
resource_id: string | null;
|
|
115
|
-
subject_type: "user" | "group";
|
|
116
|
-
subject_id: string;
|
|
117
|
-
level: ShareLevel | null;
|
|
118
|
-
previous_level: ShareLevel | null;
|
|
119
|
-
actor: string;
|
|
120
|
-
note: string;
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
const toGrant = (row: GrantRow): ShareGrant => ({
|
|
124
|
-
resource: { app: row.app, kind: row.kind, id: row.resource_id },
|
|
125
|
-
subject: { type: row.subject_type, id: row.subject_id },
|
|
126
|
-
level: row.level,
|
|
127
|
-
grantedBy: row.granted_by,
|
|
128
|
-
grantedAt: row.granted_at,
|
|
129
|
-
note: row.note,
|
|
130
|
-
});
|
|
131
|
-
|
|
132
|
-
const toEvent = (row: EventRow): ShareEvent => ({
|
|
133
|
-
id: row.event_id,
|
|
134
|
-
at: row.at,
|
|
135
|
-
action: row.action,
|
|
136
|
-
resource:
|
|
137
|
-
row.app === null || row.kind === null || row.resource_id === null
|
|
138
|
-
? null
|
|
139
|
-
: { app: row.app, kind: row.kind, id: row.resource_id },
|
|
140
|
-
subject: { type: row.subject_type, id: row.subject_id },
|
|
141
|
-
level: row.level,
|
|
142
|
-
previousLevel: row.previous_level,
|
|
143
|
-
actor: row.actor,
|
|
144
|
-
note: row.note,
|
|
145
|
-
});
|
|
146
|
-
|
|
147
|
-
export interface GrantInput extends ShareTarget {
|
|
148
|
-
subject: ShareSubject;
|
|
149
|
-
level: ShareLevel;
|
|
150
|
-
grantedBy: string;
|
|
151
|
-
note?: string;
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
export interface RevokeInput extends ShareTarget {
|
|
155
|
-
subject: ShareSubject;
|
|
156
|
-
revokedBy: string;
|
|
157
|
-
note?: string;
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
export interface ShareEventQuery {
|
|
161
|
-
kind?: string;
|
|
162
|
-
id?: string;
|
|
163
|
-
subject?: ShareSubject;
|
|
164
|
-
/** Newest first, capped. Default 200 — a panel shows a page, and an unbounded
|
|
165
|
-
* audit read on a busy resource is the query that times out in production. */
|
|
166
|
-
limit?: number;
|
|
167
|
-
}
|
|
168
|
-
|
|
169
96
|
export type ShareStore = ReturnType<typeof createShareStore>;
|
|
170
97
|
|
|
171
98
|
/**
|
|
@@ -204,11 +131,7 @@ export function createShareStore(options: ShareStoreOptions) {
|
|
|
204
131
|
note?: string;
|
|
205
132
|
at: string;
|
|
206
133
|
}): ShareEvent => {
|
|
207
|
-
db.query(
|
|
208
|
-
`INSERT INTO ${SHARE_EVENTS_TABLE}
|
|
209
|
-
(at, action, app, kind, resource_id, subject_type, subject_id, level, previous_level, actor, note)
|
|
210
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
211
|
-
).run(
|
|
134
|
+
db.query(SHARE_SQL.insertEvent).run(
|
|
212
135
|
input.at,
|
|
213
136
|
input.action,
|
|
214
137
|
input.resource?.app ?? null,
|
|
@@ -242,10 +165,7 @@ export function createShareStore(options: ShareStoreOptions) {
|
|
|
242
165
|
|
|
243
166
|
const readGrant = (target: ShareTarget, subject: ShareSubject): ShareGrant | null => {
|
|
244
167
|
const row = db
|
|
245
|
-
.query(
|
|
246
|
-
`SELECT * FROM ${SHARE_GRANTS_TABLE}
|
|
247
|
-
WHERE app = ? AND kind = ? AND resource_id = ? AND subject_type = ? AND subject_id = ?`,
|
|
248
|
-
)
|
|
168
|
+
.query(SHARE_SQL.selectGrant)
|
|
249
169
|
.get(app, target.kind, target.id, subject.type, subject.id) as GrantRow | null;
|
|
250
170
|
return row ? toGrant(row) : null;
|
|
251
171
|
};
|
|
@@ -270,14 +190,7 @@ export function createShareStore(options: ShareStoreOptions) {
|
|
|
270
190
|
const events: ShareEvent[] = [];
|
|
271
191
|
const grant = db.transaction((): ShareGrant => {
|
|
272
192
|
const existing = readGrant(input, subject);
|
|
273
|
-
db.query(
|
|
274
|
-
`INSERT INTO ${SHARE_GRANTS_TABLE}
|
|
275
|
-
(app, kind, resource_id, subject_type, subject_id, level, granted_by, granted_at, note)
|
|
276
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
277
|
-
ON CONFLICT (app, kind, resource_id, subject_type, subject_id)
|
|
278
|
-
DO UPDATE SET level = excluded.level, granted_by = excluded.granted_by,
|
|
279
|
-
granted_at = excluded.granted_at, note = excluded.note`,
|
|
280
|
-
).run(
|
|
193
|
+
db.query(SHARE_SQL.upsertGrant).run(
|
|
281
194
|
app,
|
|
282
195
|
input.kind,
|
|
283
196
|
input.id,
|
|
@@ -324,10 +237,7 @@ export function createShareStore(options: ShareStoreOptions) {
|
|
|
324
237
|
const removed = db.transaction((): ShareLevel | null => {
|
|
325
238
|
const existing = readGrant(input, subject);
|
|
326
239
|
if (!existing) return null;
|
|
327
|
-
db.query(
|
|
328
|
-
`DELETE FROM ${SHARE_GRANTS_TABLE}
|
|
329
|
-
WHERE app = ? AND kind = ? AND resource_id = ? AND subject_type = ? AND subject_id = ?`,
|
|
330
|
-
).run(app, input.kind, input.id, subject.type, subject.id);
|
|
240
|
+
db.query(SHARE_SQL.deleteGrant).run(app, input.kind, input.id, subject.type, subject.id);
|
|
331
241
|
events.push(
|
|
332
242
|
recordEvent({
|
|
333
243
|
action: "revoke",
|
|
@@ -359,12 +269,10 @@ export function createShareStore(options: ShareStoreOptions) {
|
|
|
359
269
|
const events: ShareEvent[] = [];
|
|
360
270
|
const count = db.transaction((): number => {
|
|
361
271
|
const rows = db
|
|
362
|
-
.query(
|
|
272
|
+
.query(SHARE_SQL.selectResourceGrants)
|
|
363
273
|
.all(app, target.kind, target.id) as GrantRow[];
|
|
364
274
|
if (rows.length === 0) return 0;
|
|
365
|
-
db.query(
|
|
366
|
-
`DELETE FROM ${SHARE_GRANTS_TABLE} WHERE app = ? AND kind = ? AND resource_id = ?`,
|
|
367
|
-
).run(app, target.kind, target.id);
|
|
275
|
+
db.query(SHARE_SQL.deleteResourceGrants).run(app, target.kind, target.id);
|
|
368
276
|
for (const row of rows) {
|
|
369
277
|
events.push(
|
|
370
278
|
recordEvent({
|
|
@@ -392,14 +300,10 @@ export function createShareStore(options: ShareStoreOptions) {
|
|
|
392
300
|
const events: ShareEvent[] = [];
|
|
393
301
|
const count = db.transaction((): number => {
|
|
394
302
|
const rows = db
|
|
395
|
-
.query(
|
|
396
|
-
`SELECT * FROM ${SHARE_GRANTS_TABLE} WHERE app = ? AND subject_type = ? AND subject_id = ?`,
|
|
397
|
-
)
|
|
303
|
+
.query(SHARE_SQL.selectSubjectGrants)
|
|
398
304
|
.all(app, normal.type, normal.id) as GrantRow[];
|
|
399
305
|
if (rows.length === 0) return 0;
|
|
400
|
-
db.query(
|
|
401
|
-
`DELETE FROM ${SHARE_GRANTS_TABLE} WHERE app = ? AND subject_type = ? AND subject_id = ?`,
|
|
402
|
-
).run(app, normal.type, normal.id);
|
|
306
|
+
db.query(SHARE_SQL.deleteSubjectGrants).run(app, normal.type, normal.id);
|
|
403
307
|
for (const row of rows) {
|
|
404
308
|
events.push(
|
|
405
309
|
recordEvent({
|
|
@@ -421,16 +325,7 @@ export function createShareStore(options: ShareStoreOptions) {
|
|
|
421
325
|
|
|
422
326
|
/** Who can see this record, strongest first then oldest — the panel's read. */
|
|
423
327
|
listForResource(target: ShareTarget): ShareGrant[] {
|
|
424
|
-
const rows = db
|
|
425
|
-
.query(
|
|
426
|
-
`SELECT * FROM ${SHARE_GRANTS_TABLE}
|
|
427
|
-
WHERE app = ? AND kind = ? AND resource_id = ?
|
|
428
|
-
-- Strongest first, then oldest. Spelled as a CASE rather than an
|
|
429
|
-
-- ORDER BY on level, which happens to work only because 'write'
|
|
430
|
-
-- sorts after 'read' and would silently mis-order a third level.
|
|
431
|
-
ORDER BY CASE level WHEN 'write' THEN 0 ELSE 1 END, granted_at ASC`,
|
|
432
|
-
)
|
|
433
|
-
.all(app, target.kind, target.id) as GrantRow[];
|
|
328
|
+
const rows = db.query(SHARE_SQL.listForResource).all(app, target.kind, target.id) as GrantRow[];
|
|
434
329
|
return rows.map(toGrant);
|
|
435
330
|
},
|
|
436
331
|
|
|
@@ -438,13 +333,7 @@ export function createShareStore(options: ShareStoreOptions) {
|
|
|
438
333
|
* NOT expanded here; use {@link resourceIdsFor} for the resolved answer. */
|
|
439
334
|
listForSubject(subject: ShareSubject): ShareGrant[] {
|
|
440
335
|
const normal = normalized(subject);
|
|
441
|
-
const rows = db
|
|
442
|
-
.query(
|
|
443
|
-
`SELECT * FROM ${SHARE_GRANTS_TABLE}
|
|
444
|
-
WHERE app = ? AND subject_type = ? AND subject_id = ?
|
|
445
|
-
ORDER BY granted_at DESC`,
|
|
446
|
-
)
|
|
447
|
-
.all(app, normal.type, normal.id) as GrantRow[];
|
|
336
|
+
const rows = db.query(SHARE_SQL.listForSubject).all(app, normal.type, normal.id) as GrantRow[];
|
|
448
337
|
return rows.map(toGrant);
|
|
449
338
|
},
|
|
450
339
|
|
|
@@ -459,13 +348,7 @@ export function createShareStore(options: ShareStoreOptions) {
|
|
|
459
348
|
const subjectId = normalizeSubjectId("user", viewer.subjectId);
|
|
460
349
|
if (!subjectId) return "none";
|
|
461
350
|
const rows = db
|
|
462
|
-
.query(
|
|
463
|
-
`SELECT level FROM ${SHARE_GRANTS_TABLE}
|
|
464
|
-
WHERE app = ? AND kind = ? AND resource_id = ?
|
|
465
|
-
AND ( (subject_type = 'user' AND subject_id = ?)
|
|
466
|
-
OR (subject_type = 'group' AND subject_id IN
|
|
467
|
-
(SELECT group_id FROM ${SHARE_GROUP_MEMBERS_TABLE} WHERE subject_id = ?)) )`,
|
|
468
|
-
)
|
|
351
|
+
.query(SHARE_SQL.accessFor)
|
|
469
352
|
.all(app, target.kind, target.id, subjectId, subjectId) as { level: ShareLevel }[];
|
|
470
353
|
let access: ShareAccess = "none";
|
|
471
354
|
for (const row of rows) access = strongerAccess(access, row.level);
|
|
@@ -479,14 +362,7 @@ export function createShareStore(options: ShareStoreOptions) {
|
|
|
479
362
|
if (!subjectId) return [];
|
|
480
363
|
const levels = levelsMeeting(atLeast);
|
|
481
364
|
const rows = db
|
|
482
|
-
.query(
|
|
483
|
-
`SELECT DISTINCT resource_id FROM ${SHARE_GRANTS_TABLE}
|
|
484
|
-
WHERE app = ? AND kind = ? AND level IN (${levels.map(() => "?").join(", ")})
|
|
485
|
-
AND ( (subject_type = 'user' AND subject_id = ?)
|
|
486
|
-
OR (subject_type = 'group' AND subject_id IN
|
|
487
|
-
(SELECT group_id FROM ${SHARE_GROUP_MEMBERS_TABLE} WHERE subject_id = ?)) )
|
|
488
|
-
ORDER BY resource_id`,
|
|
489
|
-
)
|
|
365
|
+
.query(SHARE_SQL.resourceIdsFor(levels.length))
|
|
490
366
|
.all(app, kind, ...levels, subjectId, subjectId) as { resource_id: string }[];
|
|
491
367
|
return rows.map((row) => row.resource_id);
|
|
492
368
|
},
|
|
@@ -505,11 +381,7 @@ export function createShareStore(options: ShareStoreOptions) {
|
|
|
505
381
|
const at = clock();
|
|
506
382
|
const events: ShareEvent[] = [];
|
|
507
383
|
const created = db.transaction((): ShareGroup => {
|
|
508
|
-
db.query(
|
|
509
|
-
`INSERT INTO ${SHARE_GROUPS_TABLE} (group_id, name, created_by, created_at, note)
|
|
510
|
-
VALUES (?, ?, ?, ?, ?)
|
|
511
|
-
ON CONFLICT (group_id) DO UPDATE SET name = excluded.name, note = excluded.note`,
|
|
512
|
-
).run(id, input.name, input.createdBy, at, input.note ?? "");
|
|
384
|
+
db.query(SHARE_SQL.upsertGroup).run(id, input.name, input.createdBy, at, input.note ?? "");
|
|
513
385
|
events.push(
|
|
514
386
|
recordEvent({
|
|
515
387
|
action: "group.create",
|
|
@@ -538,7 +410,7 @@ export function createShareStore(options: ShareStoreOptions) {
|
|
|
538
410
|
const at = clock();
|
|
539
411
|
const events: ShareEvent[] = [];
|
|
540
412
|
db.transaction(() => {
|
|
541
|
-
db.query(
|
|
413
|
+
db.query(SHARE_SQL.renameGroup).run(name, groupId);
|
|
542
414
|
events.push(
|
|
543
415
|
recordEvent({
|
|
544
416
|
action: "group.rename",
|
|
@@ -568,14 +440,8 @@ export function createShareStore(options: ShareStoreOptions) {
|
|
|
568
440
|
const at = clock();
|
|
569
441
|
const events: ShareEvent[] = [];
|
|
570
442
|
db.transaction(() => {
|
|
571
|
-
const grants = db
|
|
572
|
-
|
|
573
|
-
`SELECT * FROM ${SHARE_GRANTS_TABLE} WHERE app = ? AND subject_type = 'group' AND subject_id = ?`,
|
|
574
|
-
)
|
|
575
|
-
.all(app, groupId) as GrantRow[];
|
|
576
|
-
db.query(
|
|
577
|
-
`DELETE FROM ${SHARE_GRANTS_TABLE} WHERE app = ? AND subject_type = 'group' AND subject_id = ?`,
|
|
578
|
-
).run(app, groupId);
|
|
443
|
+
const grants = db.query(SHARE_SQL.selectGroupGrants).all(app, groupId) as GrantRow[];
|
|
444
|
+
db.query(SHARE_SQL.deleteGroupGrants).run(app, groupId);
|
|
579
445
|
for (const row of grants) {
|
|
580
446
|
events.push(
|
|
581
447
|
recordEvent({
|
|
@@ -589,7 +455,7 @@ export function createShareStore(options: ShareStoreOptions) {
|
|
|
589
455
|
}),
|
|
590
456
|
);
|
|
591
457
|
}
|
|
592
|
-
db.query(
|
|
458
|
+
db.query(SHARE_SQL.deleteGroup).run(groupId);
|
|
593
459
|
events.push(
|
|
594
460
|
recordEvent({
|
|
595
461
|
action: "group.delete",
|
|
@@ -606,15 +472,7 @@ export function createShareStore(options: ShareStoreOptions) {
|
|
|
606
472
|
},
|
|
607
473
|
|
|
608
474
|
listGroups(): ShareGroup[] {
|
|
609
|
-
const rows = db
|
|
610
|
-
.query(`SELECT * FROM ${SHARE_GROUPS_TABLE} ORDER BY name`)
|
|
611
|
-
.all() as {
|
|
612
|
-
group_id: string;
|
|
613
|
-
name: string;
|
|
614
|
-
created_by: string;
|
|
615
|
-
created_at: string;
|
|
616
|
-
note: string;
|
|
617
|
-
}[];
|
|
475
|
+
const rows = db.query(SHARE_SQL.listGroups).all() as GroupRow[];
|
|
618
476
|
return rows.map((row) => ({
|
|
619
477
|
id: row.group_id,
|
|
620
478
|
name: row.name,
|
|
@@ -631,10 +489,7 @@ export function createShareStore(options: ShareStoreOptions) {
|
|
|
631
489
|
const at = clock();
|
|
632
490
|
const events: ShareEvent[] = [];
|
|
633
491
|
db.transaction(() => {
|
|
634
|
-
db.query(
|
|
635
|
-
`INSERT INTO ${SHARE_GROUP_MEMBERS_TABLE} (group_id, subject_id, added_by, added_at)
|
|
636
|
-
VALUES (?, ?, ?, ?) ON CONFLICT (group_id, subject_id) DO NOTHING`,
|
|
637
|
-
).run(id, member, actor, at);
|
|
492
|
+
db.query(SHARE_SQL.insertMember).run(id, member, actor, at);
|
|
638
493
|
events.push(
|
|
639
494
|
recordEvent({
|
|
640
495
|
action: "group.join",
|
|
@@ -656,9 +511,7 @@ export function createShareStore(options: ShareStoreOptions) {
|
|
|
656
511
|
const at = clock();
|
|
657
512
|
const events: ShareEvent[] = [];
|
|
658
513
|
db.transaction(() => {
|
|
659
|
-
db.query(
|
|
660
|
-
`DELETE FROM ${SHARE_GROUP_MEMBERS_TABLE} WHERE group_id = ? AND subject_id = ?`,
|
|
661
|
-
).run(id, member);
|
|
514
|
+
db.query(SHARE_SQL.deleteMember).run(id, member);
|
|
662
515
|
events.push(
|
|
663
516
|
recordEvent({
|
|
664
517
|
action: "group.leave",
|
|
@@ -676,15 +529,8 @@ export function createShareStore(options: ShareStoreOptions) {
|
|
|
676
529
|
|
|
677
530
|
listGroupMembers(groupId: string): ShareGroupMember[] {
|
|
678
531
|
const rows = db
|
|
679
|
-
.query(
|
|
680
|
-
|
|
681
|
-
)
|
|
682
|
-
.all(normalizeSubjectId("group", groupId)) as {
|
|
683
|
-
group_id: string;
|
|
684
|
-
subject_id: string;
|
|
685
|
-
added_by: string;
|
|
686
|
-
added_at: string;
|
|
687
|
-
}[];
|
|
532
|
+
.query(SHARE_SQL.listMembers)
|
|
533
|
+
.all(normalizeSubjectId("group", groupId)) as GroupMemberRow[];
|
|
688
534
|
return rows.map((row) => ({
|
|
689
535
|
groupId: row.group_id,
|
|
690
536
|
subjectId: row.subject_id,
|
|
@@ -697,11 +543,7 @@ export function createShareStore(options: ShareStoreOptions) {
|
|
|
697
543
|
groupIdsFor(subjectId: string): string[] {
|
|
698
544
|
const member = normalizeSubjectId("user", subjectId);
|
|
699
545
|
if (!member) return [];
|
|
700
|
-
const rows = db
|
|
701
|
-
.query(
|
|
702
|
-
`SELECT group_id FROM ${SHARE_GROUP_MEMBERS_TABLE} WHERE subject_id = ? ORDER BY group_id`,
|
|
703
|
-
)
|
|
704
|
-
.all(member) as { group_id: string }[];
|
|
546
|
+
const rows = db.query(SHARE_SQL.groupIdsFor).all(member) as { group_id: string }[];
|
|
705
547
|
return rows.map((row) => row.group_id);
|
|
706
548
|
},
|
|
707
549
|
|
|
@@ -709,28 +551,11 @@ export function createShareStore(options: ShareStoreOptions) {
|
|
|
709
551
|
|
|
710
552
|
/** The trail, newest first. Filter by resource, by subject, or neither. */
|
|
711
553
|
events(query: ShareEventQuery = {}): ShareEvent[] {
|
|
712
|
-
const
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
}
|
|
718
|
-
if (query.id !== undefined) {
|
|
719
|
-
clauses.push("resource_id = ?");
|
|
720
|
-
params.push(query.id);
|
|
721
|
-
}
|
|
722
|
-
if (query.subject) {
|
|
723
|
-
const normal = normalized(query.subject);
|
|
724
|
-
clauses.push("subject_type = ? AND subject_id = ?");
|
|
725
|
-
params.push(normal.type, normal.id);
|
|
726
|
-
}
|
|
727
|
-
const where = clauses.map((clause) => `(${clause})`).join(" AND ");
|
|
728
|
-
const limit = Math.max(1, Math.min(query.limit ?? 200, 1000));
|
|
729
|
-
const rows = db
|
|
730
|
-
.query(
|
|
731
|
-
`SELECT * FROM ${SHARE_EVENTS_TABLE} WHERE ${where} ORDER BY event_id DESC LIMIT ?`,
|
|
732
|
-
)
|
|
733
|
-
.all(...params, limit) as EventRow[];
|
|
554
|
+
const { where, params } = eventsWhere(app, {
|
|
555
|
+
...query,
|
|
556
|
+
subject: query.subject ? normalized(query.subject) : undefined,
|
|
557
|
+
});
|
|
558
|
+
const rows = db.query(SHARE_SQL.events(where)).all(...params, eventLimit(query.limit)) as EventRow[];
|
|
734
559
|
return rows.map(toEvent);
|
|
735
560
|
},
|
|
736
561
|
};
|
|
@@ -89,3 +89,67 @@ describe('cursedbelt-server/telemetry', () => {
|
|
|
89
89
|
expect(found).toEqual(['bun:sqlite']);
|
|
90
90
|
});
|
|
91
91
|
});
|
|
92
|
+
|
|
93
|
+
describe('🔴 cursedbelt-server/engagement/d1 — a Worker mounts it (task 2096)', () => {
|
|
94
|
+
test('reaches no `bun:` module at runtime — not the store, not the Bun-only barrel', () => {
|
|
95
|
+
const { files, bunImports } = runtimeGraph(join(import.meta.dir, 'engagement', 'd1.ts'));
|
|
96
|
+
expect(bunImports).toEqual([]);
|
|
97
|
+
// Not vacuous: the policy and the manifest rule are both in the graph, and the store is not.
|
|
98
|
+
expect(files.some((f) => f.endsWith('engagement/policy.ts'))).toBe(true);
|
|
99
|
+
expect(files.some((f) => f.endsWith('engagement/manifestPlaces.ts'))).toBe(true);
|
|
100
|
+
expect(files.some((f) => f.endsWith('engagement/store.ts'))).toBe(false);
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
test("its export map's `import` is the real module, not the Bun-only refusal", () => {
|
|
104
|
+
const pkg = JSON.parse(readFileSync(join(import.meta.dir, '..', '..', 'package.json'), 'utf8')) as {
|
|
105
|
+
exports: Record<string, { import?: string }>;
|
|
106
|
+
};
|
|
107
|
+
expect(pkg.exports['./engagement/d1']?.import).toBe('./dist/server/engagement/d1.js');
|
|
108
|
+
});
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* The three D1 halves `apps/family`'s Worker port needs (task 393). Each sits beside a sync
|
|
113
|
+
* store that is `bun:sqlite` by construction, so the walk also proves the split held: the
|
|
114
|
+
* driver-free modules they share are IN the graph, and the Bun-only siblings are NOT.
|
|
115
|
+
*/
|
|
116
|
+
describe('🔴 cursedbelt-server/{sharing,activity,notifications}/d1 — a Worker imports them (task 393)', () => {
|
|
117
|
+
const pkg = JSON.parse(readFileSync(join(import.meta.dir, '..', '..', 'package.json'), 'utf8')) as {
|
|
118
|
+
exports: Record<string, { import?: string; source?: string }>;
|
|
119
|
+
};
|
|
120
|
+
const CASES = [
|
|
121
|
+
{
|
|
122
|
+
subpath: './sharing/d1',
|
|
123
|
+
entry: join(import.meta.dir, 'sharing', 'd1.ts'),
|
|
124
|
+
reaches: ['sharing/statements.ts', 'sharing/schema.ts', 'sharing/sql.ts', 'sharing/contract.ts'],
|
|
125
|
+
never: ['sharing/store.ts', 'sharing/migrations.ts', 'migration/runner.ts'],
|
|
126
|
+
},
|
|
127
|
+
{
|
|
128
|
+
subpath: './activity/d1',
|
|
129
|
+
entry: join(import.meta.dir, 'activity', 'd1.ts'),
|
|
130
|
+
reaches: ['activity/query.ts', 'activity/schema.ts', 'activity/shareSource.ts', 'sharing/schema.ts'],
|
|
131
|
+
never: ['activity/store.ts', 'activity/migrations.ts', 'sharing/migrations.ts', 'migration/runner.ts'],
|
|
132
|
+
},
|
|
133
|
+
{
|
|
134
|
+
subpath: './notifications/d1',
|
|
135
|
+
entry: join(import.meta.dir, 'notifications', 'd1.ts'),
|
|
136
|
+
reaches: ['notifications/routes.ts'],
|
|
137
|
+
never: ['notifications/service.ts', 'notifications/migrations.ts', 'notifications/hub.ts'],
|
|
138
|
+
},
|
|
139
|
+
] as const;
|
|
140
|
+
|
|
141
|
+
for (const { subpath, entry, reaches, never } of CASES) {
|
|
142
|
+
test(`${subpath} reaches no \`bun:\` module at runtime, and none of its Bun-only siblings`, () => {
|
|
143
|
+
const { files, bunImports } = runtimeGraph(entry);
|
|
144
|
+
expect(bunImports).toEqual([]);
|
|
145
|
+
for (const file of reaches) expect({ file, reached: files.some((f) => f.endsWith(file)) }).toEqual({ file, reached: true });
|
|
146
|
+
for (const file of never) expect({ file, reached: files.some((f) => f.endsWith(file)) }).toEqual({ file, reached: false });
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
test(`${subpath}'s export map points at the real module, not the Bun-only refusal`, () => {
|
|
150
|
+
const rel = entry.split('/src/server/')[1]?.replace(/\.ts$/, '');
|
|
151
|
+
expect(pkg.exports[subpath]?.import).toBe(`./dist/server/${rel}.js`);
|
|
152
|
+
expect(pkg.exports[subpath]?.source).toBe(`./src/server/${rel}.ts`);
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
});
|