@stage-labs/metro 0.1.0-beta.30 → 0.1.0-beta.31
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/api.js +12 -12
- package/dist/bedrock.js +5 -3
- package/dist/cli.js +18 -11
- package/dist/store.js +26 -3
- package/package.json +1 -1
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/agent-api.ts +9 -14
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/agent-connector-api.ts +150 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/agent-pair.ts +20 -0
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/api-http.ts +29 -6
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/boot.ts +27 -18
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/cli-pair-api.ts +21 -19
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/connector-api.ts +4 -30
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/relay.ts +13 -12
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/run-api.ts +13 -4
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/session-apis.ts +8 -3
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/session.ts +13 -13
- package/runtime/node_modules/@metro-labs/mcp/src/db/agent-connectors.ts +143 -0
- package/runtime/node_modules/@metro-labs/mcp/src/db/connector-relay.ts +8 -8
- package/runtime/node_modules/@metro-labs/mcp/src/db/connectors.ts +1 -1
- package/runtime/node_modules/@metro-labs/mcp/src/db/projects.ts +1 -1
- package/runtime/node_modules/@metro-labs/mcp/src/db/runtime-admin.ts +8 -15
- package/runtime/node_modules/@metro-labs/mcp/src/db/schema.ts +6 -20
- package/runtime/node_modules/@metro-labs/mcp/src/db/users.ts +10 -5
- package/runtime/runtime.json +1 -1
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/cli-pair.ts +0 -20
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/collection-api.ts +0 -202
- package/runtime/node_modules/@metro-labs/mcp/src/daemon/run-pair.ts +0 -18
- package/runtime/node_modules/@metro-labs/mcp/src/db/connector-collections.ts +0 -218
|
@@ -1,202 +0,0 @@
|
|
|
1
|
-
import type { IncomingMessage, ServerResponse } from 'node:http';
|
|
2
|
-
import { errMsg, log } from './log.js';
|
|
3
|
-
import {
|
|
4
|
-
apiFailure,
|
|
5
|
-
apiSession,
|
|
6
|
-
projectParam,
|
|
7
|
-
bodyField,
|
|
8
|
-
cors,
|
|
9
|
-
readJsonBody,
|
|
10
|
-
sendJson,
|
|
11
|
-
type ApiSession,
|
|
12
|
-
} from './api-http.js';
|
|
13
|
-
import { parseId } from '../db/ids.js';
|
|
14
|
-
import { mintCliCode } from './cli-pair.js';
|
|
15
|
-
import type { ConnectorApiDeps } from './connector-api.js';
|
|
16
|
-
|
|
17
|
-
const PREFIX = '/api/collections';
|
|
18
|
-
|
|
19
|
-
type Routable =
|
|
20
|
-
| { kind: 'index' }
|
|
21
|
-
| { kind: 'collection'; id: string }
|
|
22
|
-
| { kind: 'rename'; id: string }
|
|
23
|
-
| { kind: 'code'; id: string }
|
|
24
|
-
| { kind: 'items'; id: string }
|
|
25
|
-
| { kind: 'item'; id: string; connectorId: string };
|
|
26
|
-
|
|
27
|
-
type Target = Routable | { kind: 'unknown' } | null;
|
|
28
|
-
|
|
29
|
-
const ALLOWED: Record<Routable['kind'], string[]> = {
|
|
30
|
-
index: ['GET', 'POST'],
|
|
31
|
-
collection: ['GET', 'DELETE'],
|
|
32
|
-
rename: ['POST'],
|
|
33
|
-
code: ['POST'],
|
|
34
|
-
items: ['POST'],
|
|
35
|
-
item: ['DELETE'],
|
|
36
|
-
};
|
|
37
|
-
|
|
38
|
-
function itemTarget(id: string, rest: string[]): Target {
|
|
39
|
-
if (rest.length === 1) return { kind: 'items', id };
|
|
40
|
-
if (rest.length > 2) return { kind: 'unknown' };
|
|
41
|
-
const connectorId = parseId(rest[1] ?? '');
|
|
42
|
-
return connectorId === null
|
|
43
|
-
? { kind: 'unknown' }
|
|
44
|
-
: { kind: 'item', id, connectorId };
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
function subTarget(id: string, rest: string[]): Target {
|
|
48
|
-
if (rest.length === 0) return { kind: 'collection', id };
|
|
49
|
-
const head = rest[0];
|
|
50
|
-
if (head === 'items') return itemTarget(id, rest);
|
|
51
|
-
if (rest.length > 1) return { kind: 'unknown' };
|
|
52
|
-
if (head === 'rename') return { kind: 'rename', id };
|
|
53
|
-
if (head === 'code') return { kind: 'code', id };
|
|
54
|
-
return { kind: 'unknown' };
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
function target(path: string): Target {
|
|
58
|
-
if (path === PREFIX || path === `${PREFIX}/`) return { kind: 'index' };
|
|
59
|
-
if (!path.startsWith(`${PREFIX}/`)) return null;
|
|
60
|
-
const segments = path.slice(PREFIX.length + 1).split('/').filter(Boolean);
|
|
61
|
-
const head = segments[0];
|
|
62
|
-
if (head === undefined) return { kind: 'index' };
|
|
63
|
-
const id = parseId(head);
|
|
64
|
-
return id === null ? { kind: 'unknown' } : subTarget(id, segments.slice(1));
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
async function nameFrom(req: IncomingMessage): Promise<string | null> {
|
|
68
|
-
const name = bodyField(await readJsonBody(req), 'name');
|
|
69
|
-
return typeof name === 'string' ? name : null;
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
async function handleIndex(
|
|
73
|
-
req: IncomingMessage,
|
|
74
|
-
res: ServerResponse,
|
|
75
|
-
deps: ConnectorApiDeps,
|
|
76
|
-
session: ApiSession,
|
|
77
|
-
): Promise<void> {
|
|
78
|
-
const project = projectParam(req);
|
|
79
|
-
if (project === null) {
|
|
80
|
-
sendJson(req, res, 400, { error: 'a project is required' });
|
|
81
|
-
return;
|
|
82
|
-
}
|
|
83
|
-
if (req.method === 'GET') {
|
|
84
|
-
sendJson(req, res, 200, {
|
|
85
|
-
collections: await deps.listCollections(session.email, project),
|
|
86
|
-
});
|
|
87
|
-
return;
|
|
88
|
-
}
|
|
89
|
-
const name = await nameFrom(req);
|
|
90
|
-
if (name === null) {
|
|
91
|
-
sendJson(req, res, 400, { error: 'name is required' });
|
|
92
|
-
return;
|
|
93
|
-
}
|
|
94
|
-
const list = await deps.createCollection(session.email, project, name);
|
|
95
|
-
log.info({ id: list.id, name: list.name }, 'collection-api: created collection');
|
|
96
|
-
sendJson(req, res, 201, list);
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
async function handleRename(
|
|
100
|
-
req: IncomingMessage,
|
|
101
|
-
res: ServerResponse,
|
|
102
|
-
deps: ConnectorApiDeps,
|
|
103
|
-
session: ApiSession,
|
|
104
|
-
id: string,
|
|
105
|
-
): Promise<void> {
|
|
106
|
-
const name = await nameFrom(req);
|
|
107
|
-
if (name === null) {
|
|
108
|
-
sendJson(req, res, 400, { error: 'name is required' });
|
|
109
|
-
return;
|
|
110
|
-
}
|
|
111
|
-
sendJson(req, res, 200, await deps.renameCollection(session.email, id, name));
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
async function handleItems(
|
|
115
|
-
req: IncomingMessage,
|
|
116
|
-
res: ServerResponse,
|
|
117
|
-
deps: ConnectorApiDeps,
|
|
118
|
-
session: ApiSession,
|
|
119
|
-
id: string,
|
|
120
|
-
): Promise<void> {
|
|
121
|
-
const connectorId = bodyField(await readJsonBody(req), 'connectorId');
|
|
122
|
-
if (typeof connectorId !== 'string' || parseId(connectorId) === null) {
|
|
123
|
-
sendJson(req, res, 400, { error: 'connectorId is required' });
|
|
124
|
-
return;
|
|
125
|
-
}
|
|
126
|
-
sendJson(req, res, 200, await deps.addToCollection(session.email, id, connectorId));
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
async function handleCode(
|
|
130
|
-
req: IncomingMessage,
|
|
131
|
-
res: ServerResponse,
|
|
132
|
-
deps: ConnectorApiDeps,
|
|
133
|
-
session: ApiSession,
|
|
134
|
-
id: string,
|
|
135
|
-
): Promise<void> {
|
|
136
|
-
const collection = await deps.getCollection(session.email, id);
|
|
137
|
-
const minted = mintCliCode({ email: session.email, collectionId: collection.id });
|
|
138
|
-
log.info({ id: collection.id, email: session.email }, 'collection-api: minted cli code');
|
|
139
|
-
sendJson(req, res, 200, { ...minted, collection: collection.name });
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
async function route(
|
|
143
|
-
req: IncomingMessage,
|
|
144
|
-
res: ServerResponse,
|
|
145
|
-
deps: ConnectorApiDeps,
|
|
146
|
-
session: ApiSession,
|
|
147
|
-
tgt: Routable,
|
|
148
|
-
): Promise<void> {
|
|
149
|
-
try {
|
|
150
|
-
if (tgt.kind === 'index')
|
|
151
|
-
await handleIndex(req, res, deps, session);
|
|
152
|
-
else if (tgt.kind === 'rename')
|
|
153
|
-
await handleRename(req, res, deps, session, tgt.id);
|
|
154
|
-
else if (tgt.kind === 'code')
|
|
155
|
-
await handleCode(req, res, deps, session, tgt.id);
|
|
156
|
-
else if (tgt.kind === 'items')
|
|
157
|
-
await handleItems(req, res, deps, session, tgt.id);
|
|
158
|
-
else if (tgt.kind === 'item')
|
|
159
|
-
sendJson(
|
|
160
|
-
req,
|
|
161
|
-
res,
|
|
162
|
-
200,
|
|
163
|
-
await deps.removeFromCollection(session.email, tgt.id, tgt.connectorId),
|
|
164
|
-
);
|
|
165
|
-
else if (req.method === 'DELETE')
|
|
166
|
-
sendJson(req, res, 200, await deps.deleteCollection(session.email, tgt.id));
|
|
167
|
-
else sendJson(req, res, 200, await deps.getCollection(session.email, tgt.id));
|
|
168
|
-
} catch (err) {
|
|
169
|
-
apiFailure(req, res, err, 'collection-api');
|
|
170
|
-
}
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
export function handleCollectionApiRequest(
|
|
174
|
-
req: IncomingMessage,
|
|
175
|
-
res: ServerResponse,
|
|
176
|
-
deps: ConnectorApiDeps,
|
|
177
|
-
): boolean {
|
|
178
|
-
const tgt = target((req.url ?? '').split('?')[0] ?? '');
|
|
179
|
-
if (tgt === null) return false;
|
|
180
|
-
if (req.method === 'OPTIONS') {
|
|
181
|
-
res.writeHead(204, cors(req)).end();
|
|
182
|
-
return true;
|
|
183
|
-
}
|
|
184
|
-
if (tgt.kind === 'unknown') {
|
|
185
|
-
sendJson(req, res, 404, { error: 'no such collection' });
|
|
186
|
-
return true;
|
|
187
|
-
}
|
|
188
|
-
if (!ALLOWED[tgt.kind].includes(req.method ?? '')) {
|
|
189
|
-
sendJson(req, res, 405, { error: 'method not allowed' });
|
|
190
|
-
return true;
|
|
191
|
-
}
|
|
192
|
-
const session = apiSession(req);
|
|
193
|
-
if (!session) {
|
|
194
|
-
sendJson(req, res, 401, { error: 'unauthorized' });
|
|
195
|
-
return true;
|
|
196
|
-
}
|
|
197
|
-
route(req, res, deps, session, tgt).catch((err: unknown) => {
|
|
198
|
-
log.warn({ err: errMsg(err) }, 'list-api: unhandled error');
|
|
199
|
-
if (!res.headersSent) sendJson(req, res, 500, { error: 'list api failed' });
|
|
200
|
-
});
|
|
201
|
-
return true;
|
|
202
|
-
}
|
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
import { makeCodeStore } from './pair-codes.js';
|
|
2
|
-
|
|
3
|
-
export interface RunCode {
|
|
4
|
-
email: string;
|
|
5
|
-
agentId: string;
|
|
6
|
-
}
|
|
7
|
-
|
|
8
|
-
const store = makeCodeStore<RunCode>('mr');
|
|
9
|
-
|
|
10
|
-
export const RUN_CODE_RE = store.pattern;
|
|
11
|
-
|
|
12
|
-
export const mintRunCode = (
|
|
13
|
-
entry: RunCode,
|
|
14
|
-
now?: number,
|
|
15
|
-
): { code: string; expiresAt: number } => store.mint(entry, now);
|
|
16
|
-
|
|
17
|
-
export const takeRunCode = (code: string, now?: number): RunCode | undefined =>
|
|
18
|
-
store.take(code, now);
|
|
@@ -1,218 +0,0 @@
|
|
|
1
|
-
import { isUniqueViolation } from './users.js';
|
|
2
|
-
import { projectIdOrThrow } from './projects.js';
|
|
3
|
-
import { and, asc, eq, inArray, ne } from 'drizzle-orm';
|
|
4
|
-
import { getDb } from './client.js';
|
|
5
|
-
import { newId } from './ids.js';
|
|
6
|
-
import { ConnectorError, connectorName } from './connector-config.js';
|
|
7
|
-
import { collectionItems, collections, connectors } from './schema.js';
|
|
8
|
-
|
|
9
|
-
export interface ConnectorCollectionRow {
|
|
10
|
-
id: string;
|
|
11
|
-
name: string;
|
|
12
|
-
connectorIds: string[];
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
interface CollectionRow {
|
|
16
|
-
id: string;
|
|
17
|
-
projectId: string;
|
|
18
|
-
name: string;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
const missing = (): ConnectorError =>
|
|
22
|
-
new ConnectorError('no such collection', 404);
|
|
23
|
-
|
|
24
|
-
const duplicate = (name: string): ConnectorError =>
|
|
25
|
-
new ConnectorError(`you already have a collection named '${name}'`, 409);
|
|
26
|
-
|
|
27
|
-
const nameClash = (name: string, collection: string): ConnectorError =>
|
|
28
|
-
new ConnectorError(
|
|
29
|
-
`the collection '${collection}' already has a connector named '${name}'`,
|
|
30
|
-
409,
|
|
31
|
-
);
|
|
32
|
-
|
|
33
|
-
async function collidingCollection(
|
|
34
|
-
collectionIds: string[],
|
|
35
|
-
name: string,
|
|
36
|
-
exceptConnectorId: string,
|
|
37
|
-
): Promise<string | null> {
|
|
38
|
-
if (collectionIds.length === 0) return null;
|
|
39
|
-
const rows = await getDb()
|
|
40
|
-
.select({ collection: collections.name })
|
|
41
|
-
.from(collectionItems)
|
|
42
|
-
.innerJoin(connectors, eq(connectors.id, collectionItems.connectorId))
|
|
43
|
-
.innerJoin(collections, eq(collections.id, collectionItems.collectionId))
|
|
44
|
-
.where(
|
|
45
|
-
and(
|
|
46
|
-
inArray(collectionItems.collectionId, collectionIds),
|
|
47
|
-
eq(connectors.name, name),
|
|
48
|
-
ne(connectors.id, exceptConnectorId),
|
|
49
|
-
),
|
|
50
|
-
)
|
|
51
|
-
.limit(1);
|
|
52
|
-
return rows[0]?.collection ?? null;
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
export async function assertRenameFreeOfClash(
|
|
56
|
-
connectorId: string,
|
|
57
|
-
name: string,
|
|
58
|
-
): Promise<void> {
|
|
59
|
-
const rows = await getDb()
|
|
60
|
-
.select({ collectionId: collectionItems.collectionId })
|
|
61
|
-
.from(collectionItems)
|
|
62
|
-
.where(eq(collectionItems.connectorId, connectorId));
|
|
63
|
-
const clash = await collidingCollection(
|
|
64
|
-
rows.map((r) => r.collectionId),
|
|
65
|
-
name,
|
|
66
|
-
connectorId,
|
|
67
|
-
);
|
|
68
|
-
if (clash !== null) throw nameClash(name, clash);
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
async function ownedCollectionOrThrow(
|
|
72
|
-
email: string,
|
|
73
|
-
id: string,
|
|
74
|
-
): Promise<CollectionRow> {
|
|
75
|
-
const rows = await getDb()
|
|
76
|
-
.select()
|
|
77
|
-
.from(collections)
|
|
78
|
-
.where(eq(collections.id, id));
|
|
79
|
-
const row = rows[0];
|
|
80
|
-
if (row === undefined) throw missing();
|
|
81
|
-
try {
|
|
82
|
-
await projectIdOrThrow(email, row.projectId);
|
|
83
|
-
} catch {
|
|
84
|
-
throw missing();
|
|
85
|
-
}
|
|
86
|
-
return row;
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
async function itemsOf(collectionId: string): Promise<string[]> {
|
|
90
|
-
const rows = await getDb()
|
|
91
|
-
.select({ connectorId: collectionItems.connectorId })
|
|
92
|
-
.from(collectionItems)
|
|
93
|
-
.where(eq(collectionItems.collectionId, collectionId))
|
|
94
|
-
.orderBy(asc(collectionItems.id));
|
|
95
|
-
return rows.map((r) => r.connectorId);
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
async function withItems(row: CollectionRow): Promise<ConnectorCollectionRow> {
|
|
99
|
-
return { id: row.id, name: row.name, connectorIds: await itemsOf(row.id) };
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
export async function listCollectionsForEmail(
|
|
103
|
-
email: string,
|
|
104
|
-
project: string,
|
|
105
|
-
): Promise<ConnectorCollectionRow[]> {
|
|
106
|
-
const projectId = await projectIdOrThrow(email, project);
|
|
107
|
-
const rows = await getDb()
|
|
108
|
-
.select()
|
|
109
|
-
.from(collections)
|
|
110
|
-
.where(eq(collections.projectId, projectId))
|
|
111
|
-
.orderBy(asc(collections.id));
|
|
112
|
-
return Promise.all(rows.map(withItems));
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
export async function getCollectionForEmail(
|
|
116
|
-
email: string,
|
|
117
|
-
id: string,
|
|
118
|
-
): Promise<ConnectorCollectionRow> {
|
|
119
|
-
return withItems(await ownedCollectionOrThrow(email, id));
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
export async function createCollectionForEmail(
|
|
123
|
-
email: string,
|
|
124
|
-
project: string,
|
|
125
|
-
raw: string,
|
|
126
|
-
): Promise<ConnectorCollectionRow> {
|
|
127
|
-
const name = connectorName(raw);
|
|
128
|
-
const projectId = await projectIdOrThrow(email, project);
|
|
129
|
-
try {
|
|
130
|
-
const rows = await getDb()
|
|
131
|
-
.insert(collections)
|
|
132
|
-
.values({ id: newId(), projectId, name })
|
|
133
|
-
.returning();
|
|
134
|
-
const row = rows[0];
|
|
135
|
-
if (row === undefined)
|
|
136
|
-
throw new ConnectorError('collection insert returned no row', 500);
|
|
137
|
-
return { id: row.id, name: row.name, connectorIds: [] };
|
|
138
|
-
} catch (err) {
|
|
139
|
-
if (!isUniqueViolation(err)) throw err;
|
|
140
|
-
throw duplicate(name);
|
|
141
|
-
}
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
export async function renameCollectionForEmail(
|
|
145
|
-
email: string,
|
|
146
|
-
id: string,
|
|
147
|
-
raw: string,
|
|
148
|
-
): Promise<ConnectorCollectionRow> {
|
|
149
|
-
const name = connectorName(raw);
|
|
150
|
-
const row = await ownedCollectionOrThrow(email, id);
|
|
151
|
-
try {
|
|
152
|
-
await getDb()
|
|
153
|
-
.update(collections)
|
|
154
|
-
.set({ name })
|
|
155
|
-
.where(eq(collections.id, row.id));
|
|
156
|
-
} catch (err) {
|
|
157
|
-
if (!isUniqueViolation(err)) throw err;
|
|
158
|
-
throw duplicate(name);
|
|
159
|
-
}
|
|
160
|
-
return withItems({ ...row, name });
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
export async function deleteCollectionForEmail(
|
|
164
|
-
email: string,
|
|
165
|
-
id: string,
|
|
166
|
-
): Promise<{ id: string; name: string }> {
|
|
167
|
-
const row = await ownedCollectionOrThrow(email, id);
|
|
168
|
-
await getDb().delete(collections).where(eq(collections.id, row.id));
|
|
169
|
-
return { id: row.id, name: row.name };
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
async function connectorNameIn(
|
|
173
|
-
projectId: string,
|
|
174
|
-
connectorId: string,
|
|
175
|
-
): Promise<string | null> {
|
|
176
|
-
const rows = await getDb()
|
|
177
|
-
.select({ name: connectors.name })
|
|
178
|
-
.from(connectors)
|
|
179
|
-
.where(and(eq(connectors.id, connectorId), eq(connectors.projectId, projectId)));
|
|
180
|
-
return rows[0]?.name ?? null;
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
export async function addToCollectionForEmail(
|
|
184
|
-
email: string,
|
|
185
|
-
id: string,
|
|
186
|
-
connectorId: string,
|
|
187
|
-
): Promise<ConnectorCollectionRow> {
|
|
188
|
-
const row = await ownedCollectionOrThrow(email, id);
|
|
189
|
-
const name = await connectorNameIn(row.projectId, connectorId);
|
|
190
|
-
if (name === null) throw new ConnectorError('no such connector', 404);
|
|
191
|
-
const clash = await collidingCollection([row.id], name, connectorId);
|
|
192
|
-
if (clash !== null) throw nameClash(name, clash);
|
|
193
|
-
try {
|
|
194
|
-
await getDb()
|
|
195
|
-
.insert(collectionItems)
|
|
196
|
-
.values({ id: newId(), collectionId: row.id, connectorId });
|
|
197
|
-
} catch (err) {
|
|
198
|
-
if (!isUniqueViolation(err)) throw err;
|
|
199
|
-
}
|
|
200
|
-
return withItems(row);
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
export async function removeFromCollectionForEmail(
|
|
204
|
-
email: string,
|
|
205
|
-
id: string,
|
|
206
|
-
connectorId: string,
|
|
207
|
-
): Promise<ConnectorCollectionRow> {
|
|
208
|
-
const row = await ownedCollectionOrThrow(email, id);
|
|
209
|
-
await getDb()
|
|
210
|
-
.delete(collectionItems)
|
|
211
|
-
.where(
|
|
212
|
-
and(
|
|
213
|
-
eq(collectionItems.collectionId, row.id),
|
|
214
|
-
eq(collectionItems.connectorId, connectorId),
|
|
215
|
-
),
|
|
216
|
-
);
|
|
217
|
-
return withItems(row);
|
|
218
|
-
}
|