Skip to content

Commit 6231972

Browse files
cursoragentgeclos
andcommitted
feat: add ChSqlClient and withPostgres/withClickHouse helpers
Introduces ChSqlClient as the ClickHouse counterpart to SqlClient: - ChSqlClient domain interface in @domain/shared (pass-through transaction, direct query execution) - ChSqlClientLive implementation in @platform/db-clickhouse - DatasetRowRepositoryLive migrated from closure-captured client to ChSqlClient service pattern (Layer.effect + yield* ChSqlClient) Adds withPostgres and withClickHouse helpers that bundle repository layers with their database client in a single call: - Uses Layer.provideMerge so the SqlClient/ChSqlClient service is available both to the repo layers AND to the outer effect (needed for use-case-level sqlClient.transaction() calls) - Repos sharing the same helper call share the same client instance Boundary callers updated from: Effect.provide(RepoLive), Effect.provide(SqlClientLive(client, orgId)), To: Effect.provide(withPostgres(client, orgId, RepoLive)), Co-authored-by: Gerard <gerard@latitude.so>
1 parent e957676 commit 6231972

File tree

16 files changed

+247
-191
lines changed

16 files changed

+247
-191
lines changed

apps/api/src/routes/api-keys.ts

Lines changed: 4 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import {
88
import { ApiKeyId } from "@domain/shared"
99
import { OpenAPIHono, createRoute, z } from "@hono/zod-openapi"
1010
import type { RedisClient } from "@platform/cache-redis"
11-
import { ApiKeyRepositoryLive, SqlClientLive } from "@platform/db-postgres"
11+
import { ApiKeyRepositoryLive, withPostgres } from "@platform/db-postgres"
1212
import { Effect } from "effect"
1313
import { ErrorSchema, OrgAndIdParamsSchema, OrgParamsSchema, PROTECTED_SECURITY } from "../openapi/schemas.ts"
1414
import type { OrganizationScopedEnv } from "../types.ts"
@@ -175,8 +175,7 @@ export const createApiKeysRoutes = () => {
175175

176176
const apiKey = await Effect.runPromise(
177177
generateApiKeyUseCase({ name }).pipe(
178-
Effect.provide(ApiKeyRepositoryLive),
179-
Effect.provide(SqlClientLive(c.var.postgresClient, c.var.organization.id)),
178+
Effect.provide(withPostgres(c.var.postgresClient, c.var.organization.id, ApiKeyRepositoryLive)),
180179
),
181180
)
182181
return c.json(toApiKeyResponse(apiKey), 201)
@@ -187,10 +186,7 @@ export const createApiKeysRoutes = () => {
187186
Effect.gen(function* () {
188187
const repo = yield* ApiKeyRepository
189188
return yield* repo.findAll()
190-
}).pipe(
191-
Effect.provide(ApiKeyRepositoryLive),
192-
Effect.provide(SqlClientLive(c.var.postgresClient, c.var.organization.id)),
193-
),
189+
}).pipe(Effect.provide(withPostgres(c.var.postgresClient, c.var.organization.id, ApiKeyRepositoryLive))),
194190
)
195191
return c.json({ apiKeys: apiKeys.map(toApiKeyListItemResponse) }, 200)
196192
})
@@ -200,9 +196,8 @@ export const createApiKeysRoutes = () => {
200196

201197
await Effect.runPromise(
202198
revokeApiKeyUseCase({ id: ApiKeyId(idParam) }).pipe(
203-
Effect.provide(ApiKeyRepositoryLive),
204199
Effect.provideService(ApiKeyCacheInvalidator, createApiKeyCacheInvalidator(c.var.redis)),
205-
Effect.provide(SqlClientLive(c.var.postgresClient, c.var.organization.id)),
200+
Effect.provide(withPostgres(c.var.postgresClient, c.var.organization.id, ApiKeyRepositoryLive)),
206201
),
207202
)
208203
return c.body(null, 204)

apps/api/src/routes/projects.ts

Lines changed: 6 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import {
77
} from "@domain/projects"
88
import { ProjectId } from "@domain/shared"
99
import { OpenAPIHono, createRoute, z } from "@hono/zod-openapi"
10-
import { ProjectRepositoryLive, SqlClientLive } from "@platform/db-postgres"
10+
import { ProjectRepositoryLive, withPostgres } from "@platform/db-postgres"
1111
import { Effect } from "effect"
1212
import { ErrorSchema, OrgAndIdParamsSchema, OrgParamsSchema, PROTECTED_SECURITY } from "../openapi/schemas.ts"
1313
import type { OrganizationScopedEnv } from "../types.ts"
@@ -220,8 +220,7 @@ export const createProjectsRoutes = () => {
220220

221221
const project = await Effect.runPromise(
222222
createProjectUseCase(input).pipe(
223-
Effect.provide(ProjectRepositoryLive),
224-
Effect.provide(SqlClientLive(c.var.postgresClient, c.var.organization.id)),
223+
Effect.provide(withPostgres(c.var.postgresClient, c.var.organization.id, ProjectRepositoryLive)),
225224
),
226225
)
227226
return c.json(toProjectResponse(project), 201)
@@ -232,10 +231,7 @@ export const createProjectsRoutes = () => {
232231
Effect.gen(function* () {
233232
const repo = yield* ProjectRepository
234233
return yield* repo.findAll()
235-
}).pipe(
236-
Effect.provide(ProjectRepositoryLive),
237-
Effect.provide(SqlClientLive(c.var.postgresClient, c.var.organization.id)),
238-
),
234+
}).pipe(Effect.provide(withPostgres(c.var.postgresClient, c.var.organization.id, ProjectRepositoryLive))),
239235
)
240236

