Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions .changeset/defer-adapter-validation.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
---
"payloadcms-vectorize": patch
"@payloadcms-vectorize/pg": patch
"@payloadcms-vectorize/cf": patch
"@payloadcms-vectorize/mongodb": patch
---

Defer adapter config validation to call-time instead of throwing at construction.

The mongodb and cf adapter factories previously threw on missing config (e.g. `uri`, `dbName`, `binding`) the moment they were called, which happens while the Payload config is being built. That broke `payload generate:types` and `generate:importmap` in environments without runtime variables — such as CI that builds `payload-types` to publish as a separate package. Validation now runs when an adapter method is actually invoked, so config-time codegen no longer requires runtime credentials. Valid configurations behave exactly as before.

Closes #64.
16 changes: 14 additions & 2 deletions adapters/cf/dev/specs/adapter.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -93,13 +93,25 @@ function createMockPayload(mockBinding: any, overrides: Record<string, any> = {}

describe('createCloudflareVectorizeIntegration', () => {
describe('validation', () => {
test('should throw if vectorize binding is missing', () => {
test('should NOT throw at construction if vectorize binding is missing', () => {
expect(() => {
createCloudflareVectorizeIntegration({
config: { default: { dims: 384 } },
binding: undefined as any,
})
}).toThrow('Cloudflare Vectorize binding is required')
}).not.toThrow()
})

test('should throw at call-time if vectorize binding is missing', async () => {
const { adapter } = createCloudflareVectorizeIntegration({
config: { default: { dims: 384 } },
binding: undefined as any,
})
const payloadWithoutBinding = createMockPayload(undefined)

await expect(
adapter.deleteChunks(payloadWithoutBinding, 'default', 'col', 'doc-1'),
).rejects.toThrow('Cloudflare Vectorize binding not found')
})

test('should create integration with valid config', () => {
Expand Down
4 changes: 0 additions & 4 deletions adapters/cf/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,10 +39,6 @@ interface CloudflareVectorizeConfig {
export const createCloudflareVectorizeIntegration = (
options: CloudflareVectorizeConfig,
): { adapter: DbAdapter } => {
if (!options.binding) {
throw new Error('[@payloadcms-vectorize/cf] Cloudflare Vectorize binding is required')
}

const poolConfig = options.config

const adapter: DbAdapter = {
Expand Down
88 changes: 88 additions & 0 deletions adapters/mongodb/dev/specs/validation.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
import { describe, expect, test } from 'vitest'
import type { BasePayload } from 'payload'
import { createMongoVectorIntegration } from '../../src/index.js'
import type { MongoVectorIntegrationConfig } from '../../src/types.js'

const VALID: MongoVectorIntegrationConfig = {
uri: 'mongodb://localhost:27017',
dbName: 'test',
pools: { default: { dimensions: 8 } },
}

const dummyPayload = {} as BasePayload

describe('createMongoVectorIntegration validation is deferred to call-time', () => {
describe('construction does not throw on missing/invalid config', () => {
test('missing uri', () => {
expect(() =>
createMongoVectorIntegration({ ...VALID, uri: undefined as any }),
).not.toThrow()
})

test('missing dbName', () => {
expect(() =>
createMongoVectorIntegration({ ...VALID, dbName: undefined as any }),
).not.toThrow()
})

test('empty pools', () => {
expect(() => createMongoVectorIntegration({ ...VALID, pools: {} })).not.toThrow()
})

test('missing pools', () => {
expect(() =>
createMongoVectorIntegration({ ...VALID, pools: undefined as any }),
).not.toThrow()
})

test('invalid dimensions', () => {
expect(() =>
createMongoVectorIntegration({ ...VALID, pools: { default: { dimensions: 0 } } }),
).not.toThrow()
})
})

describe('getConfigExtension does not throw at config-build time', () => {
test('with fully missing config', () => {
const { adapter } = createMongoVectorIntegration({
uri: undefined as any,
dbName: undefined as any,
pools: undefined as any,
})
expect(() => adapter.getConfigExtension({} as any)).not.toThrow()
})
})

describe('adapter methods throw at call-time when config is missing/invalid', () => {
test('missing uri', async () => {
const { adapter } = createMongoVectorIntegration({ ...VALID, uri: undefined as any })
await expect(
adapter.hasEmbeddingVersion(dummyPayload, 'default', 'col', 'doc-1', 'v1'),
).rejects.toThrow(/`uri` is required/)
})

test('missing dbName', async () => {
const { adapter } = createMongoVectorIntegration({ ...VALID, dbName: undefined as any })
await expect(
adapter.hasEmbeddingVersion(dummyPayload, 'default', 'col', 'doc-1', 'v1'),
).rejects.toThrow(/`dbName` is required/)
})

test('empty pools', async () => {
const { adapter } = createMongoVectorIntegration({ ...VALID, pools: {} })
await expect(
adapter.hasEmbeddingVersion(dummyPayload, 'default', 'col', 'doc-1', 'v1'),
).rejects.toThrow(/at least one pool/)
})

test('invalid dimensions', async () => {
const { adapter } = createMongoVectorIntegration({
...VALID,
pools: { default: { dimensions: 0 } },
})
await expect(
adapter.hasEmbeddingVersion(dummyPayload, 'default', 'col', 'doc-1', 'v1'),
).rejects.toThrow(/positive numeric `dimensions`/)
})
})
})
40 changes: 24 additions & 16 deletions adapters/mongodb/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,35 +17,42 @@ export type {
export const createMongoVectorIntegration = (
options: MongoVectorIntegrationConfig,
): { adapter: DbAdapter } => {
if (!options.uri) throw new Error('[@payloadcms-vectorize/mongodb] `uri` is required')
if (!options.dbName) throw new Error('[@payloadcms-vectorize/mongodb] `dbName` is required')
if (!options.pools || Object.keys(options.pools).length === 0) {
throw new Error('[@payloadcms-vectorize/mongodb] `pools` must contain at least one pool')
const resolvePools = (): Record<string, ResolvedPoolConfig> => {
const resolved: Record<string, ResolvedPoolConfig> = {}
for (const [name, p] of Object.entries(options.pools ?? {})) {
resolved[name] = resolvePoolConfig(name, p)
}
return resolved
}

const resolvedPools: Record<string, ResolvedPoolConfig> = {}
for (const [name, p] of Object.entries(options.pools)) {
if (typeof p.dimensions !== 'number' || p.dimensions <= 0) {
throw new Error(
`[@payloadcms-vectorize/mongodb] pool "${name}" requires a positive numeric \`dimensions\``,
)
const getCtx = () => {
if (!options.uri) throw new Error('[@payloadcms-vectorize/mongodb] `uri` is required')
if (!options.dbName) throw new Error('[@payloadcms-vectorize/mongodb] `dbName` is required')
if (!options.pools || Object.keys(options.pools).length === 0) {
throw new Error('[@payloadcms-vectorize/mongodb] `pools` must contain at least one pool')
}
resolvedPools[name] = resolvePoolConfig(name, p)
for (const [name, p] of Object.entries(options.pools)) {
if (typeof p.dimensions !== 'number' || p.dimensions <= 0) {
throw new Error(
`[@payloadcms-vectorize/mongodb] pool "${name}" requires a positive numeric \`dimensions\``,
)
}
}
return { uri: options.uri, dbName: options.dbName, pools: resolvePools() }
}

const ctx = { uri: options.uri, dbName: options.dbName, pools: resolvedPools }

const adapter: DbAdapter = {
getConfigExtension: () => ({
custom: {
_mongoConfig: { dbName: options.dbName, pools: resolvedPools },
_mongoConfig: { dbName: options.dbName, pools: resolvePools() },
},
}),

storeChunk: (payload, poolName, chunk) =>
storeChunkImpl(ctx, payload, poolName, chunk),
storeChunkImpl(getCtx(), payload, poolName, chunk),

deleteChunks: async (_payload, poolName, sourceCollection, docId) => {
const ctx = getCtx()
const cfg = ctx.pools[poolName]
if (!cfg) {
throw new Error(`[@payloadcms-vectorize/mongodb] Unknown pool "${poolName}"`)
Expand All @@ -64,6 +71,7 @@ export const createMongoVectorIntegration = (
docId,
embeddingVersion,
) => {
const ctx = getCtx()
const cfg = ctx.pools[poolName]
if (!cfg) {
throw new Error(`[@payloadcms-vectorize/mongodb] Unknown pool "${poolName}"`)
Expand All @@ -80,7 +88,7 @@ export const createMongoVectorIntegration = (
},

search: (payload, queryEmbedding, poolName, limit, where) =>
searchImpl(ctx, payload, queryEmbedding, poolName, limit, where),
searchImpl(getCtx(), payload, queryEmbedding, poolName, limit, where),
}

return { adapter }
Expand Down
Loading