Skip to content

Commit 1c7e4e1

Browse files
feat(api): expose v2 resource owners
1 parent 3b11f09 commit 1c7e4e1

15 files changed

Lines changed: 342 additions & 106 deletions

File tree

apps/docs/openapi-v2-knowledge.json

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1760,6 +1760,7 @@
17601760
"id",
17611761
"name",
17621762
"description",
1763+
"ownerEmail",
17631764
"tokenCount",
17641765
"embeddingModel",
17651766
"embeddingDimension",
@@ -1784,6 +1785,12 @@
17841785
"description": "Optional description of the knowledge base. null when not set.",
17851786
"example": "All product docs and guides"
17861787
},
1788+
"ownerEmail": {
1789+
"type": "string",
1790+
"format": "email",
1791+
"description": "Current email address of the knowledge base owner.",
1792+
"example": "owner@example.com"
1793+
},
17871794
"tokenCount": {
17881795
"type": "integer",
17891796
"description": "Total number of tokens across all indexed documents.",

apps/docs/openapi-v2-tables.json

Lines changed: 7 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -4437,11 +4437,6 @@
44374437
"enum": ["string", "number", "currency", "boolean", "date", "json", "select"],
44384438
"description": "Data type of the column."
44394439
},
4440-
"required": {
4441-
"type": "boolean",
4442-
"default": false,
4443-
"description": "Whether the column requires a value on insert."
4444-
},
44454440
"unique": {
44464441
"type": "boolean",
44474442
"default": false,
@@ -4477,6 +4472,7 @@
44774472
"id",
44784473
"name",
44794474
"description",
4475+
"ownerEmail",
44804476
"schema",
44814477
"rowCount",
44824478
"maxRows",
@@ -4502,6 +4498,12 @@
45024498
"description": "Optional description of the table. Null when not set.",
45034499
"example": "Customer contact records"
45044500
},
4501+
"ownerEmail": {
4502+
"type": "string",
4503+
"format": "email",
4504+
"description": "Current email address of the table owner.",
4505+
"example": "owner@example.com"
4506+
},
45054507
"schema": {
45064508
"type": "object",
45074509
"description": "Table schema definition.",
@@ -4707,10 +4709,6 @@
47074709
"enum": ["string", "number", "currency", "boolean", "date", "json", "select"],
47084710
"description": "New data type for the column."
47094711
},
4710-
"required": {
4711-
"type": "boolean",
4712-
"description": "Whether the column requires a value on insert."
4713-
},
47144712
"unique": {
47154713
"type": "boolean",
47164714
"description": "Whether values in this column must be unique across all rows."

apps/sim/app/api/v2/knowledge/[id]/route.ts

Lines changed: 5 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -12,42 +12,12 @@ import {
1212
updateKnowledgeBaseOperation,
1313
} from '@/lib/knowledge/application/knowledge-bases'
1414
import { knowledgeOperations } from '@/lib/knowledge/application/operations'
15-
import type { KnowledgeBaseWithCounts } from '@/lib/knowledge/types'
15+
import { toV2KnowledgeBase } from '@/app/api/v2/knowledge/utils'
1616
import { v2Error } from '@/app/api/v2/lib/response'
1717

1818
export const dynamic = 'force-dynamic'
1919
export const revalidate = 0
2020

21-
function toV2KnowledgeBase(knowledgeBase: KnowledgeBaseWithCounts, folderPath: string) {
22-
return {
23-
id: knowledgeBase.id,
24-
name: knowledgeBase.name,
25-
description: knowledgeBase.description,
26-
tokenCount: knowledgeBase.tokenCount,
27-
embeddingModel: knowledgeBase.embeddingModel,
28-
embeddingDimension: knowledgeBase.embeddingDimension,
29-
chunkingConfig: {
30-
maxSize: knowledgeBase.chunkingConfig.maxSize,
31-
minSize: knowledgeBase.chunkingConfig.minSize,
32-
overlap: knowledgeBase.chunkingConfig.overlap,
33-
strategy: knowledgeBase.chunkingConfig.strategy,
34-
strategyOptions: knowledgeBase.chunkingConfig.strategyOptions
35-
? {
36-
pattern: knowledgeBase.chunkingConfig.strategyOptions.pattern,
37-
separators: knowledgeBase.chunkingConfig.strategyOptions.separators,
38-
recipe: knowledgeBase.chunkingConfig.strategyOptions.recipe,
39-
strictBoundaries: knowledgeBase.chunkingConfig.strategyOptions.strictBoundaries,
40-
}
41-
: undefined,
42-
},
43-
docCount: knowledgeBase.docCount,
44-
connectorTypes: knowledgeBase.connectorTypes,
45-
createdAt: knowledgeBase.createdAt.toISOString(),
46-
updatedAt: knowledgeBase.updatedAt.toISOString(),
47-
folderPath,
48-
}
49-
}
50-
5121
/** GET /api/v2/knowledge/[id] — Get knowledge base details. */
5222
export const GET = defineV2JsonRoute({
5323
contract: v2GetKnowledgeBaseContract,
@@ -60,8 +30,8 @@ export const GET = defineV2JsonRoute({
6030
assertedWorkspaceId: query.workspaceId,
6131
}),
6232
useCase: readKnowledgeBase,
63-
present: ({ knowledgeBase, folderPath }) => ({
64-
data: { knowledgeBase: toV2KnowledgeBase(knowledgeBase, folderPath) },
33+
present: async ({ knowledgeBase, folderPath }) => ({
34+
data: { knowledgeBase: await toV2KnowledgeBase(knowledgeBase, folderPath) },
6535
}),
6636
})
6737

@@ -85,8 +55,8 @@ export const PUT = defineV2JsonRoute({
8555
source: 'api',
8656
}),
8757
useCase: updateKnowledgeBaseOperation,
88-
present: ({ knowledgeBase, folderPath }) => ({
89-
data: { knowledgeBase: toV2KnowledgeBase(knowledgeBase, folderPath) },
58+
present: async ({ knowledgeBase, folderPath }) => ({
59+
data: { knowledgeBase: await toV2KnowledgeBase(knowledgeBase, folderPath) },
9060
}),
9161
})
9262

apps/sim/app/api/v2/knowledge/route.test.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ const {
1212
mockCreate,
1313
mockPlatformCreated,
1414
mockCapture,
15+
mockGetUserEmailsByIds,
1516
} = vi.hoisted(() => ({
1617
mockAuthenticate: vi.fn(),
1718
mockCheckPreAuth: vi.fn(),
@@ -20,6 +21,7 @@ const {
2021
mockCreate: vi.fn(),
2122
mockPlatformCreated: vi.fn(),
2223
mockCapture: vi.fn(),
24+
mockGetUserEmailsByIds: vi.fn(),
2325
}))
2426

2527
vi.mock('@/lib/api/server/routes/v2-api-key-auth', () => ({
@@ -52,6 +54,10 @@ vi.mock('@/lib/core/telemetry', () => ({
5254
}))
5355

5456
vi.mock('@/lib/posthog/server', () => ({ captureServerEvent: mockCapture }))
57+
vi.mock('@/lib/users/queries', () => ({
58+
getUserEmailsByIds: mockGetUserEmailsByIds,
59+
requireResolvedUserEmail: (emails: Map<string, string>, userId: string) => emails.get(userId)!,
60+
}))
5561

5662
import { GET, POST } from '@/app/api/v2/knowledge/route'
5763

@@ -95,6 +101,7 @@ describe('/api/v2/knowledge route composition', () => {
95101
rateLimitSubscription: null,
96102
keyType: 'personal',
97103
})
104+
mockGetUserEmailsByIds.mockResolvedValue(new Map([['user-1', 'owner@example.com']]))
98105
mockList.mockResolvedValue({
99106
knowledgeBases: [{ knowledgeBase: buildKnowledgeBase(), folderPath: '/' }],
100107
})
@@ -126,6 +133,7 @@ describe('/api/v2/knowledge route composition', () => {
126133
expect.objectContaining({
127134
id: 'kb-1',
128135
folderPath: '/',
136+
ownerEmail: 'owner@example.com',
129137
connectorTypes: ['notion'],
130138
createdAt: '2024-01-01T00:00:00.000Z',
131139
}),
@@ -144,6 +152,7 @@ describe('/api/v2/knowledge route composition', () => {
144152
const response = await POST(request)
145153

146154
expect(response.status).toBe(201)
155+
expect((await response.clone().json()).data.knowledgeBase.ownerEmail).toBe('owner@example.com')
147156
expect(mockCreate).toHaveBeenCalledWith({
148157
principal: { kind: 'personal_api_key', userId: 'user-1', keyId: 'key-1' },
149158
input: {

apps/sim/app/api/v2/knowledge/route.ts

Lines changed: 5 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -14,43 +14,13 @@ import {
1414
listKnowledgeBases,
1515
} from '@/lib/knowledge/application/knowledge-bases'
1616
import { knowledgeOperations } from '@/lib/knowledge/application/operations'
17-
import type { KnowledgeBaseWithCounts } from '@/lib/knowledge/types'
1817
import { captureServerEvent } from '@/lib/posthog/server'
18+
import { toV2KnowledgeBase, toV2KnowledgeBases } from '@/app/api/v2/knowledge/utils'
1919
import { v2Error } from '@/app/api/v2/lib/response'
2020

2121
export const dynamic = 'force-dynamic'
2222
export const revalidate = 0
2323

24-
function toV2KnowledgeBase(knowledgeBase: KnowledgeBaseWithCounts, folderPath: string) {
25-
return {
26-
id: knowledgeBase.id,
27-
name: knowledgeBase.name,
28-
description: knowledgeBase.description,
29-
tokenCount: knowledgeBase.tokenCount,
30-
embeddingModel: knowledgeBase.embeddingModel,
31-
embeddingDimension: knowledgeBase.embeddingDimension,
32-
chunkingConfig: {
33-
maxSize: knowledgeBase.chunkingConfig.maxSize,
34-
minSize: knowledgeBase.chunkingConfig.minSize,
35-
overlap: knowledgeBase.chunkingConfig.overlap,
36-
strategy: knowledgeBase.chunkingConfig.strategy,
37-
strategyOptions: knowledgeBase.chunkingConfig.strategyOptions
38-
? {
39-
pattern: knowledgeBase.chunkingConfig.strategyOptions.pattern,
40-
separators: knowledgeBase.chunkingConfig.strategyOptions.separators,
41-
recipe: knowledgeBase.chunkingConfig.strategyOptions.recipe,
42-
strictBoundaries: knowledgeBase.chunkingConfig.strategyOptions.strictBoundaries,
43-
}
44-
: undefined,
45-
},
46-
docCount: knowledgeBase.docCount,
47-
connectorTypes: knowledgeBase.connectorTypes,
48-
createdAt: knowledgeBase.createdAt.toISOString(),
49-
updatedAt: knowledgeBase.updatedAt.toISOString(),
50-
folderPath,
51-
}
52-
}
53-
5424
/** GET /api/v2/knowledge — List knowledge bases in a workspace. */
5525
export const GET = defineV2JsonRoute({
5626
contract: v2ListKnowledgeBasesContract,
@@ -66,10 +36,8 @@ export const GET = defineV2JsonRoute({
6636
sortOrder: query.sortOrder,
6737
}),
6838
useCase: listKnowledgeBases,
69-
present: ({ knowledgeBases }) => ({
70-
data: knowledgeBases.map(({ knowledgeBase, folderPath }) =>
71-
toV2KnowledgeBase(knowledgeBase, folderPath)
72-
),
39+
present: async ({ knowledgeBases }) => ({
40+
data: await toV2KnowledgeBases(knowledgeBases),
7341
nextCursor: null,
7442
}),
7543
})
@@ -117,7 +85,7 @@ export const POST = defineV2JsonRoute({
11785
)
11886
}
11987
},
120-
present: ({ knowledgeBase, folderPath }) => ({
121-
data: { knowledgeBase: toV2KnowledgeBase(knowledgeBase, folderPath) },
88+
present: async ({ knowledgeBase, folderPath }) => ({
89+
data: { knowledgeBase: await toV2KnowledgeBase(knowledgeBase, folderPath) },
12290
}),
12391
})
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
import type { V2KnowledgeBase } from '@/lib/api/contracts/v2/knowledge'
2+
import type { KnowledgeBaseWithCounts } from '@/lib/knowledge/types'
3+
import { getUserEmailsByIds, requireResolvedUserEmail } from '@/lib/users/queries'
4+
5+
interface KnowledgeBaseWithFolder {
6+
knowledgeBase: KnowledgeBaseWithCounts
7+
folderPath: string
8+
}
9+
10+
function serializeV2KnowledgeBase(
11+
knowledgeBase: KnowledgeBaseWithCounts,
12+
folderPath: string,
13+
ownerEmail: string
14+
): V2KnowledgeBase {
15+
return {
16+
id: knowledgeBase.id,
17+
name: knowledgeBase.name,
18+
description: knowledgeBase.description,
19+
ownerEmail,
20+
tokenCount: knowledgeBase.tokenCount,
21+
embeddingModel: knowledgeBase.embeddingModel,
22+
embeddingDimension: knowledgeBase.embeddingDimension,
23+
chunkingConfig: {
24+
maxSize: knowledgeBase.chunkingConfig.maxSize,
25+
minSize: knowledgeBase.chunkingConfig.minSize,
26+
overlap: knowledgeBase.chunkingConfig.overlap,
27+
strategy: knowledgeBase.chunkingConfig.strategy,
28+
strategyOptions: knowledgeBase.chunkingConfig.strategyOptions
29+
? {
30+
pattern: knowledgeBase.chunkingConfig.strategyOptions.pattern,
31+
separators: knowledgeBase.chunkingConfig.strategyOptions.separators,
32+
recipe: knowledgeBase.chunkingConfig.strategyOptions.recipe,
33+
strictBoundaries: knowledgeBase.chunkingConfig.strategyOptions.strictBoundaries,
34+
}
35+
: undefined,
36+
},
37+
docCount: knowledgeBase.docCount,
38+
connectorTypes: knowledgeBase.connectorTypes,
39+
createdAt: knowledgeBase.createdAt.toISOString(),
40+
updatedAt: knowledgeBase.updatedAt.toISOString(),
41+
folderPath,
42+
}
43+
}
44+
45+
/** Resolves and serializes one knowledge base with public owner attribution. */
46+
export async function toV2KnowledgeBase(
47+
knowledgeBase: KnowledgeBaseWithCounts,
48+
folderPath: string
49+
): Promise<V2KnowledgeBase> {
50+
const emailByUserId = await getUserEmailsByIds([knowledgeBase.userId])
51+
return serializeV2KnowledgeBase(
52+
knowledgeBase,
53+
folderPath,
54+
requireResolvedUserEmail(emailByUserId, knowledgeBase.userId)
55+
)
56+
}
57+
58+
/** Batch-resolves owner emails before serializing a knowledge-base list. */
59+
export async function toV2KnowledgeBases(
60+
entries: readonly KnowledgeBaseWithFolder[]
61+
): Promise<V2KnowledgeBase[]> {
62+
const emailByUserId = await getUserEmailsByIds(
63+
entries.map(({ knowledgeBase }) => knowledgeBase.userId)
64+
)
65+
return entries.map(({ knowledgeBase, folderPath }) =>
66+
serializeV2KnowledgeBase(
67+
knowledgeBase,
68+
folderPath,
69+
requireResolvedUserEmail(emailByUserId, knowledgeBase.userId)
70+
)
71+
)
72+
}

apps/sim/app/api/v2/tables/[tableId]/route.test.ts

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ const mocks = vi.hoisted(() => ({
1414
update: vi.fn(),
1515
remove: vi.fn(),
1616
capture: vi.fn(),
17+
getUserEmailsByIds: vi.fn(),
1718
}))
1819

1920
vi.mock('@/lib/api/server/routes/v2-api-key-auth', () => ({
@@ -34,6 +35,10 @@ vi.mock('@/lib/table/application/tables', () => ({
3435
updateTableUseCase: { operation: { id: 'tables.update' }, execute: mocks.update },
3536
deleteTableUseCase: { operation: { id: 'tables.delete' }, execute: mocks.remove },
3637
}))
38+
vi.mock('@/lib/users/queries', () => ({
39+
getUserEmailsByIds: mocks.getUserEmailsByIds,
40+
requireResolvedUserEmail: (emails: Map<string, string>, userId: string) => emails.get(userId)!,
41+
}))
3742

3843
import { OrchestrationError } from '@/lib/core/orchestration/types'
3944
import { DELETE, GET, PATCH } from '@/app/api/v2/tables/[tableId]/route'
@@ -60,7 +65,7 @@ const rate = {
6065
const table = {
6166
id: 'table-1',
6267
workspaceId: WORKSPACE_ID,
63-
userId: 'owner-1',
68+
createdBy: 'owner-1',
6469
name: 'Contacts',
6570
description: null,
6671
schema: {
@@ -101,6 +106,7 @@ describe('/api/v2/tables/[tableId]', () => {
101106
mocks.preauthRate.mockResolvedValue(rate)
102107
mocks.operationRate.mockResolvedValue(rate)
103108
mocks.gate.mockResolvedValue(null)
109+
mocks.getUserEmailsByIds.mockResolvedValue(new Map([['owner-1', 'owner@example.com']]))
104110
mocks.read.mockResolvedValue({ table, folderPath: '/' })
105111
mocks.update.mockResolvedValue({
106112
table,
@@ -123,7 +129,10 @@ describe('/api/v2/tables/[tableId]', () => {
123129
const response = await GET(req, context)
124130

125131
expect(response.status).toBe(200)
126-
expect((await response.json()).data.table.id).toBe('table-1')
132+
expect((await response.json()).data.table).toMatchObject({
133+
id: 'table-1',
134+
ownerEmail: 'owner@example.com',
135+
})
127136
expect(mocks.read).toHaveBeenCalledWith({
128137
principal,
129138
input: { tableId: 'table-1', workspaceId: WORKSPACE_ID },
@@ -138,7 +147,10 @@ describe('/api/v2/tables/[tableId]', () => {
138147
)
139148

140149
expect(response.status).toBe(200)
141-
expect((await response.json()).data.table.name).toBe('Contacts')
150+
expect((await response.json()).data.table).toMatchObject({
151+
name: 'Contacts',
152+
ownerEmail: 'owner@example.com',
153+
})
142154
})
143155

144156
it('reports committed fields when a later composite PATCH step fails', async () => {

0 commit comments

Comments
 (0)