@hexis-ai/engram-server 0.12.0 → 0.14.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/adapters/memory-key-store.d.ts +13 -0
- package/dist/adapters/memory-key-store.js +27 -0
- package/dist/adapters/memory.js +53 -66
- package/dist/adapters/pg-tagged.d.ts +18 -0
- package/dist/adapters/pg-tagged.js +29 -0
- package/dist/adapters/postgres-key-store.d.ts +5 -0
- package/dist/adapters/postgres-key-store.js +21 -5
- package/dist/adapters/postgres-org-store.d.ts +5 -1
- package/dist/adapters/postgres-org-store.js +25 -8
- package/dist/adapters/postgres.js +76 -83
- package/dist/adapters/util.d.ts +27 -0
- package/dist/adapters/util.js +47 -0
- package/dist/admin.js +78 -89
- package/dist/key-store.d.ts +13 -0
- package/dist/main.js +29 -44
- package/dist/migrations/0008-trigger-metadata.d.ts +2 -0
- package/dist/migrations/0008-trigger-metadata.js +15 -0
- package/dist/migrations/index.js +2 -0
- package/dist/openapi.js +340 -3
- package/dist/org-store.d.ts +7 -6
- package/dist/routes/orgs.d.ts +27 -0
- package/dist/routes/orgs.js +185 -0
- package/dist/schemas.d.ts +22 -0
- package/dist/schemas.js +23 -0
- package/dist/server.d.ts +13 -0
- package/dist/server.js +40 -23
- package/dist/services/orgs.d.ts +99 -0
- package/dist/services/orgs.js +163 -0
- package/dist/storage.d.ts +8 -0
- package/dist/storage.js +20 -0
- package/openapi.json +1331 -13
- package/package.json +4 -13
|
@@ -6,13 +6,26 @@ export declare class InMemoryKeyStore implements KeyStore {
|
|
|
6
6
|
private readonly workspaces;
|
|
7
7
|
private readonly keys;
|
|
8
8
|
private readonly byHash;
|
|
9
|
+
/** Mirror of engram_workspaces.org_id. Lookup-only — fakes / org-stores
|
|
10
|
+
* in tests read this to answer membership queries without re-tracking
|
|
11
|
+
* the binding themselves. */
|
|
12
|
+
private readonly orgIds;
|
|
9
13
|
createWorkspace(input: {
|
|
10
14
|
id?: string;
|
|
11
15
|
name?: string;
|
|
12
16
|
metadata?: Record<string, unknown>;
|
|
17
|
+
orgId?: string;
|
|
13
18
|
}): Promise<Workspace>;
|
|
19
|
+
/** Org id stamped on a workspace at creation, or undefined if not org-scoped. */
|
|
20
|
+
getWorkspaceOrgId(workspaceId: string): string | undefined;
|
|
21
|
+
/** All (workspaceId, orgId) pairs. Read-only view for fakes. */
|
|
22
|
+
workspaceOrgBindings(): ReadonlyMap<string, string>;
|
|
14
23
|
getWorkspace(id: string): Promise<Workspace | null>;
|
|
15
24
|
listWorkspaces(): Promise<Workspace[]>;
|
|
25
|
+
updateWorkspace(id: string, patch: {
|
|
26
|
+
name?: string;
|
|
27
|
+
metadata?: Record<string, unknown>;
|
|
28
|
+
}): Promise<Workspace>;
|
|
16
29
|
deleteWorkspace(id: string): Promise<void>;
|
|
17
30
|
issueKey(workspaceId: string, opts?: {
|
|
18
31
|
name?: string;
|
|
@@ -6,6 +6,10 @@ export class InMemoryKeyStore {
|
|
|
6
6
|
workspaces = new Map();
|
|
7
7
|
keys = new Map();
|
|
8
8
|
byHash = new Map();
|
|
9
|
+
/** Mirror of engram_workspaces.org_id. Lookup-only — fakes / org-stores
|
|
10
|
+
* in tests read this to answer membership queries without re-tracking
|
|
11
|
+
* the binding themselves. */
|
|
12
|
+
orgIds = new Map();
|
|
9
13
|
async createWorkspace(input) {
|
|
10
14
|
const id = resolveWorkspaceId(input);
|
|
11
15
|
const existing = this.workspaces.get(id);
|
|
@@ -18,16 +22,39 @@ export class InMemoryKeyStore {
|
|
|
18
22
|
createdAt: new Date().toISOString(),
|
|
19
23
|
};
|
|
20
24
|
this.workspaces.set(id, ws);
|
|
25
|
+
if (input.orgId !== undefined)
|
|
26
|
+
this.orgIds.set(id, input.orgId);
|
|
21
27
|
return ws;
|
|
22
28
|
}
|
|
29
|
+
/** Org id stamped on a workspace at creation, or undefined if not org-scoped. */
|
|
30
|
+
getWorkspaceOrgId(workspaceId) {
|
|
31
|
+
return this.orgIds.get(workspaceId);
|
|
32
|
+
}
|
|
33
|
+
/** All (workspaceId, orgId) pairs. Read-only view for fakes. */
|
|
34
|
+
workspaceOrgBindings() {
|
|
35
|
+
return this.orgIds;
|
|
36
|
+
}
|
|
23
37
|
async getWorkspace(id) {
|
|
24
38
|
return this.workspaces.get(id) ?? null;
|
|
25
39
|
}
|
|
26
40
|
async listWorkspaces() {
|
|
27
41
|
return [...this.workspaces.values()].sort((a, b) => b.createdAt.localeCompare(a.createdAt));
|
|
28
42
|
}
|
|
43
|
+
async updateWorkspace(id, patch) {
|
|
44
|
+
const existing = this.workspaces.get(id);
|
|
45
|
+
if (!existing)
|
|
46
|
+
throw new Error("workspace_not_found");
|
|
47
|
+
const next = {
|
|
48
|
+
...existing,
|
|
49
|
+
...(patch.name !== undefined ? { name: patch.name } : {}),
|
|
50
|
+
...(patch.metadata !== undefined ? { metadata: patch.metadata } : {}),
|
|
51
|
+
};
|
|
52
|
+
this.workspaces.set(id, next);
|
|
53
|
+
return next;
|
|
54
|
+
}
|
|
29
55
|
async deleteWorkspace(id) {
|
|
30
56
|
this.workspaces.delete(id);
|
|
57
|
+
this.orgIds.delete(id);
|
|
31
58
|
for (const [keyId, row] of this.keys) {
|
|
32
59
|
if (row.workspaceId === id) {
|
|
33
60
|
this.byHash.delete(row.keyHash);
|
package/dist/adapters/memory.js
CHANGED
|
@@ -1,11 +1,5 @@
|
|
|
1
|
-
import { foldEvents } from "../storage";
|
|
2
|
-
|
|
3
|
-
const ALPHA = "abcdefghijklmnopqrstuvwxyz0123456789";
|
|
4
|
-
let out = "p_";
|
|
5
|
-
for (let i = 0; i < 8; i++)
|
|
6
|
-
out += ALPHA[Math.floor(Math.random() * ALPHA.length)];
|
|
7
|
-
return out;
|
|
8
|
-
}
|
|
1
|
+
import { foldEvents, newPersonId } from "../storage";
|
|
2
|
+
import { applyPartial } from "./util";
|
|
9
3
|
/**
|
|
10
4
|
* In-process storage adapter for tests, dev, and small single-node deploys.
|
|
11
5
|
* Idempotency: events keyed by (sessionId, seq) — duplicates overwrite by seq.
|
|
@@ -19,7 +13,7 @@ export class InMemoryAdapter {
|
|
|
19
13
|
identities = new Map();
|
|
20
14
|
newPersonId;
|
|
21
15
|
constructor(opts = {}) {
|
|
22
|
-
this.newPersonId = opts.newPersonId ??
|
|
16
|
+
this.newPersonId = opts.newPersonId ?? newPersonId;
|
|
23
17
|
}
|
|
24
18
|
// --- Sessions -----------------------------------------------------
|
|
25
19
|
async createSession(init) {
|
|
@@ -48,6 +42,12 @@ export class InMemoryAdapter {
|
|
|
48
42
|
...(init.trigger_event_id != null
|
|
49
43
|
? { trigger_event_id: init.trigger_event_id }
|
|
50
44
|
: {}),
|
|
45
|
+
...(init.trigger_purpose != null
|
|
46
|
+
? { trigger_purpose: init.trigger_purpose }
|
|
47
|
+
: {}),
|
|
48
|
+
...(init.trigger_resume_hint != null
|
|
49
|
+
? { trigger_resume_hint: init.trigger_resume_hint }
|
|
50
|
+
: {}),
|
|
51
51
|
},
|
|
52
52
|
events: new Map(),
|
|
53
53
|
});
|
|
@@ -88,49 +88,12 @@ export class InMemoryAdapter {
|
|
|
88
88
|
const s = this.sessions.get(sessionId);
|
|
89
89
|
if (!s)
|
|
90
90
|
return null;
|
|
91
|
-
// Patch semantics: undefined = leave alone; null = clear
|
|
92
|
-
//
|
|
93
|
-
//
|
|
94
|
-
const next =
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
delete next.title;
|
|
98
|
-
else
|
|
99
|
-
next.title = patch.title;
|
|
100
|
-
}
|
|
101
|
-
if (patch.channel !== undefined) {
|
|
102
|
-
if (patch.channel === null)
|
|
103
|
-
delete next.channel;
|
|
104
|
-
else
|
|
105
|
-
next.channel = patch.channel;
|
|
106
|
-
}
|
|
107
|
-
if (patch.status !== undefined) {
|
|
108
|
-
next.status = patch.status;
|
|
109
|
-
}
|
|
110
|
-
if (patch.summary !== undefined) {
|
|
111
|
-
if (patch.summary === null)
|
|
112
|
-
delete next.summary;
|
|
113
|
-
else
|
|
114
|
-
next.summary = patch.summary;
|
|
115
|
-
}
|
|
116
|
-
if (patch.model !== undefined) {
|
|
117
|
-
if (patch.model === null)
|
|
118
|
-
delete next.model;
|
|
119
|
-
else
|
|
120
|
-
next.model = patch.model;
|
|
121
|
-
}
|
|
122
|
-
if (patch.trigger_conversation_id !== undefined) {
|
|
123
|
-
if (patch.trigger_conversation_id === null)
|
|
124
|
-
delete next.trigger_conversation_id;
|
|
125
|
-
else
|
|
126
|
-
next.trigger_conversation_id = patch.trigger_conversation_id;
|
|
127
|
-
}
|
|
128
|
-
if (patch.trigger_event_id !== undefined) {
|
|
129
|
-
if (patch.trigger_event_id === null)
|
|
130
|
-
delete next.trigger_event_id;
|
|
131
|
-
else
|
|
132
|
-
next.trigger_event_id = patch.trigger_event_id;
|
|
133
|
-
}
|
|
91
|
+
// Patch semantics: undefined = leave alone; null = clear (deletes the
|
|
92
|
+
// key, matching the SessionRow shape where optional fields are absent);
|
|
93
|
+
// value = set. `status` clears to "active" to match the column default.
|
|
94
|
+
const next = applyPartial(s.row, patch, {
|
|
95
|
+
status: "active",
|
|
96
|
+
});
|
|
134
97
|
next.updatedAt = new Date().toISOString();
|
|
135
98
|
s.row = next;
|
|
136
99
|
return foldEvents(s.row, [...s.events.values()], new Date());
|
|
@@ -213,16 +176,15 @@ export class InMemoryAdapter {
|
|
|
213
176
|
const existing = this.persons.get(id);
|
|
214
177
|
if (!existing)
|
|
215
178
|
return null;
|
|
216
|
-
|
|
217
|
-
//
|
|
218
|
-
const next = {
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
};
|
|
179
|
+
// undefined = no-op; null = clear (display_name/role/team to null,
|
|
180
|
+
// source to "auto" to match the column default).
|
|
181
|
+
const next = applyPartial(existing, patch, {
|
|
182
|
+
display_name: null,
|
|
183
|
+
role: null,
|
|
184
|
+
team: null,
|
|
185
|
+
source: "auto",
|
|
186
|
+
});
|
|
187
|
+
next.updated_at = new Date().toISOString();
|
|
226
188
|
this.persons.set(id, next);
|
|
227
189
|
return next;
|
|
228
190
|
}
|
|
@@ -269,8 +231,18 @@ export class InMemoryAdapter {
|
|
|
269
231
|
}
|
|
270
232
|
// --- Aliases ------------------------------------------------------
|
|
271
233
|
async upsertAlias(personId, input) {
|
|
272
|
-
if (!this.persons.has(personId))
|
|
273
|
-
|
|
234
|
+
if (!this.persons.has(personId)) {
|
|
235
|
+
const stubNow = new Date().toISOString();
|
|
236
|
+
this.persons.set(personId, {
|
|
237
|
+
id: personId,
|
|
238
|
+
display_name: null,
|
|
239
|
+
role: null,
|
|
240
|
+
team: null,
|
|
241
|
+
source: undefined,
|
|
242
|
+
created_at: stubNow,
|
|
243
|
+
updated_at: stubNow,
|
|
244
|
+
});
|
|
245
|
+
}
|
|
274
246
|
const key = aliasKey(personId, input.name);
|
|
275
247
|
const now = new Date().toISOString();
|
|
276
248
|
const existing = this.aliases.get(key);
|
|
@@ -304,8 +276,23 @@ export class InMemoryAdapter {
|
|
|
304
276
|
}
|
|
305
277
|
// --- Identities ---------------------------------------------------
|
|
306
278
|
async upsertIdentity(ref, input) {
|
|
307
|
-
|
|
308
|
-
|
|
279
|
+
// Auto-create a stub person when missing. The two telemetry
|
|
280
|
+
// calls (person + identity) race over the network; treating
|
|
281
|
+
// the identity PUT as authoritative for "this person id exists"
|
|
282
|
+
// matches the host-supplied-id upsert model and avoids a 404
|
|
283
|
+
// when the calls arrive out of order.
|
|
284
|
+
if (!this.persons.has(input.person_id)) {
|
|
285
|
+
const stubNow = new Date().toISOString();
|
|
286
|
+
this.persons.set(input.person_id, {
|
|
287
|
+
id: input.person_id,
|
|
288
|
+
display_name: null,
|
|
289
|
+
role: null,
|
|
290
|
+
team: null,
|
|
291
|
+
source: undefined,
|
|
292
|
+
created_at: stubNow,
|
|
293
|
+
updated_at: stubNow,
|
|
294
|
+
});
|
|
295
|
+
}
|
|
309
296
|
const now = new Date().toISOString();
|
|
310
297
|
const existing = this.identities.get(ref);
|
|
311
298
|
// COALESCE-on-conflict for the soft-overlap fields; ref-level
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tagged-template SQL client backed by node-postgres (`pg`).
|
|
3
|
+
*
|
|
4
|
+
* The rest of the codebase was written against the `postgres` package's
|
|
5
|
+
* tagged-template surface (`SqlClient` in ./postgres.ts). This adapter
|
|
6
|
+
* exposes the same shape on top of a `pg.Pool` (or `pg.PoolClient`) so
|
|
7
|
+
* we can unify on a single SQL driver — better-auth requires `pg`, and
|
|
8
|
+
* keeping two pools around just to satisfy `postgres-js`'s template API
|
|
9
|
+
* was preventing cross-store transactions.
|
|
10
|
+
*
|
|
11
|
+
* Translation rule: each `${value}` becomes `$N` in the emitted SQL,
|
|
12
|
+
* and the values land in the parameter array in tag order. Arrays
|
|
13
|
+
* map directly to Postgres arrays via `pg`'s native handling (callers
|
|
14
|
+
* already include explicit `::text[]` casts where needed).
|
|
15
|
+
*/
|
|
16
|
+
import type { Pool, PoolClient } from "pg";
|
|
17
|
+
import type { SqlClient } from "./postgres";
|
|
18
|
+
export declare function pgSqlClient(executor: Pool | PoolClient): SqlClient;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tagged-template SQL client backed by node-postgres (`pg`).
|
|
3
|
+
*
|
|
4
|
+
* The rest of the codebase was written against the `postgres` package's
|
|
5
|
+
* tagged-template surface (`SqlClient` in ./postgres.ts). This adapter
|
|
6
|
+
* exposes the same shape on top of a `pg.Pool` (or `pg.PoolClient`) so
|
|
7
|
+
* we can unify on a single SQL driver — better-auth requires `pg`, and
|
|
8
|
+
* keeping two pools around just to satisfy `postgres-js`'s template API
|
|
9
|
+
* was preventing cross-store transactions.
|
|
10
|
+
*
|
|
11
|
+
* Translation rule: each `${value}` becomes `$N` in the emitted SQL,
|
|
12
|
+
* and the values land in the parameter array in tag order. Arrays
|
|
13
|
+
* map directly to Postgres arrays via `pg`'s native handling (callers
|
|
14
|
+
* already include explicit `::text[]` casts where needed).
|
|
15
|
+
*/
|
|
16
|
+
export function pgSqlClient(executor) {
|
|
17
|
+
const tagged = (async (strings, ...values) => {
|
|
18
|
+
let text = strings[0];
|
|
19
|
+
for (let i = 0; i < values.length; i++) {
|
|
20
|
+
text += `$${i + 1}` + strings[i + 1];
|
|
21
|
+
}
|
|
22
|
+
const r = await executor.query({ text, values });
|
|
23
|
+
return r.rows;
|
|
24
|
+
});
|
|
25
|
+
tagged.unsafe = async (text) => {
|
|
26
|
+
await executor.query(text);
|
|
27
|
+
};
|
|
28
|
+
return tagged;
|
|
29
|
+
}
|
|
@@ -12,9 +12,14 @@ export declare class PostgresKeyStore implements KeyStore {
|
|
|
12
12
|
id?: string;
|
|
13
13
|
name?: string;
|
|
14
14
|
metadata?: Record<string, unknown>;
|
|
15
|
+
orgId?: string;
|
|
15
16
|
}): Promise<Workspace>;
|
|
16
17
|
getWorkspace(id: string): Promise<Workspace | null>;
|
|
17
18
|
listWorkspaces(): Promise<Workspace[]>;
|
|
19
|
+
updateWorkspace(id: string, patch: {
|
|
20
|
+
name?: string;
|
|
21
|
+
metadata?: Record<string, unknown>;
|
|
22
|
+
}): Promise<Workspace>;
|
|
18
23
|
deleteWorkspace(id: string): Promise<void>;
|
|
19
24
|
issueKey(workspaceId: string, opts?: {
|
|
20
25
|
name?: string;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { deriveKey, hashKey, mintKey, resolveWorkspaceId, } from "../key-store";
|
|
2
2
|
import { runMigrations } from "../migrator";
|
|
3
|
+
import { isoString } from "./util";
|
|
3
4
|
export class PostgresKeyStore {
|
|
4
5
|
sql;
|
|
5
6
|
constructor(sql) {
|
|
@@ -15,8 +16,13 @@ export class PostgresKeyStore {
|
|
|
15
16
|
async createWorkspace(input) {
|
|
16
17
|
const id = resolveWorkspaceId(input);
|
|
17
18
|
await this.sql `
|
|
18
|
-
INSERT INTO engram_workspaces (id, name, metadata)
|
|
19
|
-
VALUES (
|
|
19
|
+
INSERT INTO engram_workspaces (id, name, metadata, org_id)
|
|
20
|
+
VALUES (
|
|
21
|
+
${id},
|
|
22
|
+
${input.name ?? null},
|
|
23
|
+
${(input.metadata ?? {})},
|
|
24
|
+
${input.orgId ?? null}
|
|
25
|
+
)
|
|
20
26
|
ON CONFLICT (id) DO NOTHING
|
|
21
27
|
`;
|
|
22
28
|
const ws = await this.getWorkspace(id);
|
|
@@ -36,6 +42,19 @@ export class PostgresKeyStore {
|
|
|
36
42
|
`;
|
|
37
43
|
return rows.map(toWorkspace);
|
|
38
44
|
}
|
|
45
|
+
async updateWorkspace(id, patch) {
|
|
46
|
+
const rows = await this.sql `
|
|
47
|
+
UPDATE engram_workspaces
|
|
48
|
+
SET
|
|
49
|
+
name = COALESCE(${patch.name ?? null}, name),
|
|
50
|
+
metadata = COALESCE(${patch.metadata ? patch.metadata : null}, metadata)
|
|
51
|
+
WHERE id = ${id}
|
|
52
|
+
RETURNING id, name, metadata, created_at
|
|
53
|
+
`;
|
|
54
|
+
if (rows.length === 0)
|
|
55
|
+
throw new Error("workspace_not_found");
|
|
56
|
+
return toWorkspace(rows[0]);
|
|
57
|
+
}
|
|
39
58
|
async deleteWorkspace(id) {
|
|
40
59
|
// engram_api_keys cascades via FK. engram_sessions/events are explicit
|
|
41
60
|
// since they predate the workspaces table and have no FK to it.
|
|
@@ -132,6 +151,3 @@ function toInfo(r) {
|
|
|
132
151
|
...(r.revoked_at !== null ? { revokedAt: isoString(r.revoked_at) } : {}),
|
|
133
152
|
};
|
|
134
153
|
}
|
|
135
|
-
function isoString(v) {
|
|
136
|
-
return typeof v === "string" ? v : v.toISOString();
|
|
137
|
-
}
|
|
@@ -10,6 +10,10 @@ export declare class PostgresOrgStore implements OrgStore {
|
|
|
10
10
|
}): Promise<OrgRow>;
|
|
11
11
|
getOrg(id: string): Promise<OrgRow | null>;
|
|
12
12
|
listOrgs(): Promise<OrgRow[]>;
|
|
13
|
+
updateOrg(id: string, patch: {
|
|
14
|
+
name?: string;
|
|
15
|
+
metadata?: Record<string, unknown>;
|
|
16
|
+
}): Promise<OrgRow>;
|
|
13
17
|
deleteOrg(id: string): Promise<void>;
|
|
14
18
|
listMembers(orgId: string): Promise<OrgMembershipRow[]>;
|
|
15
19
|
upsertMember(input: {
|
|
@@ -23,7 +27,6 @@ export declare class PostgresOrgStore implements OrgStore {
|
|
|
23
27
|
email: string;
|
|
24
28
|
} | null>;
|
|
25
29
|
listOrgsForUser(userId: string): Promise<OrgMembershipRow[]>;
|
|
26
|
-
setWorkspaceOrg(workspaceId: string, orgId: string): Promise<void>;
|
|
27
30
|
listWorkspacesForOrg(orgId: string): Promise<{
|
|
28
31
|
id: string;
|
|
29
32
|
name: string | null;
|
|
@@ -34,4 +37,5 @@ export declare class PostgresOrgStore implements OrgStore {
|
|
|
34
37
|
orgId: string;
|
|
35
38
|
}[]>;
|
|
36
39
|
userCanAccessWorkspace(userId: string, workspaceId: string): Promise<boolean>;
|
|
40
|
+
workspaceInOrg(orgId: string, workspaceId: string): Promise<boolean>;
|
|
37
41
|
}
|
|
@@ -1,13 +1,11 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
|
-
|
|
3
|
-
return typeof v === "string" ? v : v.toISOString();
|
|
4
|
-
}
|
|
2
|
+
import { isoString } from "./util";
|
|
5
3
|
function toOrg(r) {
|
|
6
4
|
return {
|
|
7
5
|
id: r.id,
|
|
8
6
|
name: r.name,
|
|
9
7
|
metadata: r.metadata ?? {},
|
|
10
|
-
createdAt:
|
|
8
|
+
createdAt: isoString(r.created_at),
|
|
11
9
|
};
|
|
12
10
|
}
|
|
13
11
|
function toMember(r) {
|
|
@@ -15,7 +13,7 @@ function toMember(r) {
|
|
|
15
13
|
orgId: r.org_id,
|
|
16
14
|
userId: r.user_id,
|
|
17
15
|
role: r.role,
|
|
18
|
-
joinedAt:
|
|
16
|
+
joinedAt: isoString(r.joined_at),
|
|
19
17
|
};
|
|
20
18
|
}
|
|
21
19
|
export class PostgresOrgStore {
|
|
@@ -45,6 +43,21 @@ export class PostgresOrgStore {
|
|
|
45
43
|
const { rows } = await this.pool.query(`SELECT id, name, metadata, created_at FROM engram_orgs ORDER BY created_at`);
|
|
46
44
|
return rows.map(toOrg);
|
|
47
45
|
}
|
|
46
|
+
async updateOrg(id, patch) {
|
|
47
|
+
const { rows } = await this.pool.query(`UPDATE engram_orgs
|
|
48
|
+
SET
|
|
49
|
+
name = COALESCE($2, name),
|
|
50
|
+
metadata = COALESCE($3::jsonb, metadata)
|
|
51
|
+
WHERE id = $1
|
|
52
|
+
RETURNING id, name, metadata, created_at`, [
|
|
53
|
+
id,
|
|
54
|
+
patch.name ?? null,
|
|
55
|
+
patch.metadata ? JSON.stringify(patch.metadata) : null,
|
|
56
|
+
]);
|
|
57
|
+
if (rows.length === 0)
|
|
58
|
+
throw new Error("org_not_found");
|
|
59
|
+
return toOrg(rows[0]);
|
|
60
|
+
}
|
|
48
61
|
async deleteOrg(id) {
|
|
49
62
|
// CASCADE drops members + workspaces under this org.
|
|
50
63
|
await this.pool.query(`DELETE FROM engram_orgs WHERE id = $1`, [id]);
|
|
@@ -76,9 +89,8 @@ export class PostgresOrgStore {
|
|
|
76
89
|
return rows.map(toMember);
|
|
77
90
|
}
|
|
78
91
|
// ---------- workspace ↔ org link -----------------------------
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
}
|
|
92
|
+
// engram_workspaces.org_id is written by KeyStore.createWorkspace in
|
|
93
|
+
// the same INSERT as the workspace row. This store only reads.
|
|
82
94
|
async listWorkspacesForOrg(orgId) {
|
|
83
95
|
const { rows } = await this.pool.query(`SELECT id, name FROM engram_workspaces WHERE org_id = $1 ORDER BY created_at`, [orgId]);
|
|
84
96
|
return rows;
|
|
@@ -99,4 +111,9 @@ export class PostgresOrgStore {
|
|
|
99
111
|
LIMIT 1`, [userId, workspaceId]);
|
|
100
112
|
return rows.length > 0;
|
|
101
113
|
}
|
|
114
|
+
async workspaceInOrg(orgId, workspaceId) {
|
|
115
|
+
const { rows } = await this.pool.query(`SELECT 1 AS ok FROM engram_workspaces
|
|
116
|
+
WHERE id = $1 AND org_id = $2 LIMIT 1`, [workspaceId, orgId]);
|
|
117
|
+
return rows.length > 0;
|
|
118
|
+
}
|
|
102
119
|
}
|