241237
return c.json({ projects: projects.map(toProjectResponse) }, 200)
@@ -249,10 +245,7 @@ export const createProjectsRoutes = () => {
249245
Effect.gen(function* () {
250246
const repo = yield* ProjectRepository
251247
return yield* repo.findById(id)
252-
}).pipe(
253-
Effect.provide(ProjectRepositoryLive),
254-
Effect.provide(SqlClientLive(c.var.postgresClient, c.var.organization.id)),
255-
),
248+
}).pipe(Effect.provide(withPostgres(c.var.postgresClient, c.var.organization.id, ProjectRepositoryLive))),
256249
)
257250

258251
return c.json(toProjectResponse(project), 200)
@@ -268,10 +261,7 @@ export const createProjectsRoutes = () => {
268261
id,
269262
...(body.name !== undefined ? { name: body.name } : {}),
270263
...(body.description !== undefined ? { description: body.description } : {}),
271-
}).pipe(
272-
Effect.provide(ProjectRepositoryLive),
273-
Effect.provide(SqlClientLive(c.var.postgresClient, c.var.organization.id)),
274-
),
264+
}).pipe(Effect.provide(withPostgres(c.var.postgresClient, c.var.organization.id, ProjectRepositoryLive))),
275265
)
276266

277267
return c.json(toProjectResponse(updatedProject), 200)
@@ -285,10 +275,7 @@ export const createProjectsRoutes = () => {
285275
Effect.gen(function* () {
286276
const repo = yield* ProjectRepository
287277
return yield* repo.softDelete(id)
288-
}).pipe(
289-
Effect.provide(ProjectRepositoryLive),
290-
Effect.provide(SqlClientLive(c.var.postgresClient, c.var.organization.id)),
291-
),
278+
}).pipe(Effect.provide(withPostgres(c.var.postgresClient, c.var.organization.id, ProjectRepositoryLive))),
292279
)
293280
return c.body(null, 204)
294281
})

apps/web/src/domains/api-keys/api-keys.functions.ts

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { type ApiKey, ApiKeyRepository, generateApiKeyUseCase, updateApiKeyUseCase } from "@domain/api-keys"
22
import { ApiKeyId } from "@domain/shared"
3-
import { ApiKeyRepositoryLive, SqlClientLive } from "@platform/db-postgres"
3+
import { ApiKeyRepositoryLive, withPostgres } from "@platform/db-postgres"
44
import { createServerFn } from "@tanstack/react-start"
55
import { Effect } from "effect"
66
import { z } from "zod"
@@ -38,7 +38,7 @@ export const listApiKeys = createServerFn({ method: "GET" })
3838
Effect.gen(function* () {
3939
const repo = yield* ApiKeyRepository
4040
return yield* repo.findAll()
41-
}).pipe(Effect.provide(ApiKeyRepositoryLive), Effect.provide(SqlClientLive(client, organizationId))),
41+
}).pipe(Effect.provide(withPostgres(client, organizationId, ApiKeyRepositoryLive))),
4242
)
4343

4444
return apiKeys.map(toRecord)
@@ -53,8 +53,7 @@ export const createApiKey = createServerFn({ method: "POST" })
5353

