Implement file storage layer with local provider, upload/download API, tests

- StorageProvider interface with LocalProvider (S3 placeholder)
- File table with entity_type/entity_id references, content type, path
- POST /v1/files (multipart upload), GET /v1/files (list by entity),
  GET /v1/files/:id (metadata), GET /v1/files/serve/* (content),
  DELETE /v1/files/:id
- member_identifier drops base64 columns, uses file_id FKs
- File validation: type whitelist, size limits, per-entity max
- Fastify storage plugin injects provider into app
- 6 API tests for upload, list, get, delete, validation
- Test runner kills stale port before starting backend
This commit is contained in:
Ryan Moon
2026-03-28 15:29:06 -05:00
parent de4d2e0a32
commit 760e995ae3
19 changed files with 615 additions and 6 deletions

View File

@@ -82,8 +82,8 @@ export const memberIdentifiers = pgTable('member_identifier', {
issuingAuthority: varchar('issuing_authority', { length: 255 }),
issuedDate: date('issued_date'),
expiresAt: date('expires_at'),
imageFront: text('image_front'),
imageBack: text('image_back'),
imageFrontFileId: uuid('image_front_file_id'),
imageBackFileId: uuid('image_back_file_id'),
notes: text('notes'),
isPrimary: boolean('is_primary').notNull().default(false),
createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),

View File

@@ -0,0 +1,21 @@
import { pgTable, uuid, varchar, integer, timestamp } from 'drizzle-orm/pg-core'
import { companies } from './stores.js'
export const files = pgTable('file', {
id: uuid('id').primaryKey().defaultRandom(),
companyId: uuid('company_id')
.notNull()
.references(() => companies.id),
path: varchar('path', { length: 1000 }).notNull(),
filename: varchar('filename', { length: 255 }).notNull(),
contentType: varchar('content_type', { length: 100 }).notNull(),
sizeBytes: integer('size_bytes').notNull(),
entityType: varchar('entity_type', { length: 100 }).notNull(),
entityId: uuid('entity_id').notNull(),
category: varchar('category', { length: 100 }).notNull(),
uploadedBy: uuid('uploaded_by'),
createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
})
export type FileRecord = typeof files.$inferSelect
export type FileRecordInsert = typeof files.$inferInsert