Files
zpan/server/db/image-hosting.test.ts
T
2dca9d9ffb feat(db): image hosting schema, apiKey plugin, migration 0011 (v2.4.0 T1) (#315)
* feat(db): image hosting schema, apiKey plugin, migration 0011

Adds the DB infrastructure for the v2.4 image hosting feature:

- server/db/schema.ts: new `image_hosting_configs` (per-org singleton,
  custom domain + referer allowlist) and `image_hostings` tables with
  all required indexes and cascade-delete FKs
- server/db/auth-schema.ts: `apikey` table for @better-auth/api-key
  plugin (referenceId-indexed, rate-limiting fields, permissions column)
- server/auth.ts: enable apiKey plugin (references='organization',
  image-hosting:upload permission declared)
- shared/types/index.ts: ImageHostingConfig, ImageHosting, ImageHostingStatus
- migrations/0011_image-hosting.sql: generated by drizzle-kit (NOT
  hand-authored); creates all three new tables + indexes
- migrations/meta/0011_snapshot.json: baseline snapshot for future
  migration generation (repo was missing snapshots 0002–0010)
- package.json: add @better-auth/api-key ^1.6.2 dep, bump
  better-auth to ^1.6.2

Note: migration is tagged 0011_image-hosting (idx=11 in journal) rather
than 0012 because the existing 0011_notifications was registered as idx=10
(pre-existing numbering offset from a skipped 0006_ slot).

Agent-Profile: https://agent-kanban.dev/agents/a6bb038c4226a87f

* fix: resolve coverage gap and migration filename collision

- Add getTableConfig FK reference tests to cover deferred .references()
  callbacks in schema.ts (fixes Codecov patch coverage < 94.98%)
- Rename 0011_image-hosting → 0012_image-hosting to avoid filename
  collision with pre-existing 0011_notifications migration

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Bob <aibob@mails.agent-kanban.dev>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-21 03:18:52 -04:00

169 lines
5.7 KiB
TypeScript

import { getTableConfig } from 'drizzle-orm/sqlite-core'
import { describe, expect, it } from 'vitest'
import { apikey } from './auth-schema.js'
import { imageHostingConfigs, imageHostings } from './schema.js'
describe('imageHostingConfigs table', () => {
it('uses org_id as primary key', () => {
expect(imageHostingConfigs.orgId.name).toBe('org_id')
expect(imageHostingConfigs.orgId.primary).toBe(true)
})
it('has a nullable custom_domain column', () => {
expect(imageHostingConfigs.customDomain).toBeDefined()
expect(imageHostingConfigs.customDomain.name).toBe('custom_domain')
expect(imageHostingConfigs.customDomain.notNull).toBe(false)
})
it('custom_domain has a unique constraint', () => {
expect(imageHostingConfigs.customDomain.isUnique).toBe(true)
})
it('has nullable cf_hostname_id column', () => {
expect(imageHostingConfigs.cfHostnameId).toBeDefined()
expect(imageHostingConfigs.cfHostnameId.name).toBe('cf_hostname_id')
expect(imageHostingConfigs.cfHostnameId.notNull).toBe(false)
})
it('has nullable domain_verified_at column (timestamp_ms)', () => {
expect(imageHostingConfigs.domainVerifiedAt).toBeDefined()
expect(imageHostingConfigs.domainVerifiedAt.name).toBe('domain_verified_at')
expect(imageHostingConfigs.domainVerifiedAt.columnType).toBe('SQLiteTimestamp')
expect(imageHostingConfigs.domainVerifiedAt.notNull).toBe(false)
})
it('has nullable referer_allowlist column', () => {
expect(imageHostingConfigs.refererAllowlist).toBeDefined()
expect(imageHostingConfigs.refererAllowlist.notNull).toBe(false)
})
it('has not-null created_at and updated_at timestamps', () => {
expect(imageHostingConfigs.createdAt.notNull).toBe(true)
expect(imageHostingConfigs.updatedAt.notNull).toBe(true)
})
it('org_id has a cascade-delete FK to organization', () => {
const { foreignKeys } = getTableConfig(imageHostingConfigs)
expect(foreignKeys).toHaveLength(1)
const ref = foreignKeys[0].reference()
expect(ref.foreignColumns).toHaveLength(1)
expect(ref.foreignColumns[0].name).toBe('id')
})
})
describe('imageHostings table', () => {
it('has a text primary key id', () => {
expect(imageHostings.id.name).toBe('id')
expect(imageHostings.id.primary).toBe(true)
expect(imageHostings.id.columnType).toBe('SQLiteText')
})
it('has not-null org_id column', () => {
expect(imageHostings.orgId.name).toBe('org_id')
expect(imageHostings.orgId.notNull).toBe(true)
})
it('has unique token column', () => {
expect(imageHostings.token.name).toBe('token')
expect(imageHostings.token.notNull).toBe(true)
expect(imageHostings.token.isUnique).toBe(true)
})
it('has not-null path column', () => {
expect(imageHostings.path.name).toBe('path')
expect(imageHostings.path.notNull).toBe(true)
})
it('has not-null storage_id and storage_key columns', () => {
expect(imageHostings.storageId.notNull).toBe(true)
expect(imageHostings.storageKey.notNull).toBe(true)
})
it('has not-null size and mime columns', () => {
expect(imageHostings.size.notNull).toBe(true)
expect(imageHostings.mime.notNull).toBe(true)
})
it('has nullable width and height columns', () => {
expect(imageHostings.width.notNull).toBe(false)
expect(imageHostings.height.notNull).toBe(false)
})
it('status defaults to "draft"', () => {
expect(imageHostings.status.notNull).toBe(true)
expect(imageHostings.status.default).toBe('draft')
})
it('access_count defaults to 0', () => {
expect(imageHostings.accessCount.notNull).toBe(true)
expect(imageHostings.accessCount.default).toBe(0)
})
it('has nullable last_accessed_at timestamp', () => {
expect(imageHostings.lastAccessedAt.notNull).toBe(false)
})
it('has not-null created_at timestamp', () => {
expect(imageHostings.createdAt.notNull).toBe(true)
})
it('org_id and storage_id have cascade/no-action FKs', () => {
const { foreignKeys } = getTableConfig(imageHostings)
expect(foreignKeys).toHaveLength(2)
// Both FKs resolve to a single-column reference on 'id'
for (const fk of foreignKeys) {
const ref = fk.reference()
expect(ref.foreignColumns).toHaveLength(1)
expect(ref.foreignColumns[0].name).toBe('id')
}
})
})
describe('apikey table', () => {
it('has a text primary key id', () => {
expect(apikey.id.name).toBe('id')
expect(apikey.id.primary).toBe(true)
})
it('config_id defaults to "default"', () => {
expect(apikey.configId.name).toBe('config_id')
expect(apikey.configId.notNull).toBe(true)
expect(apikey.configId.default).toBe('default')
})
it('has not-null reference_id for org/user scoping', () => {
expect(apikey.referenceId.name).toBe('reference_id')
expect(apikey.referenceId.notNull).toBe(true)
})
it('has not-null key column', () => {
expect(apikey.key.name).toBe('key')
expect(apikey.key.notNull).toBe(true)
})
it('enabled defaults to true', () => {
expect(apikey.enabled.notNull).toBe(true)
expect(apikey.enabled.default).toBe(true)
})
it('rate_limit_enabled defaults to true', () => {
expect(apikey.rateLimitEnabled.notNull).toBe(true)
expect(apikey.rateLimitEnabled.default).toBe(true)
})
it('request_count defaults to 0', () => {
expect(apikey.requestCount.notNull).toBe(true)
expect(apikey.requestCount.default).toBe(0)
})
it('has not-null created_at and updated_at', () => {
expect(apikey.createdAt.notNull).toBe(true)
expect(apikey.updatedAt.notNull).toBe(true)
})
it('has nullable permissions column for JSON-serialized statements', () => {
expect(apikey.permissions).toBeDefined()
expect(apikey.permissions.notNull).toBe(false)
})
})