5454
const apiKey = await Effect.runPromise(
5555
generateApiKeyUseCase({ name: data.name }).pipe(
56-
Effect.provide(ApiKeyRepositoryLive),
57-
Effect.provide(SqlClientLive(client, organizationId)),
56+
Effect.provide(withPostgres(client, organizationId, ApiKeyRepositoryLive)),
5857
),
5958
)
6059

@@ -70,8 +69,7 @@ export const updateApiKey = createServerFn({ method: "POST" })
7069

7170
const apiKey = await Effect.runPromise(
7271
updateApiKeyUseCase({ id: ApiKeyId(data.id), name: data.name }).pipe(
73-
Effect.provide(ApiKeyRepositoryLive),
74-
Effect.provide(SqlClientLive(client, organizationId)),
72+
Effect.provide(withPostgres(client, organizationId, ApiKeyRepositoryLive)),
7573
),
7674
)
7775

@@ -89,6 +87,6 @@ export const deleteApiKey = createServerFn({ method: "POST" })
8987
Effect.gen(function* () {
9088
const repo = yield* ApiKeyRepository
9189
yield* repo.delete(ApiKeyId(data.id))
92-
}).pipe(Effect.provide(ApiKeyRepositoryLive), Effect.provide(SqlClientLive(client, organizationId))),
90+
}).pipe(Effect.provide(withPostgres(client, organizationId, ApiKeyRepositoryLive))),
9391
)
9492
})

apps/web/src/domains/auth/auth.functions.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import {
1313
OrganizationRepositoryLive,
1414
SqlClientLive,
1515
UserRepositoryLive,
16+
withPostgres,
1617
} from "@platform/db-postgres"
1718
import { createServerFn } from "@tanstack/react-start"
1819
import { Effect } from "effect"
@@ -159,8 +160,7 @@ export const exchangeCliSession = createServerFn({ method: "POST" })
159160
const createdAt = new Date().toISOString().slice(0, 10) // YYYY-MM-DD
160161
const apiKey = await Effect.runPromise(
161162
generateApiKeyUseCase({ name: `CLI (${createdAt})` }).pipe(
162-
Effect.provide(ApiKeyRepositoryLive),
163-
Effect.provide(SqlClientLive(adminClient, OrganizationId(activeOrganizationId))),
163+
Effect.provide(withPostgres(adminClient, OrganizationId(activeOrganizationId), ApiKeyRepositoryLive)),
164164
),
165165
)
166166

apps/web/src/domains/datasets/datasets.functions.ts

