Files
atelier/sidecar/db/providers/schema.ts
T
pastilhas c892f2e700 officer_db with atelier_ prefixes, like every other plugin
the extraction kept atelier's own postgres database, because the brief said to
when the tables already had rows. the owner reversed it, and the reason is not
tidiness: a plugin that behaves differently from every other plugin is a
plugin nobody can reason about.

the separate database meant a second connection string to set before install,
a second thing to back up, a second thing to get wrong on a new machine, and
failure modes no other plugin shares — none of it bought anything except
avoiding a migration.

four tables now: atelier_collections, atelier_collection_items,
atelier_providers, atelier_generations. index and check constraint names carry
the prefix too — those are global in postgres and are the one thing that would
otherwise collide with another plugin.

db/schema.ts re-exports the definitions rather than duplicating them: they stay
beside the code that queries them in sidecar/db/, and that file exists to be
found by gen-plugin-schemas.

ATELIER_POSTGRES_URL is gone. db.ts reads OFFICERDEV_POSTGRES_URL ?? POSTGRES_URL,
which the platform already sets.

the standalone service's rows are NOT here and nothing depends on them being
here — the old service is still running against the old database, untouched,
and migrating is a separate watched step after this is proven on alpha.
2026-08-20 17:30:53 +00:00

55 lines
2.5 KiB
TypeScript

import { pgTable, uuid, text, boolean, timestamp, index } from 'drizzle-orm/pg-core';
/**
* A configured connection to a model: adapter + model + credentials.
*
* ── Why this is a row and the adapter is not ──
*
* "OpenAI" is not a thing you can generate with. "OpenAI's gpt-image-1, with this key" is. The two
* differ in how often they change: an ADAPTER is code that learns a vendor's HTTP API and is written
* once; a PROVIDER is a decision — which model, whose key, is it on — and you make several of those
* per adapter and change them at runtime.
*
* So `openai + gpt-image-1` and `openai + dall-e-3` are two rows sharing one adapter and possibly one
* key. The previous design keyed configuration on the adapter name, which made that unsayable: there
* was exactly one OpenAI, and its model was a property of it rather than the point of it.
*
* Nothing here constrains `model` against the adapter. Vendors ship models faster than a list in this
* repo is updated, and refusing a new one on release day is a worse failure than passing it through
* and letting the vendor answer.
*/
export const providers = pgTable('atelier_providers',
{
id: uuid('id').primaryKey().defaultRandom(),
/** What a human calls it — "OpenAI · gpt-image-1". Unique, so it can be named in a request. */
name: text('name').notNull().unique(),
/** Which adapter drives it. Text, not an enum: adding an adapter must not be a schema change. */
adapter: text('adapter').notNull(),
model: text('model').notNull(),
/** AES-256-GCM via servers/crypto.ts. Never leaves the server and never appears in a response. */
apiKeyEncrypted: text('api_key_encrypted'),
enabled: boolean('enabled').notNull().default(true),
/**
* The one to use when a request names no provider and no model, or names a model nothing serves.
*
* A column rather than "the oldest row" because it is a decision the owner makes and should be
* able to change without deleting and recreating rows. Exactly one row should carry it; the
* queries below enforce that by clearing the others when one is set.
*/
isDefault: boolean('is_default').notNull().default(false),
createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(),
},
(table) => [
// Resolving a request by model is the hot path: every generation does it once.
index('atelier_providers_model_idx').on(table.model, table.enabled),
],
);