@hexis-ai/engram-server 0.11.3 → 0.13.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 +4 -0
- package/dist/adapters/memory-key-store.js +12 -0
- package/dist/adapters/memory.js +47 -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 +4 -0
- package/dist/adapters/postgres-key-store.js +14 -3
- package/dist/adapters/postgres-org-store.d.ts +42 -0
- package/dist/adapters/postgres-org-store.js +120 -0
- package/dist/adapters/postgres.js +57 -80
- package/dist/adapters/util.d.ts +27 -0
- package/dist/adapters/util.js +47 -0
- package/dist/admin.d.ts +26 -4
- package/dist/admin.js +126 -7
- package/dist/auth-resolver.d.ts +32 -0
- package/dist/auth-resolver.js +53 -0
- package/dist/auth.d.ts +196 -0
- package/dist/auth.js +164 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +4 -0
- package/dist/key-store.d.ts +5 -0
- package/dist/main.js +84 -26
- package/dist/migrations/0006-auth.d.ts +2 -0
- package/dist/migrations/0006-auth.js +84 -0
- package/dist/migrations/0007-orgs.d.ts +2 -0
- package/dist/migrations/0007-orgs.js +59 -0
- package/dist/migrations/index.js +4 -0
- package/dist/openapi.js +340 -3
- package/dist/org-store.d.ts +73 -0
- package/dist/org-store.js +12 -0
- package/dist/routes/orgs.d.ts +27 -0
- package/dist/routes/orgs.js +185 -0
- package/dist/schemas.d.ts +18 -0
- package/dist/schemas.js +19 -0
- package/dist/server.d.ts +39 -0
- package/dist/server.js +85 -7
- package/dist/services/orgs.d.ts +95 -0
- package/dist/services/orgs.js +159 -0
- package/dist/storage.d.ts +6 -0
- package/dist/storage.js +14 -0
- package/openapi.json +1279 -1
- package/package.json +5 -11
|
@@ -13,6 +13,10 @@ export declare class InMemoryKeyStore implements KeyStore {
|
|
|
13
13
|
}): Promise<Workspace>;
|
|
14
14
|
getWorkspace(id: string): Promise<Workspace | null>;
|
|
15
15
|
listWorkspaces(): Promise<Workspace[]>;
|
|
16
|
+
updateWorkspace(id: string, patch: {
|
|
17
|
+
name?: string;
|
|
18
|
+
metadata?: Record<string, unknown>;
|
|
19
|
+
}): Promise<Workspace>;
|
|
16
20
|
deleteWorkspace(id: string): Promise<void>;
|
|
17
21
|
issueKey(workspaceId: string, opts?: {
|
|
18
22
|
name?: string;
|
|
@@ -26,6 +26,18 @@ export class InMemoryKeyStore {
|
|
|
26
26
|
async listWorkspaces() {
|
|
27
27
|
return [...this.workspaces.values()].sort((a, b) => b.createdAt.localeCompare(a.createdAt));
|
|
28
28
|
}
|
|
29
|
+
async updateWorkspace(id, patch) {
|
|
30
|
+
const existing = this.workspaces.get(id);
|
|
31
|
+
if (!existing)
|
|
32
|
+
throw new Error("workspace_not_found");
|
|
33
|
+
const next = {
|
|
34
|
+
...existing,
|
|
35
|
+
...(patch.name !== undefined ? { name: patch.name } : {}),
|
|
36
|
+
...(patch.metadata !== undefined ? { metadata: patch.metadata } : {}),
|
|
37
|
+
};
|
|
38
|
+
this.workspaces.set(id, next);
|
|
39
|
+
return next;
|
|
40
|
+
}
|
|
29
41
|
async deleteWorkspace(id) {
|
|
30
42
|
this.workspaces.delete(id);
|
|
31
43
|
for (const [keyId, row] of this.keys) {
|
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) {
|
|
@@ -88,49 +82,12 @@ export class InMemoryAdapter {
|
|
|
88
82
|
const s = this.sessions.get(sessionId);
|
|
89
83
|
if (!s)
|
|
90
84
|
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
|
-
}
|
|
85
|
+
// Patch semantics: undefined = leave alone; null = clear (deletes the
|
|
86
|
+
// key, matching the SessionRow shape where optional fields are absent);
|
|
87
|
+
// value = set. `status` clears to "active" to match the column default.
|
|
88
|
+
const next = applyPartial(s.row, patch, {
|
|
89
|
+
status: "active",
|
|
90
|
+
});
|
|
134
91
|
next.updatedAt = new Date().toISOString();
|
|
135
92
|
s.row = next;
|
|
136
93
|
return foldEvents(s.row, [...s.events.values()], new Date());
|
|
@@ -213,16 +170,15 @@ export class InMemoryAdapter {
|
|
|
213
170
|
const existing = this.persons.get(id);
|
|
214
171
|
if (!existing)
|
|
215
172
|
return null;
|
|
216
|
-
|
|
217
|
-
//
|
|
218
|
-
const next = {
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
};
|
|
173
|
+
// undefined = no-op; null = clear (display_name/role/team to null,
|
|
174
|
+
// source to "auto" to match the column default).
|
|
175
|
+
const next = applyPartial(existing, patch, {
|
|
176
|
+
display_name: null,
|
|
177
|
+
role: null,
|
|
178
|
+
team: null,
|
|
179
|
+
source: "auto",
|
|
180
|
+
});
|
|
181
|
+
next.updated_at = new Date().toISOString();
|
|
226
182
|
this.persons.set(id, next);
|
|
227
183
|
return next;
|
|
228
184
|
}
|
|
@@ -269,8 +225,18 @@ export class InMemoryAdapter {
|
|
|
269
225
|
}
|
|
270
226
|
// --- Aliases ------------------------------------------------------
|
|
271
227
|
async upsertAlias(personId, input) {
|
|
272
|
-
if (!this.persons.has(personId))
|
|
273
|
-
|
|
228
|
+
if (!this.persons.has(personId)) {
|
|
229
|
+
const stubNow = new Date().toISOString();
|
|
230
|
+
this.persons.set(personId, {
|
|
231
|
+
id: personId,
|
|
232
|
+
display_name: null,
|
|
233
|
+
role: null,
|
|
234
|
+
team: null,
|
|
235
|
+
source: undefined,
|
|
236
|
+
created_at: stubNow,
|
|
237
|
+
updated_at: stubNow,
|
|
238
|
+
});
|
|
239
|
+
}
|
|
274
240
|
const key = aliasKey(personId, input.name);
|
|
275
241
|
const now = new Date().toISOString();
|
|
276
242
|
const existing = this.aliases.get(key);
|
|
@@ -304,8 +270,23 @@ export class InMemoryAdapter {
|
|
|
304
270
|
}
|
|
305
271
|
// --- Identities ---------------------------------------------------
|
|
306
272
|
async upsertIdentity(ref, input) {
|
|
307
|
-
|
|
308
|
-
|
|
273
|
+
// Auto-create a stub person when missing. The two telemetry
|
|
274
|
+
// calls (person + identity) race over the network; treating
|
|
275
|
+
// the identity PUT as authoritative for "this person id exists"
|
|
276
|
+
// matches the host-supplied-id upsert model and avoids a 404
|
|
277
|
+
// when the calls arrive out of order.
|
|
278
|
+
if (!this.persons.has(input.person_id)) {
|
|
279
|
+
const stubNow = new Date().toISOString();
|
|
280
|
+
this.persons.set(input.person_id, {
|
|
281
|
+
id: input.person_id,
|
|
282
|
+
display_name: null,
|
|
283
|
+
role: null,
|
|
284
|
+
team: null,
|
|
285
|
+
source: undefined,
|
|
286
|
+
created_at: stubNow,
|
|
287
|
+
updated_at: stubNow,
|
|
288
|
+
});
|
|
289
|
+
}
|
|
309
290
|
const now = new Date().toISOString();
|
|
310
291
|
const existing = this.identities.get(ref);
|
|
311
292
|
// 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
|
+
}
|
|
@@ -15,6 +15,10 @@ export declare class PostgresKeyStore implements KeyStore {
|
|
|
15
15
|
}): Promise<Workspace>;
|
|
16
16
|
getWorkspace(id: string): Promise<Workspace | null>;
|
|
17
17
|
listWorkspaces(): Promise<Workspace[]>;
|
|
18
|
+
updateWorkspace(id: string, patch: {
|
|
19
|
+
name?: string;
|
|
20
|
+
metadata?: Record<string, unknown>;
|
|
21
|
+
}): Promise<Workspace>;
|
|
18
22
|
deleteWorkspace(id: string): Promise<void>;
|
|
19
23
|
issueKey(workspaceId: string, opts?: {
|
|
20
24
|
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) {
|
|
@@ -36,6 +37,19 @@ export class PostgresKeyStore {
|
|
|
36
37
|
`;
|
|
37
38
|
return rows.map(toWorkspace);
|
|
38
39
|
}
|
|
40
|
+
async updateWorkspace(id, patch) {
|
|
41
|
+
const rows = await this.sql `
|
|
42
|
+
UPDATE engram_workspaces
|
|
43
|
+
SET
|
|
44
|
+
name = COALESCE(${patch.name ?? null}, name),
|
|
45
|
+
metadata = COALESCE(${patch.metadata ? patch.metadata : null}, metadata)
|
|
46
|
+
WHERE id = ${id}
|
|
47
|
+
RETURNING id, name, metadata, created_at
|
|
48
|
+
`;
|
|
49
|
+
if (rows.length === 0)
|
|
50
|
+
throw new Error("workspace_not_found");
|
|
51
|
+
return toWorkspace(rows[0]);
|
|
52
|
+
}
|
|
39
53
|
async deleteWorkspace(id) {
|
|
40
54
|
// engram_api_keys cascades via FK. engram_sessions/events are explicit
|
|
41
55
|
// since they predate the workspaces table and have no FK to it.
|
|
@@ -132,6 +146,3 @@ function toInfo(r) {
|
|
|
132
146
|
...(r.revoked_at !== null ? { revokedAt: isoString(r.revoked_at) } : {}),
|
|
133
147
|
};
|
|
134
148
|
}
|
|
135
|
-
function isoString(v) {
|
|
136
|
-
return typeof v === "string" ? v : v.toISOString();
|
|
137
|
-
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import type { Pool } from "pg";
|
|
2
|
+
import type { OrgMembershipRow, OrgRow, OrgStore } from "../org-store";
|
|
3
|
+
export declare class PostgresOrgStore implements OrgStore {
|
|
4
|
+
private readonly pool;
|
|
5
|
+
constructor(pool: Pool);
|
|
6
|
+
createOrg(input: {
|
|
7
|
+
id?: string;
|
|
8
|
+
name?: string;
|
|
9
|
+
metadata?: Record<string, unknown>;
|
|
10
|
+
}): Promise<OrgRow>;
|
|
11
|
+
getOrg(id: string): Promise<OrgRow | null>;
|
|
12
|
+
listOrgs(): Promise<OrgRow[]>;
|
|
13
|
+
updateOrg(id: string, patch: {
|
|
14
|
+
name?: string;
|
|
15
|
+
metadata?: Record<string, unknown>;
|
|
16
|
+
}): Promise<OrgRow>;
|
|
17
|
+
deleteOrg(id: string): Promise<void>;
|
|
18
|
+
listMembers(orgId: string): Promise<OrgMembershipRow[]>;
|
|
19
|
+
upsertMember(input: {
|
|
20
|
+
orgId: string;
|
|
21
|
+
userId: string;
|
|
22
|
+
role?: string;
|
|
23
|
+
}): Promise<OrgMembershipRow>;
|
|
24
|
+
removeMember(orgId: string, userId: string): Promise<void>;
|
|
25
|
+
findUserByEmail(email: string): Promise<{
|
|
26
|
+
id: string;
|
|
27
|
+
email: string;
|
|
28
|
+
} | null>;
|
|
29
|
+
listOrgsForUser(userId: string): Promise<OrgMembershipRow[]>;
|
|
30
|
+
setWorkspaceOrg(workspaceId: string, orgId: string): Promise<void>;
|
|
31
|
+
listWorkspacesForOrg(orgId: string): Promise<{
|
|
32
|
+
id: string;
|
|
33
|
+
name: string | null;
|
|
34
|
+
}[]>;
|
|
35
|
+
listWorkspacesForUser(userId: string): Promise<{
|
|
36
|
+
id: string;
|
|
37
|
+
name: string | null;
|
|
38
|
+
orgId: string;
|
|
39
|
+
}[]>;
|
|
40
|
+
userCanAccessWorkspace(userId: string, workspaceId: string): Promise<boolean>;
|
|
41
|
+
workspaceInOrg(orgId: string, workspaceId: string): Promise<boolean>;
|
|
42
|
+
}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { isoString } from "./util";
|
|
3
|
+
function toOrg(r) {
|
|
4
|
+
return {
|
|
5
|
+
id: r.id,
|
|
6
|
+
name: r.name,
|
|
7
|
+
metadata: r.metadata ?? {},
|
|
8
|
+
createdAt: isoString(r.created_at),
|
|
9
|
+
};
|
|
10
|
+
}
|
|
11
|
+
function toMember(r) {
|
|
12
|
+
return {
|
|
13
|
+
orgId: r.org_id,
|
|
14
|
+
userId: r.user_id,
|
|
15
|
+
role: r.role,
|
|
16
|
+
joinedAt: isoString(r.joined_at),
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
export class PostgresOrgStore {
|
|
20
|
+
pool;
|
|
21
|
+
constructor(pool) {
|
|
22
|
+
this.pool = pool;
|
|
23
|
+
}
|
|
24
|
+
// ---------- orgs ---------------------------------------------
|
|
25
|
+
async createOrg(input) {
|
|
26
|
+
const id = input.id ?? `org_${randomUUID().replace(/-/g, "").slice(0, 16)}`;
|
|
27
|
+
const { rows } = await this.pool.query(`INSERT INTO engram_orgs (id, name, metadata)
|
|
28
|
+
VALUES ($1, $2, $3::jsonb)
|
|
29
|
+
ON CONFLICT (id) DO NOTHING
|
|
30
|
+
RETURNING id, name, metadata, created_at`, [id, input.name ?? null, JSON.stringify(input.metadata ?? {})]);
|
|
31
|
+
if (rows[0])
|
|
32
|
+
return toOrg(rows[0]);
|
|
33
|
+
const existing = await this.getOrg(id);
|
|
34
|
+
if (!existing)
|
|
35
|
+
throw new Error("org_create_failed");
|
|
36
|
+
return existing;
|
|
37
|
+
}
|
|
38
|
+
async getOrg(id) {
|
|
39
|
+
const { rows } = await this.pool.query(`SELECT id, name, metadata, created_at FROM engram_orgs WHERE id = $1`, [id]);
|
|
40
|
+
return rows[0] ? toOrg(rows[0]) : null;
|
|
41
|
+
}
|
|
42
|
+
async listOrgs() {
|
|
43
|
+
const { rows } = await this.pool.query(`SELECT id, name, metadata, created_at FROM engram_orgs ORDER BY created_at`);
|
|
44
|
+
return rows.map(toOrg);
|
|
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
|
+
}
|
|
61
|
+
async deleteOrg(id) {
|
|
62
|
+
// CASCADE drops members + workspaces under this org.
|
|
63
|
+
await this.pool.query(`DELETE FROM engram_orgs WHERE id = $1`, [id]);
|
|
64
|
+
}
|
|
65
|
+
// ---------- members ------------------------------------------
|
|
66
|
+
async listMembers(orgId) {
|
|
67
|
+
const { rows } = await this.pool.query(`SELECT org_id, user_id, role, joined_at
|
|
68
|
+
FROM engram_org_members WHERE org_id = $1 ORDER BY joined_at`, [orgId]);
|
|
69
|
+
return rows.map(toMember);
|
|
70
|
+
}
|
|
71
|
+
async upsertMember(input) {
|
|
72
|
+
const role = input.role ?? "member";
|
|
73
|
+
const { rows } = await this.pool.query(`INSERT INTO engram_org_members (org_id, user_id, role)
|
|
74
|
+
VALUES ($1, $2, $3)
|
|
75
|
+
ON CONFLICT (org_id, user_id) DO UPDATE SET role = EXCLUDED.role
|
|
76
|
+
RETURNING org_id, user_id, role, joined_at`, [input.orgId, input.userId, role]);
|
|
77
|
+
return toMember(rows[0]);
|
|
78
|
+
}
|
|
79
|
+
async removeMember(orgId, userId) {
|
|
80
|
+
await this.pool.query(`DELETE FROM engram_org_members WHERE org_id = $1 AND user_id = $2`, [orgId, userId]);
|
|
81
|
+
}
|
|
82
|
+
async findUserByEmail(email) {
|
|
83
|
+
const { rows } = await this.pool.query(`SELECT id, email FROM engram_auth_users WHERE LOWER(email) = LOWER($1) LIMIT 1`, [email]);
|
|
84
|
+
return rows[0] ?? null;
|
|
85
|
+
}
|
|
86
|
+
async listOrgsForUser(userId) {
|
|
87
|
+
const { rows } = await this.pool.query(`SELECT org_id, user_id, role, joined_at
|
|
88
|
+
FROM engram_org_members WHERE user_id = $1 ORDER BY joined_at`, [userId]);
|
|
89
|
+
return rows.map(toMember);
|
|
90
|
+
}
|
|
91
|
+
// ---------- workspace ↔ org link -----------------------------
|
|
92
|
+
async setWorkspaceOrg(workspaceId, orgId) {
|
|
93
|
+
await this.pool.query(`UPDATE engram_workspaces SET org_id = $1 WHERE id = $2`, [orgId, workspaceId]);
|
|
94
|
+
}
|
|
95
|
+
async listWorkspacesForOrg(orgId) {
|
|
96
|
+
const { rows } = await this.pool.query(`SELECT id, name FROM engram_workspaces WHERE org_id = $1 ORDER BY created_at`, [orgId]);
|
|
97
|
+
return rows;
|
|
98
|
+
}
|
|
99
|
+
async listWorkspacesForUser(userId) {
|
|
100
|
+
const { rows } = await this.pool.query(`SELECT w.id, w.name, w.org_id
|
|
101
|
+
FROM engram_workspaces w
|
|
102
|
+
JOIN engram_org_members m ON m.org_id = w.org_id
|
|
103
|
+
WHERE m.user_id = $1
|
|
104
|
+
ORDER BY w.created_at`, [userId]);
|
|
105
|
+
return rows.map((r) => ({ id: r.id, name: r.name, orgId: r.org_id }));
|
|
106
|
+
}
|
|
107
|
+
async userCanAccessWorkspace(userId, workspaceId) {
|
|
108
|
+
const { rows } = await this.pool.query(`SELECT 1 AS ok
|
|
109
|
+
FROM engram_workspaces w
|
|
110
|
+
JOIN engram_org_members m ON m.org_id = w.org_id
|
|
111
|
+
WHERE m.user_id = $1 AND w.id = $2
|
|
112
|
+
LIMIT 1`, [userId, workspaceId]);
|
|
113
|
+
return rows.length > 0;
|
|
114
|
+
}
|
|
115
|
+
async workspaceInOrg(orgId, workspaceId) {
|
|
116
|
+
const { rows } = await this.pool.query(`SELECT 1 AS ok FROM engram_workspaces
|
|
117
|
+
WHERE id = $1 AND org_id = $2 LIMIT 1`, [workspaceId, orgId]);
|
|
118
|
+
return rows.length > 0;
|
|
119
|
+
}
|
|
120
|
+
}
|