Lines changed: 20 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
import type { Dataset, DatasetRow } from "@domain/datasets"
22
import { DatasetRepository, createDataset, insertRows, listDatasets, listRows } from "@domain/datasets"
33
import { DatasetId, DatasetVersionId, OrganizationId, ProjectId, putInDisk } from "@domain/shared"
4-
import { DatasetRowRepositoryLive } from "@platform/db-clickhouse"
5-
import { DatasetRepositoryLive, SqlClientLive } from "@platform/db-postgres"
4+
import { DatasetRowRepositoryLive, withClickHouse } from "@platform/db-clickhouse"
5+
import { DatasetRepositoryLive, withPostgres } from "@platform/db-postgres"
66
import { createServerFn } from "@tanstack/react-start"
77
import { Effect } from "effect"
88
import Papa from "papaparse"
@@ -127,17 +127,15 @@ export const listDatasetsQuery = createServerFn({ method: "GET" })
127127
.inputValidator(z.object({ projectId: z.string() }))
128128
.handler(async ({ data }): Promise<{ datasets: DatasetRecord[]; total: number }> => {
129129
const { organizationId } = await requireSession()
130-
const client = getPostgresClient()
131-
const chClient = getClickhouseClient()
130+
const orgId = OrganizationId(organizationId)
132131

133132
const result = await Effect.runPromise(
134133
listDatasets({
135-
organizationId: OrganizationId(organizationId),
134+
organizationId: orgId,
136135
projectId: ProjectId(data.projectId),
137136
}).pipe(
138-
Effect.provide(DatasetRepositoryLive),
139-
Effect.provide(DatasetRowRepositoryLive(chClient)),
140-
Effect.provide(SqlClientLive(client, OrganizationId(organizationId))),
137+
Effect.provide(withPostgres(getPostgresClient(), orgId, DatasetRepositoryLive)),
138+
Effect.provide(withClickHouse(getClickhouseClient(), orgId, DatasetRowRepositoryLive)),
141139
),
142140
)
143141

@@ -157,21 +155,19 @@ export const listRowsQuery = createServerFn({ method: "GET" })
157155
)
158156
.handler(async ({ data }): Promise<{ rows: DatasetRowRecord[]; total: number }> => {
159157
const { organizationId } = await requireSession()
160-
const client = getPostgresClient()
161-
const chClient = getClickhouseClient()
158+
const orgId = OrganizationId(organizationId)
162159

163160
const result = await Effect.runPromise(
164161
listRows({
165-
organizationId: OrganizationId(organizationId),
162+
organizationId: orgId,
166163
datasetId: DatasetId(data.datasetId),
167164
...(data.versionId ? { versionId: DatasetVersionId(data.versionId) } : {}),
168165
...(data.search ? { search: data.search } : {}),
169166
limit: data.limit,
170167
offset: data.offset,
171168
}).pipe(
172-
Effect.provide(DatasetRepositoryLive),
173-
Effect.provide(DatasetRowRepositoryLive(chClient)),
174-
Effect.provide(SqlClientLive(client, OrganizationId(organizationId))),
169+
Effect.provide(withPostgres(getPostgresClient(), orgId, DatasetRepositoryLive)),
170+
Effect.provide(withClickHouse(getClickhouseClient(), orgId, DatasetRowRepositoryLive)),
175171
),
176172
)
177173

@@ -183,18 +179,16 @@ export const createDatasetMutation = createServerFn({ method: "POST" })
183179
.inputValidator(z.object({ projectId: z.string(), name: z.string().min(1) }))
184180
.handler(async ({ data }): Promise<DatasetRecord> => {
185181
const { organizationId } = await requireSession()
186-
const client = getPostgresClient()
187-
const chClient = getClickhouseClient()
182+
const orgId = OrganizationId(organizationId)
188183

189184
const dataset = await Effect.runPromise(
190185
createDataset({
191-
organizationId: OrganizationId(organizationId),
186+
organizationId: orgId,
192187
projectId: ProjectId(data.projectId),
193188
name: data.name,
194189
}).pipe(
195-
Effect.provide(DatasetRepositoryLive),
196-
Effect.provide(DatasetRowRepositoryLive(chClient)),
197-
Effect.provide(SqlClientLive(client, OrganizationId(organizationId))),
190+
Effect.provide(withPostgres(getPostgresClient(), orgId, DatasetRepositoryLive)),
191+
Effect.provide(withClickHouse(getClickhouseClient(), orgId, DatasetRowRepositoryLive)),
198192
),
199193
)
200194

@@ -209,8 +203,7 @@ export const saveDatasetCsv = createServerFn({ method: "POST" })
209203
})
210204
.handler(async ({ data: formData }): Promise<{ version: number; rowCount: number }> => {
211205
const { organizationId } = await requireSession()
212-
const client = getPostgresClient()
213-
const chClient = getClickhouseClient()
206+
const orgId = OrganizationId(organizationId)
214207

215208
const file = formData.get("file")
216209
const datasetId = formData.get("datasetId")
@@ -232,7 +225,7 @@ export const saveDatasetCsv = createServerFn({ method: "POST" })
232225
const fileKey = await Effect.runPromise(
233226
putInDisk(getStorageDisk(), {
234227
namespace: "datasets",
235-
organizationId: OrganizationId(organizationId),
228+
organizationId: orgId,
236229
projectId: ProjectId(projectId),
237230
content,
238231
}),
@@ -242,11 +235,7 @@ export const saveDatasetCsv = createServerFn({ method: "POST" })
242235
Effect.gen(function* () {
243236
const repo = yield* DatasetRepository
244237
return yield* repo.updateFileKey({ id: DatasetId(datasetId), fileKey })
245-
}).pipe(
246-
Effect.provide(DatasetRepositoryLive),
247-
Effect.provide(DatasetRowRepositoryLive(chClient)),
248-
Effect.provide(SqlClientLive(client, OrganizationId(organizationId))),
249-
),
238+
}).pipe(Effect.provide(withPostgres(getPostgresClient(), orgId, DatasetRepositoryLive))),
250239
)
251240

252241
const parsed = Papa.parse<Record<string, string>>(content, { header: true, skipEmptyLines: true })
@@ -258,14 +247,13 @@ export const saveDatasetCsv = createServerFn({ method: "POST" })
258247

259248
const result = await Effect.runPromise(
260249
insertRows({
261-
organizationId: OrganizationId(organizationId),
250+
organizationId: orgId,
262251
datasetId: DatasetId(datasetId),
263252
rows: mappedRows,
264253
source: "csv",
265254
}).pipe(
266-
Effect.provide(DatasetRepositoryLive),
267-
Effect.provide(DatasetRowRepositoryLive(chClient)),
268-
Effect.provide(SqlClientLive(client, OrganizationId(organizationId))),
255+
Effect.provide(withPostgres(getPostgresClient(), orgId, DatasetRepositoryLive)),
256+
Effect.provide(withClickHouse(getClickhouseClient(), orgId, DatasetRowRepositoryLive)),
269257
),
270258
)
271259

apps/web/src/domains/members/members.functions.ts

Lines changed: 5 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,8 @@ import {
44
AuthIntentRepositoryLive,
55
MembershipRepositoryLive,
66
OrganizationRepositoryLive,
7-
SqlClientLive,
87
UserRepositoryLive,
8+
withPostgres,
99
} from "@platform/db-postgres"
1010
import { createServerFn } from "@tanstack/react-start"
1111
import { Effect } from "effect"
@@ -42,11 +42,7 @@ export const listMembers = createServerFn({ method: "GET" })
4242
const pendingInvites = yield* intentRepo.findPendingInvitesByOrganizationId(organizationId)
4343

4444
return [members, pendingInvites] as const
45-
}).pipe(
46-
Effect.provide(MembershipRepositoryLive),
47-
Effect.provide(AuthIntentRepositoryLive),
48-
Effect.provide(SqlClientLive(client, organizationId)),
49-
),
45+
}).pipe(Effect.provide(withPostgres(client, organizationId, MembershipRepositoryLive, AuthIntentRepositoryLive))),
5046
)
5147

