This commit is contained in:
@@ -0,0 +1 @@
|
||||
ALTER TYPE "public"."pig_security_tier" ADD VALUE 'government' BEFORE 'secure_cloud';
|
||||
@@ -0,0 +1 @@
|
||||
ALTER TYPE "public"."pig_fact_status" ADD VALUE 'approved' BEFORE 'proposed';
|
||||
@@ -0,0 +1,16 @@
|
||||
CREATE TABLE "platform_settings" (
|
||||
"id" text PRIMARY KEY DEFAULT 'default' NOT NULL,
|
||||
"piggy_model" text DEFAULT 'nvidia/nemotron-3-nano-30b-a3b' NOT NULL,
|
||||
"piggy_inference_base" text DEFAULT 'https://api.pinference.ai/api/v1' NOT NULL,
|
||||
"piggy_enabled" boolean DEFAULT false NOT NULL,
|
||||
"prime_api_key_encrypted" text,
|
||||
"prime_api_key_updated_at" timestamp with time zone,
|
||||
"prime_sync_enabled" boolean DEFAULT false NOT NULL,
|
||||
"prime_sync_interval_minutes" integer DEFAULT 30 NOT NULL,
|
||||
"updated_by_user_id" uuid,
|
||||
"updated_at" timestamp with time zone DEFAULT now() NOT NULL,
|
||||
CONSTRAINT "platform_settings_singleton_check" CHECK ("platform_settings"."id" = 'default'),
|
||||
CONSTRAINT "platform_settings_sync_interval_check" CHECK ("platform_settings"."prime_sync_interval_minutes" BETWEEN 1 AND 1440)
|
||||
);
|
||||
--> statement-breakpoint
|
||||
ALTER TABLE "platform_settings" ADD CONSTRAINT "platform_settings_updated_by_user_id_users_id_fk" FOREIGN KEY ("updated_by_user_id") REFERENCES "public"."users"("id") ON DELETE set null ON UPDATE no action;
|
||||
@@ -0,0 +1,33 @@
|
||||
CREATE TABLE "notification_outbox" (
|
||||
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
||||
"provider" text NOT NULL,
|
||||
"kind" text NOT NULL,
|
||||
"link_id" uuid,
|
||||
"workspace_id" text,
|
||||
"destination" text NOT NULL,
|
||||
"payload" jsonb NOT NULL,
|
||||
"idempotency_key" text NOT NULL,
|
||||
"status" text DEFAULT 'pending' NOT NULL,
|
||||
"attempts" integer DEFAULT 0 NOT NULL,
|
||||
"max_attempts" integer DEFAULT 5 NOT NULL,
|
||||
"due_at" timestamp with time zone DEFAULT now() NOT NULL,
|
||||
"leased_until" timestamp with time zone,
|
||||
"leased_by" text,
|
||||
"delivered_at" timestamp with time zone,
|
||||
"external_id" text,
|
||||
"error" text,
|
||||
"requested_by_user_id" uuid,
|
||||
"created_at" timestamp with time zone DEFAULT now() NOT NULL
|
||||
);
|
||||
--> statement-breakpoint
|
||||
DROP INDEX "channel_links_platform_channel_key";--> statement-breakpoint
|
||||
ALTER TABLE "channel_links" ADD COLUMN "workspace_id" text DEFAULT '' NOT NULL;--> statement-breakpoint
|
||||
ALTER TABLE "channel_links" ADD COLUMN "linked_by_user_id" uuid;--> statement-breakpoint
|
||||
ALTER TABLE "channel_links" ADD COLUMN "updated_at" timestamp with time zone DEFAULT now() NOT NULL;--> statement-breakpoint
|
||||
ALTER TABLE "notification_outbox" ADD CONSTRAINT "notification_outbox_link_id_channel_links_id_fk" FOREIGN KEY ("link_id") REFERENCES "public"."channel_links"("id") ON DELETE set null ON UPDATE no action;--> statement-breakpoint
|
||||
ALTER TABLE "notification_outbox" ADD CONSTRAINT "notification_outbox_requested_by_user_id_users_id_fk" FOREIGN KEY ("requested_by_user_id") REFERENCES "public"."users"("id") ON DELETE set null ON UPDATE no action;--> statement-breakpoint
|
||||
CREATE UNIQUE INDEX "notification_outbox_idempotency_key" ON "notification_outbox" USING btree ("idempotency_key");--> statement-breakpoint
|
||||
CREATE INDEX "notification_outbox_claimable_idx" ON "notification_outbox" USING btree ("provider","status","due_at");--> statement-breakpoint
|
||||
CREATE INDEX "notification_outbox_link_idx" ON "notification_outbox" USING btree ("link_id");--> statement-breakpoint
|
||||
ALTER TABLE "channel_links" ADD CONSTRAINT "channel_links_linked_by_user_id_users_id_fk" FOREIGN KEY ("linked_by_user_id") REFERENCES "public"."users"("id") ON DELETE set null ON UPDATE no action;--> statement-breakpoint
|
||||
CREATE UNIQUE INDEX "channel_links_platform_workspace_channel_key" ON "channel_links" USING btree ("platform","workspace_id","channel_id");
|
||||
@@ -0,0 +1,14 @@
|
||||
CREATE TABLE "import_identities" (
|
||||
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
||||
"entity" text NOT NULL,
|
||||
"key_column" text NOT NULL,
|
||||
"key_value" text NOT NULL,
|
||||
"record_id" uuid NOT NULL,
|
||||
"imported_by_user_id" uuid,
|
||||
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
|
||||
"updated_at" timestamp with time zone DEFAULT now() NOT NULL
|
||||
);
|
||||
--> statement-breakpoint
|
||||
ALTER TABLE "import_identities" ADD CONSTRAINT "import_identities_imported_by_user_id_users_id_fk" FOREIGN KEY ("imported_by_user_id") REFERENCES "public"."users"("id") ON DELETE set null ON UPDATE no action;--> statement-breakpoint
|
||||
CREATE UNIQUE INDEX "import_identities_source_key" ON "import_identities" USING btree ("entity","key_column","key_value");--> statement-breakpoint
|
||||
CREATE INDEX "import_identities_record_idx" ON "import_identities" USING btree ("entity","record_id");
|
||||
@@ -0,0 +1,25 @@
|
||||
CREATE TABLE "notion_connections" (
|
||||
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
||||
"user_id" uuid NOT NULL,
|
||||
"workspace_id" text NOT NULL,
|
||||
"workspace_name" text,
|
||||
"workspace_icon" text,
|
||||
"bot_id" text,
|
||||
"credentials_encrypted" text NOT NULL,
|
||||
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
|
||||
"updated_at" timestamp with time zone DEFAULT now() NOT NULL
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE "notion_oauth_states" (
|
||||
"state_hash" text PRIMARY KEY NOT NULL,
|
||||
"user_id" uuid NOT NULL,
|
||||
"verifier_hash" text NOT NULL,
|
||||
"expires_at" timestamp with time zone NOT NULL,
|
||||
"created_at" timestamp with time zone DEFAULT now() NOT NULL
|
||||
);
|
||||
--> statement-breakpoint
|
||||
ALTER TABLE "notion_connections" ADD CONSTRAINT "notion_connections_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
|
||||
ALTER TABLE "notion_oauth_states" ADD CONSTRAINT "notion_oauth_states_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
|
||||
CREATE UNIQUE INDEX "notion_connections_user_workspace_key" ON "notion_connections" USING btree ("user_id","workspace_id");--> statement-breakpoint
|
||||
CREATE INDEX "notion_connections_user_idx" ON "notion_connections" USING btree ("user_id");--> statement-breakpoint
|
||||
CREATE INDEX "notion_oauth_states_expiry_idx" ON "notion_oauth_states" USING btree ("expires_at");
|
||||
@@ -0,0 +1,26 @@
|
||||
CREATE TABLE "google_connections" (
|
||||
"user_id" uuid PRIMARY KEY NOT NULL,
|
||||
"refresh_token_encrypted" text NOT NULL,
|
||||
"access_token_encrypted" text,
|
||||
"access_token_expires_at" timestamp with time zone,
|
||||
"scopes" jsonb DEFAULT '[]'::jsonb NOT NULL,
|
||||
"connected_at" timestamp with time zone DEFAULT now() NOT NULL,
|
||||
"updated_at" timestamp with time zone DEFAULT now() NOT NULL
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE "google_oauth_flows" (
|
||||
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
||||
"user_id" uuid NOT NULL,
|
||||
"state_hash" text NOT NULL,
|
||||
"browser_binding_hash" text NOT NULL,
|
||||
"pkce_verifier_encrypted" text NOT NULL,
|
||||
"expires_at" timestamp with time zone NOT NULL,
|
||||
"consumed_at" timestamp with time zone,
|
||||
"created_at" timestamp with time zone DEFAULT now() NOT NULL
|
||||
);
|
||||
--> statement-breakpoint
|
||||
ALTER TABLE "google_connections" ADD CONSTRAINT "google_connections_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
|
||||
ALTER TABLE "google_oauth_flows" ADD CONSTRAINT "google_oauth_flows_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
|
||||
CREATE UNIQUE INDEX "google_oauth_flows_state_hash_key" ON "google_oauth_flows" USING btree ("state_hash");--> statement-breakpoint
|
||||
CREATE INDEX "google_oauth_flows_expiry_idx" ON "google_oauth_flows" USING btree ("expires_at");--> statement-breakpoint
|
||||
CREATE INDEX "google_oauth_flows_user_idx" ON "google_oauth_flows" USING btree ("user_id");
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -22,6 +22,55 @@
|
||||
"when": 1786588874252,
|
||||
"tag": "0002_invite_reuse",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 3,
|
||||
"version": "7",
|
||||
"when": 1786600259806,
|
||||
"tag": "0003_amazing_miss_america",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 4,
|
||||
"version": "7",
|
||||
"when": 1786602049010,
|
||||
"tag": "0004_fact_approval",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 5,
|
||||
"version": "7",
|
||||
"when": 1786611600000,
|
||||
"tag": "0005_platform_settings",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 6,
|
||||
"version": "7",
|
||||
"when": 1786611700000,
|
||||
"tag": "0006_dazzling_forge",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 7,
|
||||
"version": "7",
|
||||
"when": 1786611800000,
|
||||
"tag": "0007_nasty_vanisher",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 8,
|
||||
"version": "7",
|
||||
"when": 1786611900000,
|
||||
"tag": "0008_easy_scalphunter",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 9,
|
||||
"version": "7",
|
||||
"when": 1786612000000,
|
||||
"tag": "0009_warm_metal_master",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
@@ -35,6 +35,7 @@
|
||||
* is more useful than one that opens on a loss, which reads as a broken
|
||||
* product rather than an under-utilised book.
|
||||
*/
|
||||
import { ALLOCATION_STATUSES, type AllocationStatus } from '@pig/core';
|
||||
import { and, eq, like, or } from 'drizzle-orm';
|
||||
import { createDatabase } from '../client';
|
||||
import {
|
||||
@@ -47,6 +48,7 @@ import {
|
||||
contracts,
|
||||
contractObligations,
|
||||
demandDeals,
|
||||
type NewAllocation,
|
||||
sites,
|
||||
slaTerms,
|
||||
supplyDeals,
|
||||
@@ -59,6 +61,10 @@ const day = 86_400_000;
|
||||
const now = Date.now();
|
||||
const at = (days: number) => new Date(now + days * day);
|
||||
|
||||
function isAllocationStatus(value: string): value is AllocationStatus {
|
||||
return (ALLOCATION_STATUSES as readonly string[]).includes(value);
|
||||
}
|
||||
|
||||
/** GPU-hours for a block, allowing for a maintenance/ramp haircut. */
|
||||
const hours = (gpus: number, days: number, efficiency = 0.94) =>
|
||||
String(Math.round(gpus * 24 * days * efficiency));
|
||||
@@ -153,7 +159,12 @@ const DEMAND = [
|
||||
},
|
||||
request: { gpuType: 'H200', gpuCount: 256, fastFabric: true, maxPriceCents: 285 },
|
||||
// Draws from the CoreWeave block.
|
||||
allocation: { supplier: 'coreweave.com', share: 0.76, priceCents: 271, status: 'active' },
|
||||
allocation: {
|
||||
supplier: 'coreweave.com',
|
||||
share: 0.76,
|
||||
priceCents: 271,
|
||||
status: 'active',
|
||||
},
|
||||
},
|
||||
{
|
||||
account: `${PREFIX}Verity Health AI`,
|
||||
@@ -178,7 +189,12 @@ const DEMAND = [
|
||||
allowedRegions: ['eu-north', 'eu-west'],
|
||||
certifications: ['ISO 27001', 'SOC 2 Type II'],
|
||||
},
|
||||
allocation: { supplier: 'nebius.com', share: 0.55, priceCents: 249, status: 'committed' },
|
||||
allocation: {
|
||||
supplier: 'nebius.com',
|
||||
share: 0.55,
|
||||
priceCents: 249,
|
||||
status: 'committed',
|
||||
},
|
||||
},
|
||||
{
|
||||
account: `${PREFIX}Northwind Robotics`,
|
||||
@@ -195,7 +211,12 @@ const DEMAND = [
|
||||
dpaExecuted: true,
|
||||
},
|
||||
request: { gpuType: 'B200', gpuCount: 32, fastFabric: true, maxPriceCents: 460 },
|
||||
allocation: { supplier: 'crusoe.ai', share: 0.74, priceCents: 441, status: 'active' },
|
||||
allocation: {
|
||||
supplier: 'crusoe.ai',
|
||||
share: 0.74,
|
||||
priceCents: 441,
|
||||
status: 'active',
|
||||
},
|
||||
},
|
||||
{
|
||||
account: `${PREFIX}Tessellate Labs`,
|
||||
@@ -215,7 +236,13 @@ const DEMAND = [
|
||||
// A HOLD, not a sale. The deal has not closed, so this reserves capacity
|
||||
// without counting as revenue — the distinction the capacity view exists
|
||||
// to make visible.
|
||||
allocation: { supplier: 'runpod.io', share: 0.55, priceCents: 168, status: 'planned', holdDays: 12 },
|
||||
allocation: {
|
||||
supplier: 'runpod.io',
|
||||
share: 0.55,
|
||||
priceCents: 168,
|
||||
status: 'planned',
|
||||
holdDays: 12,
|
||||
},
|
||||
},
|
||||
{
|
||||
account: `${PREFIX}Aurelian Systems`,
|
||||
@@ -554,7 +581,10 @@ async function seedDemo() {
|
||||
.limit(1);
|
||||
|
||||
if (commitment) {
|
||||
await db.insert(allocations).values({
|
||||
if (!isAllocationStatus(d.allocation.status)) {
|
||||
throw new Error(`Invalid demo allocation status: ${d.allocation.status}`);
|
||||
}
|
||||
const allocationRow = {
|
||||
capacityCommitmentId: commitmentId,
|
||||
demandDealId: deal.id,
|
||||
gpuHours: String(
|
||||
@@ -568,7 +598,8 @@ async function seedDemo() {
|
||||
priority: d.allocation.status === 'planned' ? 100 : 10,
|
||||
holdExpiresAt: d.allocation.holdDays ? at(d.allocation.holdDays) : null,
|
||||
notes: d.account,
|
||||
});
|
||||
} satisfies NewAllocation;
|
||||
await db.insert(allocations).values(allocationRow);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -613,20 +644,19 @@ async function seedDemo() {
|
||||
.limit(1);
|
||||
|
||||
if (commitment && !existingResearch) {
|
||||
await db
|
||||
.insert(allocations)
|
||||
.values({
|
||||
capacityCommitmentId: coreweave,
|
||||
internalTeam: 'research',
|
||||
gpuHours: String(Math.round(Number(commitment.totalGpuHours) * 0.08)),
|
||||
pricePerGpuHourCents: 0,
|
||||
startsAt: commitment.startsAt,
|
||||
endsAt: commitment.endsAt,
|
||||
status: 'active',
|
||||
guaranteeType: 'internal',
|
||||
priority: 200,
|
||||
notes: RESEARCH_NOTE,
|
||||
});
|
||||
const researchAllocationRow = {
|
||||
capacityCommitmentId: coreweave,
|
||||
internalTeam: 'research',
|
||||
gpuHours: String(Math.round(Number(commitment.totalGpuHours) * 0.08)),
|
||||
pricePerGpuHourCents: 0,
|
||||
startsAt: commitment.startsAt,
|
||||
endsAt: commitment.endsAt,
|
||||
status: 'active',
|
||||
guaranteeType: 'internal',
|
||||
priority: 200,
|
||||
notes: RESEARCH_NOTE,
|
||||
} satisfies NewAllocation;
|
||||
await db.insert(allocations).values(researchAllocationRow);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user