This commit is contained in:
@@ -33,6 +33,13 @@ import {
|
||||
timestamp,
|
||||
uuid,
|
||||
} from 'drizzle-orm/pg-core';
|
||||
import { ALLOCATION_STATUSES, GUARANTEE_TYPES } from '@pig/core';
|
||||
export {
|
||||
ALLOCATION_STATUSES,
|
||||
CONSUMING_ALLOCATION_STATUSES,
|
||||
RESERVING_ALLOCATION_STATUSES,
|
||||
} from '@pig/core';
|
||||
export type { AllocationStatus } from '@pig/core';
|
||||
import { capacityCommitments } from './supply';
|
||||
import { demandDeals } from './demand';
|
||||
import { users } from './identity';
|
||||
@@ -95,7 +102,7 @@ export const allocations = pgTable(
|
||||
* allocations are shown separately so a seller can see what the pipeline
|
||||
* would do to utilisation without letting forecasts pollute the actuals.
|
||||
*/
|
||||
status: text('status').notNull().default('planned'),
|
||||
status: text('status', { enum: ALLOCATION_STATUSES }).notNull().default('planned'),
|
||||
|
||||
/**
|
||||
* A capacity HOLD with an expiry.
|
||||
@@ -129,7 +136,9 @@ export const allocations = pgTable(
|
||||
* system answer "can I actually sell this?" rather than "is something
|
||||
* technically free?"
|
||||
*/
|
||||
guaranteeType: text('guarantee_type').notNull().default('committed'),
|
||||
guaranteeType: text('guarantee_type', { enum: GUARANTEE_TYPES })
|
||||
.notNull()
|
||||
.default('committed'),
|
||||
priority: integer('priority').notNull().default(100),
|
||||
|
||||
/**
|
||||
@@ -160,35 +169,5 @@ export const allocations = pgTable(
|
||||
],
|
||||
);
|
||||
|
||||
/**
|
||||
* Statuses that actually consume committed capacity.
|
||||
*
|
||||
* Kept here beside the column it describes so the definition cannot drift away
|
||||
* from the schema. Utilisation and idle-capacity figures filter on this;
|
||||
* including `planned` would let optimistic forecasting hide idle hardware.
|
||||
*/
|
||||
export const CONSUMING_ALLOCATION_STATUSES = ['committed', 'active', 'completed'] as const;
|
||||
|
||||
/**
|
||||
* Statuses that block the capacity from being sold to somebody else.
|
||||
*
|
||||
* Deliberately WIDER than the set that counts toward utilisation and margin.
|
||||
* A live hold must remove inventory from availability — otherwise two sellers
|
||||
* promise the same GPUs — while not yet counting as sold, because it has not
|
||||
* been. Conflating "cannot be offered to anyone else" with "earning revenue"
|
||||
* is how a pipeline of optimistic holds comes to look like a full book.
|
||||
*/
|
||||
export const RESERVING_ALLOCATION_STATUSES = [
|
||||
'planned',
|
||||
...CONSUMING_ALLOCATION_STATUSES,
|
||||
] as const;
|
||||
|
||||
export const ALLOCATION_STATUSES = [
|
||||
'planned',
|
||||
...CONSUMING_ALLOCATION_STATUSES,
|
||||
'released',
|
||||
] as const;
|
||||
|
||||
export type AllocationStatus = (typeof ALLOCATION_STATUSES)[number];
|
||||
export type Allocation = typeof allocations.$inferSelect;
|
||||
export type NewAllocation = typeof allocations.$inferInsert;
|
||||
|
||||
@@ -218,3 +218,23 @@ export const complianceArtifacts = pgTable(
|
||||
export type ExportAuthorization = typeof exportAuthorizations.$inferSelect;
|
||||
export type ComplianceDecision = typeof complianceDecisions.$inferSelect;
|
||||
export type ComplianceArtifact = typeof complianceArtifacts.$inferSelect;
|
||||
|
||||
export type ComplianceMatchDecision = Pick<
|
||||
ComplianceDecision,
|
||||
'decision' | 'supersededAt'
|
||||
>;
|
||||
|
||||
/**
|
||||
* Whether a recorded export-control predicate permits a prospective match.
|
||||
*
|
||||
* `undefined` means no buyer-specific evaluation was requested, as in a generic
|
||||
* inventory search. Once evaluation is requested, absence, review, block and a
|
||||
* superseded allow all fail closed. Security classification is not a substitute
|
||||
* for an export-control determination.
|
||||
*/
|
||||
export function complianceDecisionAllowsMatch(
|
||||
decision: ComplianceMatchDecision | null | undefined,
|
||||
): boolean {
|
||||
if (decision === undefined) return true;
|
||||
return decision?.decision === 'allow' && decision.supersededAt === null;
|
||||
}
|
||||
|
||||
@@ -214,15 +214,25 @@ export const channelLinks = pgTable(
|
||||
id: uuid('id').primaryKey().defaultRandom(),
|
||||
/** 'slack' | 'buzz' — kept as text so a new platform needs no migration. */
|
||||
platform: text('platform').notNull(),
|
||||
/** Slack channel ids are unique only inside a workspace. Buzz uses ''. */
|
||||
workspaceId: text('workspace_id').notNull().default(''),
|
||||
channelId: text('channel_id').notNull(),
|
||||
channelName: text('channel_name'),
|
||||
accountId: uuid('account_id').references(() => accounts.id, { onDelete: 'cascade' }),
|
||||
/** Notify on stage changes, idle-capacity alerts, renewals due. */
|
||||
notifyOn: jsonb('notify_on').$type<string[]>().notNull().default([]),
|
||||
linkedByUserId: uuid('linked_by_user_id').references(() => users.id, {
|
||||
onDelete: 'set null',
|
||||
}),
|
||||
createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
|
||||
updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(),
|
||||
},
|
||||
(t) => [
|
||||
uniqueIndex('channel_links_platform_channel_key').on(t.platform, t.channelId),
|
||||
uniqueIndex('channel_links_platform_workspace_channel_key').on(
|
||||
t.platform,
|
||||
t.workspaceId,
|
||||
t.channelId,
|
||||
),
|
||||
index('channel_links_account_idx').on(t.accountId),
|
||||
],
|
||||
);
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
import { index, jsonb, pgTable, text, timestamp, uniqueIndex, uuid } from 'drizzle-orm/pg-core';
|
||||
import { users } from './identity';
|
||||
|
||||
/** One least-privilege Google Sheets connection per PIG user. */
|
||||
export const googleConnections = pgTable('google_connections', {
|
||||
userId: uuid('user_id')
|
||||
.primaryKey()
|
||||
.references(() => users.id, { onDelete: 'cascade' }),
|
||||
/** Purpose-bound AES-256-GCM envelopes. Tokens are never returned by the API. */
|
||||
refreshTokenEncrypted: text('refresh_token_encrypted').notNull(),
|
||||
accessTokenEncrypted: text('access_token_encrypted'),
|
||||
accessTokenExpiresAt: timestamp('access_token_expires_at', { withTimezone: true }),
|
||||
scopes: jsonb('scopes').$type<string[]>().notNull().default([]),
|
||||
connectedAt: timestamp('connected_at', { withTimezone: true }).notNull().defaultNow(),
|
||||
updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(),
|
||||
});
|
||||
|
||||
/**
|
||||
* Short-lived OAuth proof material stays server-side. State is hashed and the
|
||||
* PKCE verifier encrypted so a database read cannot complete an OAuth flow.
|
||||
*/
|
||||
export const googleOauthFlows = pgTable(
|
||||
'google_oauth_flows',
|
||||
{
|
||||
id: uuid('id').primaryKey().defaultRandom(),
|
||||
userId: uuid('user_id')
|
||||
.notNull()
|
||||
.references(() => users.id, { onDelete: 'cascade' }),
|
||||
stateHash: text('state_hash').notNull(),
|
||||
browserBindingHash: text('browser_binding_hash').notNull(),
|
||||
pkceVerifierEncrypted: text('pkce_verifier_encrypted').notNull(),
|
||||
expiresAt: timestamp('expires_at', { withTimezone: true }).notNull(),
|
||||
consumedAt: timestamp('consumed_at', { withTimezone: true }),
|
||||
createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
|
||||
},
|
||||
(table) => [
|
||||
uniqueIndex('google_oauth_flows_state_hash_key').on(table.stateHash),
|
||||
index('google_oauth_flows_expiry_idx').on(table.expiresAt),
|
||||
index('google_oauth_flows_user_idx').on(table.userId),
|
||||
],
|
||||
);
|
||||
|
||||
export type GoogleConnection = typeof googleConnections.$inferSelect;
|
||||
export type GoogleOauthFlow = typeof googleOauthFlows.$inferSelect;
|
||||
@@ -0,0 +1,33 @@
|
||||
import { index, pgTable, text, timestamp, uniqueIndex, uuid } from 'drizzle-orm/pg-core';
|
||||
import { users } from './identity';
|
||||
|
||||
/**
|
||||
* Durable source identities make repeated imports updates rather than duplicate
|
||||
* creates. Raw spreadsheet content stays out of the database; only the user's
|
||||
* chosen stable key and the PIG record it resolved to are retained.
|
||||
*/
|
||||
export const importIdentities = pgTable(
|
||||
'import_identities',
|
||||
{
|
||||
id: uuid('id').primaryKey().defaultRandom(),
|
||||
entity: text('entity').notNull(),
|
||||
keyColumn: text('key_column').notNull(),
|
||||
keyValue: text('key_value').notNull(),
|
||||
recordId: uuid('record_id').notNull(),
|
||||
importedByUserId: uuid('imported_by_user_id').references(() => users.id, {
|
||||
onDelete: 'set null',
|
||||
}),
|
||||
createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
|
||||
updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(),
|
||||
},
|
||||
(table) => [
|
||||
uniqueIndex('import_identities_source_key').on(
|
||||
table.entity,
|
||||
table.keyColumn,
|
||||
table.keyValue,
|
||||
),
|
||||
index('import_identities_record_idx').on(table.entity, table.recordId),
|
||||
],
|
||||
);
|
||||
|
||||
export type ImportIdentity = typeof importIdentities.$inferSelect;
|
||||
@@ -16,6 +16,7 @@
|
||||
*/
|
||||
export * from './enums';
|
||||
export * from './identity';
|
||||
export * from './settings';
|
||||
export * from './crm';
|
||||
export * from './supply';
|
||||
export * from './demand';
|
||||
@@ -24,3 +25,7 @@ export * from './contracts';
|
||||
export * from './compliance';
|
||||
export * from './agent';
|
||||
export * from './fields';
|
||||
export * from './integrations';
|
||||
export * from './imports';
|
||||
export * from './google';
|
||||
export * from './notion';
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
import { index, integer, jsonb, pgTable, text, timestamp, uniqueIndex, uuid } from 'drizzle-orm/pg-core';
|
||||
import { channelLinks } from './crm';
|
||||
import { users } from './identity';
|
||||
|
||||
/**
|
||||
* External delivery is a separate durable concern from the CRM transaction.
|
||||
* A deal write only adds a row here; a provider-specific worker owns network
|
||||
* failure, retry and completion without holding the request thread open.
|
||||
*/
|
||||
export const notificationOutbox = pgTable(
|
||||
'notification_outbox',
|
||||
{
|
||||
id: uuid('id').primaryKey().defaultRandom(),
|
||||
provider: text('provider').notNull(),
|
||||
kind: text('kind').notNull(),
|
||||
linkId: uuid('link_id').references(() => channelLinks.id, { onDelete: 'set null' }),
|
||||
workspaceId: text('workspace_id'),
|
||||
destination: text('destination').notNull(),
|
||||
payload: jsonb('payload').$type<Record<string, unknown>>().notNull(),
|
||||
idempotencyKey: text('idempotency_key').notNull(),
|
||||
|
||||
status: text('status').notNull().default('pending'),
|
||||
attempts: integer('attempts').notNull().default(0),
|
||||
maxAttempts: integer('max_attempts').notNull().default(5),
|
||||
dueAt: timestamp('due_at', { withTimezone: true }).notNull().defaultNow(),
|
||||
leasedUntil: timestamp('leased_until', { withTimezone: true }),
|
||||
leasedBy: text('leased_by'),
|
||||
deliveredAt: timestamp('delivered_at', { withTimezone: true }),
|
||||
externalId: text('external_id'),
|
||||
error: text('error'),
|
||||
|
||||
requestedByUserId: uuid('requested_by_user_id').references(() => users.id, {
|
||||
onDelete: 'set null',
|
||||
}),
|
||||
createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
|
||||
},
|
||||
(t) => [
|
||||
uniqueIndex('notification_outbox_idempotency_key').on(t.idempotencyKey),
|
||||
index('notification_outbox_claimable_idx').on(t.provider, t.status, t.dueAt),
|
||||
index('notification_outbox_link_idx').on(t.linkId),
|
||||
],
|
||||
);
|
||||
|
||||
export type NotificationOutboxItem = typeof notificationOutbox.$inferSelect;
|
||||
@@ -0,0 +1,45 @@
|
||||
import { index, pgTable, text, timestamp, uniqueIndex, uuid } from 'drizzle-orm/pg-core';
|
||||
import { users } from './identity';
|
||||
|
||||
/**
|
||||
* A Notion grant belongs to the person who completed OAuth. Credentials are a
|
||||
* single authenticated-encryption envelope so no token-shaped value is ever
|
||||
* queryable, searchable, or accidentally selected as ordinary metadata.
|
||||
*/
|
||||
export const notionConnections = pgTable(
|
||||
'notion_connections',
|
||||
{
|
||||
id: uuid('id').primaryKey().defaultRandom(),
|
||||
userId: uuid('user_id').notNull().references(() => users.id, { onDelete: 'cascade' }),
|
||||
workspaceId: text('workspace_id').notNull(),
|
||||
workspaceName: text('workspace_name'),
|
||||
workspaceIcon: text('workspace_icon'),
|
||||
botId: text('bot_id'),
|
||||
credentialsEncrypted: text('credentials_encrypted').notNull(),
|
||||
createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
|
||||
updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(),
|
||||
},
|
||||
(table) => [
|
||||
uniqueIndex('notion_connections_user_workspace_key').on(table.userId, table.workspaceId),
|
||||
index('notion_connections_user_idx').on(table.userId),
|
||||
],
|
||||
);
|
||||
|
||||
/**
|
||||
* OAuth attempts survive multiple API processes but not replay. Only hashes
|
||||
* are durable; the browser verifier is an HttpOnly, short-lived cookie.
|
||||
*/
|
||||
export const notionOauthStates = pgTable(
|
||||
'notion_oauth_states',
|
||||
{
|
||||
stateHash: text('state_hash').primaryKey(),
|
||||
userId: uuid('user_id').notNull().references(() => users.id, { onDelete: 'cascade' }),
|
||||
verifierHash: text('verifier_hash').notNull(),
|
||||
expiresAt: timestamp('expires_at', { withTimezone: true }).notNull(),
|
||||
createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
|
||||
},
|
||||
(table) => [index('notion_oauth_states_expiry_idx').on(table.expiresAt)],
|
||||
);
|
||||
|
||||
export type NotionConnection = typeof notionConnections.$inferSelect;
|
||||
export type NotionOauthState = typeof notionOauthStates.$inferSelect;
|
||||
@@ -0,0 +1,42 @@
|
||||
import {
|
||||
boolean,
|
||||
check,
|
||||
integer,
|
||||
pgTable,
|
||||
text,
|
||||
timestamp,
|
||||
uuid,
|
||||
} from 'drizzle-orm/pg-core';
|
||||
import { sql } from 'drizzle-orm';
|
||||
import { users } from './identity';
|
||||
|
||||
/** Workspace-wide controls. The check keeps this deliberately single-tenant. */
|
||||
export const platformSettings = pgTable(
|
||||
'platform_settings',
|
||||
{
|
||||
id: text('id').primaryKey().default('default'),
|
||||
piggyModel: text('piggy_model').notNull().default('nvidia/nemotron-3-nano-30b-a3b'),
|
||||
piggyInferenceBase: text('piggy_inference_base')
|
||||
.notNull()
|
||||
.default('https://api.pinference.ai/api/v1'),
|
||||
piggyEnabled: boolean('piggy_enabled').notNull().default(false),
|
||||
/** AES-256-GCM envelope. Its key lives outside the database. */
|
||||
primeApiKeyEncrypted: text('prime_api_key_encrypted'),
|
||||
primeApiKeyUpdatedAt: timestamp('prime_api_key_updated_at', { withTimezone: true }),
|
||||
primeSyncEnabled: boolean('prime_sync_enabled').notNull().default(false),
|
||||
primeSyncIntervalMinutes: integer('prime_sync_interval_minutes').notNull().default(30),
|
||||
updatedByUserId: uuid('updated_by_user_id').references(() => users.id, {
|
||||
onDelete: 'set null',
|
||||
}),
|
||||
updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(),
|
||||
},
|
||||
(t) => [
|
||||
check('platform_settings_singleton_check', sql`${t.id} = 'default'`),
|
||||
check(
|
||||
'platform_settings_sync_interval_check',
|
||||
sql`${t.primeSyncIntervalMinutes} BETWEEN 1 AND 1440`,
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
export type PlatformSettings = typeof platformSettings.$inferSelect;
|
||||
Reference in New Issue
Block a user