5248
const activeMembers: MemberRecord[] = members.map((m) => ({
@@ -90,7 +86,7 @@ export const inviteMember = createServerFn({ method: "POST" })
9086
Effect.gen(function* () {
9187
const repo = yield* OrganizationRepository
9288
return yield* repo.findById(organizationId)
93-
}).pipe(Effect.provide(OrganizationRepositoryLive), Effect.provide(SqlClientLive(client, organizationId))),
89+
}).pipe(Effect.provide(withPostgres(client, organizationId, OrganizationRepositoryLive))),
9490
)
9591
const organizationName = org.name
9692

@@ -100,11 +96,7 @@ export const inviteMember = createServerFn({ method: "POST" })
10096
organizationId,
10197
organizationName,
10298
inviterName,
103-
}).pipe(
104-
Effect.provide(AuthIntentRepositoryLive),
105-
Effect.provide(UserRepositoryLive),
106-
Effect.provide(SqlClientLive(client, organizationId)),
107-
),
99+
}).pipe(Effect.provide(withPostgres(client, organizationId, AuthIntentRepositoryLive, UserRepositoryLive))),
108100
)
109101

110102
return { intentId: intent.id }
@@ -121,6 +113,6 @@ export const removeMember = createServerFn({ method: "POST" })
121113
removeMemberUseCase({
122114
membershipId: data.membershipId,
123115
requestingUserId: userId,
124-
}).pipe(Effect.provide(MembershipRepositoryLive), Effect.provide(SqlClientLive(client, organizationId))),
116+
}).pipe(Effect.provide(withPostgres(client, organizationId, MembershipRepositoryLive))),
125117
)
126118
})

apps/web/src/domains/organizations/organizations.functions.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
11
import { MembershipRepository, OrganizationRepository } from "@domain/organizations"
2-
import { MembershipRepositoryLive, OrganizationRepositoryLive, SqlClientLive } from "@platform/db-postgres"
2+
import {
3+
MembershipRepositoryLive,
4+
OrganizationRepositoryLive,
5+
SqlClientLive,
6+
withPostgres,
7+
} from "@platform/db-postgres"
38
import { createServerFn } from "@tanstack/react-start"
49
import { Effect } from "effect"
510
import { requireSession } from "../../server/auth.ts"
@@ -36,7 +41,7 @@ export const getOrganization = createServerFn({ method: "GET" })
3641
Effect.gen(function* () {
3742
const repo = yield* OrganizationRepository
3843
return yield* repo.findById(organizationId)
39-
}).pipe(Effect.provide(OrganizationRepositoryLive), Effect.provide(SqlClientLive(client, organizationId))),
44+
}).pipe(Effect.provide(withPostgres(client, organizationId, OrganizationRepositoryLive))),
4045
)
4146
return { id: org.id, name: org.name }
4247
})

0 commit comments

Comments
 (0)