@friggframework/core 2.0.0--canary.622.16ee19d.0 → 2.0.0--canary.622.faffe5f.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/application/commands/usage-commands.js +5 -4
- package/application/index.js +9 -18
- package/core/create-handler.js +17 -6
- package/generated/prisma-mongodb/edge.js +3 -3
- package/generated/prisma-mongodb/index.d.ts +80 -55
- package/generated/prisma-mongodb/index.js +3 -3
- package/generated/prisma-mongodb/package.json +1 -1
- package/generated/prisma-mongodb/schema.prisma +5 -3
- package/generated/prisma-mongodb/wasm.js +3 -3
- package/generated/prisma-postgresql/edge.js +3 -3
- package/generated/prisma-postgresql/index.d.ts +92 -16
- package/generated/prisma-postgresql/index.js +3 -3
- package/generated/prisma-postgresql/package.json +1 -1
- package/generated/prisma-postgresql/schema.prisma +5 -3
- package/generated/prisma-postgresql/wasm.js +3 -3
- package/handlers/integration-event-dispatcher.js +1 -1
- package/index.js +1 -1
- package/integrations/integration-base.js +23 -4
- package/modules/requester/requester.js +12 -2
- package/package.json +5 -5
- package/prisma-mongodb/schema.prisma +5 -3
- package/prisma-postgresql/migrations/20260705000000_create_usage_counter/migration.sql +3 -3
- package/prisma-postgresql/schema.prisma +5 -3
- package/reporting/reporting-router.js +1 -1
- package/reporting/use-cases/list-integrations-report.js +2 -2
- package/telemetry/README.md +22 -6
- package/telemetry/bind-telemetry-context.js +1 -1
- package/telemetry/canonical-counters.js +1 -1
- package/telemetry/exporters/exporter-factory.js +2 -2
- package/telemetry/instrument-handler.js +1 -1
- package/telemetry/no-op-telemetry.js +1 -1
- package/telemetry/north-star.js +1 -1
- package/telemetry/otel-telemetry.js +1 -1
- package/telemetry/plugin-subscribers-singleton.js +2 -2
- package/telemetry/plugin-subscribers.js +1 -1
- package/telemetry/telemetry-config.js +1 -1
- package/telemetry/telemetry-context.js +1 -1
- package/telemetry/telemetry-event-bus.js +2 -2
- package/telemetry/telemetry-service.js +3 -3
- package/telemetry/telemetry-singleton.js +1 -1
- package/telemetry/usage-rollup-singleton.js +1 -1
- package/telemetry/usage-rollup-subscriber.js +3 -3
- package/telemetry/usage-windows.js +1 -1
- package/usage/README.md +6 -4
- package/usage/repositories/usage-repository-documentdb.js +193 -9
- package/usage/repositories/usage-repository-interface.js +1 -1
- package/usage/repositories/usage-repository-postgres.js +22 -12
- package/usage/tracked-metrics.js +1 -1
|
@@ -380,13 +380,13 @@ const config = {
|
|
|
380
380
|
}
|
|
381
381
|
}
|
|
382
382
|
},
|
|
383
|
-
"inlineSchema": "// Frigg Framework - Prisma Schema (PostgreSQL)\n// PostgreSQL database schema for enterprise integration platform\n// Converted from MongoDB schema for relational database support\n\ngenerator client {\n provider = \"prisma-client-js\"\n output = \"../generated/prisma-postgresql\"\n binaryTargets = [\"native\", \"rhel-openssl-3.0.x\"] // native for local dev, rhel for Lambda deployment\n // Library engine (default since Prisma 3.x): Rust query engine loads as a\n // Node-API addon inside the same process. The binary engine forks a child\n // query-engine subprocess and communicates over a local HTTP/IPC pipe with\n // NO client-side timeout — a zombied child wedges the Node process until\n // Lambda's 900s cap. Switching to library eliminates that entire class of\n // silent hangs. See friggframework/frigg#580 for the investigation.\n engineType = \"library\"\n}\n\ndatasource db {\n provider = \"postgresql\"\n url = env(\"DATABASE_URL\")\n}\n\n// ============================================================================\n// USER MODELS\n// ============================================================================\n\n/// User model with discriminator pattern support\n/// Replaces Mongoose discriminators (IndividualUser, OrganizationUser)\nmodel User {\n id Int @id @default(autoincrement())\n type UserType\n\n // Timestamps\n createdAt DateTime @default(now())\n updatedAt DateTime @updatedAt\n\n // IndividualUser fields (nullable for organizations)\n email String?\n username String?\n hashword String? // Bcrypt hashed password (handled in application layer)\n appUserId String?\n organizationId Int?\n\n // Self-referential relation for organization membership\n organization User? @relation(\"OrgMembers\", fields: [organizationId], references: [id], onDelete: NoAction, onUpdate: NoAction)\n members User[] @relation(\"OrgMembers\")\n\n // OrganizationUser fields (nullable for individuals)\n appOrgId String?\n name String?\n\n // Relations\n tokens Token[]\n credentials Credential[]\n entities Entity[]\n integrations Integration[]\n processes Process[]\n\n @@unique([username, appUserId])\n @@index([type])\n @@index([appUserId])\n @@index([organizationId])\n}\n\nenum UserType {\n INDIVIDUAL\n ORGANIZATION\n}\n\n// ============================================================================\n// AUTHENTICATION MODELS\n// ============================================================================\n\n/// Authentication tokens with expiration\n/// Bcrypt hashed tokens stored (handled in application layer)\nmodel Token {\n id Int @id @default(autoincrement())\n token String // Bcrypt hashed\n created DateTime @default(now())\n expires DateTime?\n userId Int\n user User @relation(fields: [userId], references: [id], onDelete: Cascade)\n\n @@index([userId])\n @@index([expires])\n}\n\n// ============================================================================\n// CREDENTIAL & ENTITY MODELS\n// ============================================================================\n\n/// OAuth credentials and API tokens\n/// All sensitive data encrypted with KMS at rest\nmodel Credential {\n id Int @id @default(autoincrement())\n userId Int?\n user User? @relation(fields: [userId], references: [id], onDelete: Cascade)\n authIsValid Boolean?\n externalId String?\n\n // Dynamic OAuth fields stored as JSON (encrypted via Prisma middleware)\n // Contains: access_token, refresh_token, domain, expires_in, token_type, etc.\n data Json @default(\"{}\")\n\n createdAt DateTime @default(now())\n updatedAt DateTime @updatedAt\n\n // Relations\n entities Entity[]\n\n @@index([userId])\n @@index([externalId])\n}\n\n/// External service entities (API connections)\nmodel Entity {\n id Int @id @default(autoincrement())\n credentialId Int?\n credential Credential? @relation(fields: [credentialId], references: [id], onDelete: SetNull)\n userId Int?\n user User? @relation(fields: [userId], references: [id], onDelete: Cascade)\n name String?\n moduleName String?\n externalId String?\n\n data Json @default(\"{}\")\n\n createdAt DateTime @default(now())\n updatedAt DateTime @updatedAt\n\n // Relations - many-to-many with implicit join tables\n integrations Integration[]\n syncs Sync[]\n\n dataIdentifiers DataIdentifier[]\n associationObjects AssociationObject[]\n\n @@index([userId])\n @@index([externalId])\n @@index([moduleName])\n @@index([credentialId])\n}\n\n// ============================================================================\n// INTEGRATION MODELS\n// ============================================================================\n\n/// Main integration configuration and state\nmodel Integration {\n id Int @id @default(autoincrement())\n userId Int?\n user User? @relation(fields: [userId], references: [id], onDelete: Cascade)\n status IntegrationStatus @default(IN_CREATION)\n\n // Configuration and version\n config Json? // Integration configuration object\n version String?\n\n // Entity references (many-to-many via implicit join table)\n entities Entity[]\n\n // Message arrays (stored as JSON)\n errors Json @default(\"[]\")\n warnings Json @default(\"[]\")\n info Json @default(\"[]\")\n logs Json @default(\"[]\")\n\n createdAt DateTime @default(now())\n updatedAt DateTime @updatedAt\n\n // Relations\n associations Association[]\n syncs Sync[]\n mappings IntegrationMapping[]\n processes Process[]\n\n @@index([userId])\n @@index([status])\n}\n\nenum IntegrationStatus {\n ENABLED\n NEEDS_CONFIG\n PROCESSING\n DISABLED\n ERROR\n IN_CREATION\n IN_DELETION\n}\n\n/// Integration-specific data mappings\n/// All mapping data encrypted with KMS\nmodel IntegrationMapping {\n id Int @id @default(autoincrement())\n integrationId Int\n integration Integration @relation(fields: [integrationId], references: [id], onDelete: Cascade)\n sourceId String?\n\n // Encrypted mapping data (handled via Prisma middleware)\n mapping Json?\n\n createdAt DateTime @default(now())\n updatedAt DateTime @updatedAt\n\n @@unique([integrationId, sourceId])\n @@index([integrationId])\n @@index([sourceId])\n}\n\n// ============================================================================\n// SYNC MODELS\n// ============================================================================\n\n/// Bidirectional data synchronization tracking\nmodel Sync {\n id Int @id @default(autoincrement())\n integrationId Int?\n integration Integration? @relation(fields: [integrationId], references: [id], onDelete: Cascade)\n\n // Entity references (many-to-many via implicit join table)\n entities Entity[]\n\n hash String\n name String\n\n // Data identifiers (extracted to separate model)\n dataIdentifiers DataIdentifier[]\n\n @@index([integrationId])\n @@index([hash])\n @@index([name])\n}\n\n/// Data identifier for sync operations\n/// Replaces nested array structure in Mongoose\nmodel DataIdentifier {\n id Int @id @default(autoincrement())\n syncId Int?\n sync Sync? @relation(fields: [syncId], references: [id], onDelete: Cascade)\n entityId Int\n entity Entity @relation(fields: [entityId], references: [id], onDelete: Cascade)\n\n // Identifier data (can be any structure)\n idData Json\n\n hash String\n\n @@index([syncId])\n @@index([entityId])\n @@index([hash])\n}\n\n// ============================================================================\n// ASSOCIATION MODELS\n// ============================================================================\n\n/// Entity associations with cardinality tracking\nmodel Association {\n id Int @id @default(autoincrement())\n integrationId Int\n integration Integration @relation(fields: [integrationId], references: [id], onDelete: Cascade)\n name String\n type AssociationType\n primaryObject String\n\n // Associated objects (extracted to separate model)\n objects AssociationObject[]\n\n @@index([integrationId])\n @@index([name])\n}\n\n/// Association object entry\n/// Replaces nested array structure in Mongoose\nmodel AssociationObject {\n id Int @id @default(autoincrement())\n associationId Int\n association Association @relation(fields: [associationId], references: [id], onDelete: Cascade)\n entityId Int\n entity Entity @relation(fields: [entityId], references: [id], onDelete: Cascade)\n objectType String\n objId String\n metadata Json? // Optional metadata\n\n @@index([associationId])\n @@index([entityId])\n}\n\nenum AssociationType {\n ONE_TO_MANY\n ONE_TO_ONE\n MANY_TO_ONE\n}\n\n// ============================================================================\n// PROCESS MODELS\n// ============================================================================\n\n/// Generic Process Model - tracks any long-running operation\n/// Used for: CRM syncs, data migrations, bulk operations, etc.\nmodel Process {\n id Int @id @default(autoincrement())\n\n // Core references\n userId Int\n user User @relation(fields: [userId], references: [id], onDelete: Cascade)\n integrationId Int\n integration Integration @relation(fields: [integrationId], references: [id], onDelete: Cascade)\n\n // Process identification\n name String // e.g., \"zoho-crm-contact-sync\", \"pipedrive-lead-sync\"\n type String // e.g., \"CRM_SYNC\", \"DATA_MIGRATION\", \"BULK_OPERATION\"\n\n // State machine\n state String // Current state (integration-defined states)\n\n // Flexible storage\n context Json @default(\"{}\") // Process-specific data (pagination, metadata, etc.)\n results Json @default(\"{}\") // Process results and metrics\n\n // Hierarchy support - self-referential relation\n parentProcessId Int?\n parentProcess Process? @relation(\"ProcessHierarchy\", fields: [parentProcessId], references: [id], onDelete: SetNull)\n childProcesses Process[] @relation(\"ProcessHierarchy\")\n\n // Timestamps\n createdAt DateTime @default(now())\n updatedAt DateTime @updatedAt\n\n @@index([userId])\n @@index([integrationId])\n @@index([type])\n @@index([state])\n @@index([name])\n @@index([parentProcessId])\n}\n\n// ============================================================================\n// UTILITY MODELS\n// ============================================================================\n\n/// Generic state storage\nmodel State {\n id Int @id @default(autoincrement())\n state Json?\n}\n\n/// AWS API Gateway WebSocket connection tracking\nmodel WebsocketConnection {\n id Int @id @default(autoincrement())\n connectionId String?\n\n @@index([connectionId])\n}\n\n// ============================================================================\n// ADMIN PROCESS MODELS\n// ============================================================================\n\n/// Admin process state machine\nenum AdminScriptExecutionState {\n PENDING\n RUNNING\n COMPLETED\n FAILED\n}\n\n/// Admin process tracking (like Process but without user/integration FK)\n/// Used for: admin scripts, db migrations, system maintenance tasks\nmodel AdminScriptExecution {\n id Int @id @default(autoincrement())\n name String // e.g., \"oauth-token-refresh\", \"db-migration-xyz\"\n type String // e.g., \"ADMIN_SCRIPT\", \"DB_MIGRATION\"\n\n // State machine\n state AdminScriptExecutionState @default(PENDING)\n\n // Flexible storage (mirrors Process model pattern)\n context Json @default(\"{}\") // input, trigger, audit info, script version\n results Json @default(\"{}\") // output, logs, metrics, errors\n\n // Hierarchy support - self-referential relation\n parentExecutionId Int?\n parentExecution AdminScriptExecution? @relation(\"AdminScriptExecutionHierarchy\", fields: [parentExecutionId], references: [id], onDelete: SetNull)\n childExecutions AdminScriptExecution[] @relation(\"AdminScriptExecutionHierarchy\")\n\n // Timestamps\n createdAt DateTime @default(now())\n updatedAt DateTime @updatedAt\n\n @@index([name, createdAt(sort: Desc)])\n @@index([state])\n @@index([type])\n}\n\n/// Script scheduling configuration for hybrid scheduling (SQS + EventBridge)\nmodel ScriptSchedule {\n id Int @id @default(autoincrement())\n scriptName String @unique\n enabled Boolean @default(false)\n cronExpression String?\n timezone String @default(\"UTC\")\n lastTriggeredAt DateTime?\n nextTriggerAt DateTime?\n\n // External Scheduler (e.g., AWS EventBridge Scheduler)\n externalScheduleId String?\n externalScheduleName String?\n\n createdAt DateTime @default(now())\n updatedAt DateTime @updatedAt\n\n @@index([enabled])\n}\n\n/// ADR-011 durable usage counters. Deliberately isolated (ADR-010 Decision 3):\n/// NO userId and NO foreign key to Integration, so a user-scoped query can never\n/// return a usage row and usage history survives integration deletion. Not in\n/// the encryption registry — dimensions are bounded, non-sensitive.\nmodel UsageCounter {\n id Int @id @default(autoincrement())\n\n integrationId String\n integrationType String\n metric String\n window String // e.g. \"day:2026-07-05\" or \"hour:2026-07-05T14\"\n value Int @default(0)\n\n createdAt DateTime @default(now())\n updatedAt DateTime @updatedAt\n\n @@unique([integrationId, integrationType, metric, window])\n @@index([metric])\n @@index([integrationType])\n}\n",
|
|
384
|
-
"inlineSchemaHash": "
|
|
383
|
+
"inlineSchema": "// Frigg Framework - Prisma Schema (PostgreSQL)\n// PostgreSQL database schema for enterprise integration platform\n// Converted from MongoDB schema for relational database support\n\ngenerator client {\n provider = \"prisma-client-js\"\n output = \"../generated/prisma-postgresql\"\n binaryTargets = [\"native\", \"rhel-openssl-3.0.x\"] // native for local dev, rhel for Lambda deployment\n // Library engine (default since Prisma 3.x): Rust query engine loads as a\n // Node-API addon inside the same process. The binary engine forks a child\n // query-engine subprocess and communicates over a local HTTP/IPC pipe with\n // NO client-side timeout — a zombied child wedges the Node process until\n // Lambda's 900s cap. Switching to library eliminates that entire class of\n // silent hangs. See friggframework/frigg#580 for the investigation.\n engineType = \"library\"\n}\n\ndatasource db {\n provider = \"postgresql\"\n url = env(\"DATABASE_URL\")\n}\n\n// ============================================================================\n// USER MODELS\n// ============================================================================\n\n/// User model with discriminator pattern support\n/// Replaces Mongoose discriminators (IndividualUser, OrganizationUser)\nmodel User {\n id Int @id @default(autoincrement())\n type UserType\n\n // Timestamps\n createdAt DateTime @default(now())\n updatedAt DateTime @updatedAt\n\n // IndividualUser fields (nullable for organizations)\n email String?\n username String?\n hashword String? // Bcrypt hashed password (handled in application layer)\n appUserId String?\n organizationId Int?\n\n // Self-referential relation for organization membership\n organization User? @relation(\"OrgMembers\", fields: [organizationId], references: [id], onDelete: NoAction, onUpdate: NoAction)\n members User[] @relation(\"OrgMembers\")\n\n // OrganizationUser fields (nullable for individuals)\n appOrgId String?\n name String?\n\n // Relations\n tokens Token[]\n credentials Credential[]\n entities Entity[]\n integrations Integration[]\n processes Process[]\n\n @@unique([username, appUserId])\n @@index([type])\n @@index([appUserId])\n @@index([organizationId])\n}\n\nenum UserType {\n INDIVIDUAL\n ORGANIZATION\n}\n\n// ============================================================================\n// AUTHENTICATION MODELS\n// ============================================================================\n\n/// Authentication tokens with expiration\n/// Bcrypt hashed tokens stored (handled in application layer)\nmodel Token {\n id Int @id @default(autoincrement())\n token String // Bcrypt hashed\n created DateTime @default(now())\n expires DateTime?\n userId Int\n user User @relation(fields: [userId], references: [id], onDelete: Cascade)\n\n @@index([userId])\n @@index([expires])\n}\n\n// ============================================================================\n// CREDENTIAL & ENTITY MODELS\n// ============================================================================\n\n/// OAuth credentials and API tokens\n/// All sensitive data encrypted with KMS at rest\nmodel Credential {\n id Int @id @default(autoincrement())\n userId Int?\n user User? @relation(fields: [userId], references: [id], onDelete: Cascade)\n authIsValid Boolean?\n externalId String?\n\n // Dynamic OAuth fields stored as JSON (encrypted via Prisma middleware)\n // Contains: access_token, refresh_token, domain, expires_in, token_type, etc.\n data Json @default(\"{}\")\n\n createdAt DateTime @default(now())\n updatedAt DateTime @updatedAt\n\n // Relations\n entities Entity[]\n\n @@index([userId])\n @@index([externalId])\n}\n\n/// External service entities (API connections)\nmodel Entity {\n id Int @id @default(autoincrement())\n credentialId Int?\n credential Credential? @relation(fields: [credentialId], references: [id], onDelete: SetNull)\n userId Int?\n user User? @relation(fields: [userId], references: [id], onDelete: Cascade)\n name String?\n moduleName String?\n externalId String?\n\n data Json @default(\"{}\")\n\n createdAt DateTime @default(now())\n updatedAt DateTime @updatedAt\n\n // Relations - many-to-many with implicit join tables\n integrations Integration[]\n syncs Sync[]\n\n dataIdentifiers DataIdentifier[]\n associationObjects AssociationObject[]\n\n @@index([userId])\n @@index([externalId])\n @@index([moduleName])\n @@index([credentialId])\n}\n\n// ============================================================================\n// INTEGRATION MODELS\n// ============================================================================\n\n/// Main integration configuration and state\nmodel Integration {\n id Int @id @default(autoincrement())\n userId Int?\n user User? @relation(fields: [userId], references: [id], onDelete: Cascade)\n status IntegrationStatus @default(IN_CREATION)\n\n // Configuration and version\n config Json? // Integration configuration object\n version String?\n\n // Entity references (many-to-many via implicit join table)\n entities Entity[]\n\n // Message arrays (stored as JSON)\n errors Json @default(\"[]\")\n warnings Json @default(\"[]\")\n info Json @default(\"[]\")\n logs Json @default(\"[]\")\n\n createdAt DateTime @default(now())\n updatedAt DateTime @updatedAt\n\n // Relations\n associations Association[]\n syncs Sync[]\n mappings IntegrationMapping[]\n processes Process[]\n\n @@index([userId])\n @@index([status])\n}\n\nenum IntegrationStatus {\n ENABLED\n NEEDS_CONFIG\n PROCESSING\n DISABLED\n ERROR\n IN_CREATION\n IN_DELETION\n}\n\n/// Integration-specific data mappings\n/// All mapping data encrypted with KMS\nmodel IntegrationMapping {\n id Int @id @default(autoincrement())\n integrationId Int\n integration Integration @relation(fields: [integrationId], references: [id], onDelete: Cascade)\n sourceId String?\n\n // Encrypted mapping data (handled via Prisma middleware)\n mapping Json?\n\n createdAt DateTime @default(now())\n updatedAt DateTime @updatedAt\n\n @@unique([integrationId, sourceId])\n @@index([integrationId])\n @@index([sourceId])\n}\n\n// ============================================================================\n// SYNC MODELS\n// ============================================================================\n\n/// Bidirectional data synchronization tracking\nmodel Sync {\n id Int @id @default(autoincrement())\n integrationId Int?\n integration Integration? @relation(fields: [integrationId], references: [id], onDelete: Cascade)\n\n // Entity references (many-to-many via implicit join table)\n entities Entity[]\n\n hash String\n name String\n\n // Data identifiers (extracted to separate model)\n dataIdentifiers DataIdentifier[]\n\n @@index([integrationId])\n @@index([hash])\n @@index([name])\n}\n\n/// Data identifier for sync operations\n/// Replaces nested array structure in Mongoose\nmodel DataIdentifier {\n id Int @id @default(autoincrement())\n syncId Int?\n sync Sync? @relation(fields: [syncId], references: [id], onDelete: Cascade)\n entityId Int\n entity Entity @relation(fields: [entityId], references: [id], onDelete: Cascade)\n\n // Identifier data (can be any structure)\n idData Json\n\n hash String\n\n @@index([syncId])\n @@index([entityId])\n @@index([hash])\n}\n\n// ============================================================================\n// ASSOCIATION MODELS\n// ============================================================================\n\n/// Entity associations with cardinality tracking\nmodel Association {\n id Int @id @default(autoincrement())\n integrationId Int\n integration Integration @relation(fields: [integrationId], references: [id], onDelete: Cascade)\n name String\n type AssociationType\n primaryObject String\n\n // Associated objects (extracted to separate model)\n objects AssociationObject[]\n\n @@index([integrationId])\n @@index([name])\n}\n\n/// Association object entry\n/// Replaces nested array structure in Mongoose\nmodel AssociationObject {\n id Int @id @default(autoincrement())\n associationId Int\n association Association @relation(fields: [associationId], references: [id], onDelete: Cascade)\n entityId Int\n entity Entity @relation(fields: [entityId], references: [id], onDelete: Cascade)\n objectType String\n objId String\n metadata Json? // Optional metadata\n\n @@index([associationId])\n @@index([entityId])\n}\n\nenum AssociationType {\n ONE_TO_MANY\n ONE_TO_ONE\n MANY_TO_ONE\n}\n\n// ============================================================================\n// PROCESS MODELS\n// ============================================================================\n\n/// Generic Process Model - tracks any long-running operation\n/// Used for: CRM syncs, data migrations, bulk operations, etc.\nmodel Process {\n id Int @id @default(autoincrement())\n\n // Core references\n userId Int\n user User @relation(fields: [userId], references: [id], onDelete: Cascade)\n integrationId Int\n integration Integration @relation(fields: [integrationId], references: [id], onDelete: Cascade)\n\n // Process identification\n name String // e.g., \"zoho-crm-contact-sync\", \"pipedrive-lead-sync\"\n type String // e.g., \"CRM_SYNC\", \"DATA_MIGRATION\", \"BULK_OPERATION\"\n\n // State machine\n state String // Current state (integration-defined states)\n\n // Flexible storage\n context Json @default(\"{}\") // Process-specific data (pagination, metadata, etc.)\n results Json @default(\"{}\") // Process results and metrics\n\n // Hierarchy support - self-referential relation\n parentProcessId Int?\n parentProcess Process? @relation(\"ProcessHierarchy\", fields: [parentProcessId], references: [id], onDelete: SetNull)\n childProcesses Process[] @relation(\"ProcessHierarchy\")\n\n // Timestamps\n createdAt DateTime @default(now())\n updatedAt DateTime @updatedAt\n\n @@index([userId])\n @@index([integrationId])\n @@index([type])\n @@index([state])\n @@index([name])\n @@index([parentProcessId])\n}\n\n// ============================================================================\n// UTILITY MODELS\n// ============================================================================\n\n/// Generic state storage\nmodel State {\n id Int @id @default(autoincrement())\n state Json?\n}\n\n/// AWS API Gateway WebSocket connection tracking\nmodel WebsocketConnection {\n id Int @id @default(autoincrement())\n connectionId String?\n\n @@index([connectionId])\n}\n\n// ============================================================================\n// ADMIN PROCESS MODELS\n// ============================================================================\n\n/// Admin process state machine\nenum AdminScriptExecutionState {\n PENDING\n RUNNING\n COMPLETED\n FAILED\n}\n\n/// Admin process tracking (like Process but without user/integration FK)\n/// Used for: admin scripts, db migrations, system maintenance tasks\nmodel AdminScriptExecution {\n id Int @id @default(autoincrement())\n name String // e.g., \"oauth-token-refresh\", \"db-migration-xyz\"\n type String // e.g., \"ADMIN_SCRIPT\", \"DB_MIGRATION\"\n\n // State machine\n state AdminScriptExecutionState @default(PENDING)\n\n // Flexible storage (mirrors Process model pattern)\n context Json @default(\"{}\") // input, trigger, audit info, script version\n results Json @default(\"{}\") // output, logs, metrics, errors\n\n // Hierarchy support - self-referential relation\n parentExecutionId Int?\n parentExecution AdminScriptExecution? @relation(\"AdminScriptExecutionHierarchy\", fields: [parentExecutionId], references: [id], onDelete: SetNull)\n childExecutions AdminScriptExecution[] @relation(\"AdminScriptExecutionHierarchy\")\n\n // Timestamps\n createdAt DateTime @default(now())\n updatedAt DateTime @updatedAt\n\n @@index([name, createdAt(sort: Desc)])\n @@index([state])\n @@index([type])\n}\n\n/// Script scheduling configuration for hybrid scheduling (SQS + EventBridge)\nmodel ScriptSchedule {\n id Int @id @default(autoincrement())\n scriptName String @unique\n enabled Boolean @default(false)\n cronExpression String?\n timezone String @default(\"UTC\")\n lastTriggeredAt DateTime?\n nextTriggerAt DateTime?\n\n // External Scheduler (e.g., AWS EventBridge Scheduler)\n externalScheduleId String?\n externalScheduleName String?\n\n createdAt DateTime @default(now())\n updatedAt DateTime @updatedAt\n\n @@index([enabled])\n}\n\n/// ADR-011 durable usage counters. Deliberately isolated (ADR-010 Decision 3):\n/// NO userId and NO foreign key to Integration, so a user-scoped query can never\n/// return a usage row and usage history survives integration deletion. Not in\n/// the encryption registry — dimensions are bounded, non-sensitive.\nmodel UsageCounter {\n id Int @id @default(autoincrement())\n\n integrationId String\n integrationType String\n metric String\n window String // e.g. \"day:2026-07-05\" or \"hour:2026-07-05T14\"\n value BigInt @default(0) // BigInt: a busy day/hour counter can exceed 32-bit\n\n createdAt DateTime @default(now())\n updatedAt DateTime @updatedAt\n\n @@unique([integrationId, integrationType, metric, window])\n // Covers totals() (metric + window prefix/range, no integrationType filter).\n @@index([metric, window])\n // Covers series() (metric + integrationType + window range).\n @@index([metric, integrationType, window])\n}\n",
|
|
384
|
+
"inlineSchemaHash": "631b0201a53d34f70785c0767b3b8b8aaa2a3d6087e0861dbbf93016ace630f0",
|
|
385
385
|
"copyEngine": true
|
|
386
386
|
}
|
|
387
387
|
config.dirname = '/'
|
|
388
388
|
|
|
389
|
-
config.runtimeDataModel = JSON.parse("{\"models\":{\"User\":{\"fields\":[{\"name\":\"id\",\"kind\":\"scalar\",\"type\":\"Int\"},{\"name\":\"type\",\"kind\":\"enum\",\"type\":\"UserType\"},{\"name\":\"createdAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"updatedAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"email\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"username\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"hashword\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"appUserId\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"organizationId\",\"kind\":\"scalar\",\"type\":\"Int\"},{\"name\":\"organization\",\"kind\":\"object\",\"type\":\"User\",\"relationName\":\"OrgMembers\"},{\"name\":\"members\",\"kind\":\"object\",\"type\":\"User\",\"relationName\":\"OrgMembers\"},{\"name\":\"appOrgId\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"name\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"tokens\",\"kind\":\"object\",\"type\":\"Token\",\"relationName\":\"TokenToUser\"},{\"name\":\"credentials\",\"kind\":\"object\",\"type\":\"Credential\",\"relationName\":\"CredentialToUser\"},{\"name\":\"entities\",\"kind\":\"object\",\"type\":\"Entity\",\"relationName\":\"EntityToUser\"},{\"name\":\"integrations\",\"kind\":\"object\",\"type\":\"Integration\",\"relationName\":\"IntegrationToUser\"},{\"name\":\"processes\",\"kind\":\"object\",\"type\":\"Process\",\"relationName\":\"ProcessToUser\"}],\"dbName\":null},\"Token\":{\"fields\":[{\"name\":\"id\",\"kind\":\"scalar\",\"type\":\"Int\"},{\"name\":\"token\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"created\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"expires\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"userId\",\"kind\":\"scalar\",\"type\":\"Int\"},{\"name\":\"user\",\"kind\":\"object\",\"type\":\"User\",\"relationName\":\"TokenToUser\"}],\"dbName\":null},\"Credential\":{\"fields\":[{\"name\":\"id\",\"kind\":\"scalar\",\"type\":\"Int\"},{\"name\":\"userId\",\"kind\":\"scalar\",\"type\":\"Int\"},{\"name\":\"user\",\"kind\":\"object\",\"type\":\"User\",\"relationName\":\"CredentialToUser\"},{\"name\":\"authIsValid\",\"kind\":\"scalar\",\"type\":\"Boolean\"},{\"name\":\"externalId\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"data\",\"kind\":\"scalar\",\"type\":\"Json\"},{\"name\":\"createdAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"updatedAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"entities\",\"kind\":\"object\",\"type\":\"Entity\",\"relationName\":\"CredentialToEntity\"}],\"dbName\":null},\"Entity\":{\"fields\":[{\"name\":\"id\",\"kind\":\"scalar\",\"type\":\"Int\"},{\"name\":\"credentialId\",\"kind\":\"scalar\",\"type\":\"Int\"},{\"name\":\"credential\",\"kind\":\"object\",\"type\":\"Credential\",\"relationName\":\"CredentialToEntity\"},{\"name\":\"userId\",\"kind\":\"scalar\",\"type\":\"Int\"},{\"name\":\"user\",\"kind\":\"object\",\"type\":\"User\",\"relationName\":\"EntityToUser\"},{\"name\":\"name\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"moduleName\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"externalId\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"data\",\"kind\":\"scalar\",\"type\":\"Json\"},{\"name\":\"createdAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"updatedAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"integrations\",\"kind\":\"object\",\"type\":\"Integration\",\"relationName\":\"EntityToIntegration\"},{\"name\":\"syncs\",\"kind\":\"object\",\"type\":\"Sync\",\"relationName\":\"EntityToSync\"},{\"name\":\"dataIdentifiers\",\"kind\":\"object\",\"type\":\"DataIdentifier\",\"relationName\":\"DataIdentifierToEntity\"},{\"name\":\"associationObjects\",\"kind\":\"object\",\"type\":\"AssociationObject\",\"relationName\":\"AssociationObjectToEntity\"}],\"dbName\":null},\"Integration\":{\"fields\":[{\"name\":\"id\",\"kind\":\"scalar\",\"type\":\"Int\"},{\"name\":\"userId\",\"kind\":\"scalar\",\"type\":\"Int\"},{\"name\":\"user\",\"kind\":\"object\",\"type\":\"User\",\"relationName\":\"IntegrationToUser\"},{\"name\":\"status\",\"kind\":\"enum\",\"type\":\"IntegrationStatus\"},{\"name\":\"config\",\"kind\":\"scalar\",\"type\":\"Json\"},{\"name\":\"version\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"entities\",\"kind\":\"object\",\"type\":\"Entity\",\"relationName\":\"EntityToIntegration\"},{\"name\":\"errors\",\"kind\":\"scalar\",\"type\":\"Json\"},{\"name\":\"warnings\",\"kind\":\"scalar\",\"type\":\"Json\"},{\"name\":\"info\",\"kind\":\"scalar\",\"type\":\"Json\"},{\"name\":\"logs\",\"kind\":\"scalar\",\"type\":\"Json\"},{\"name\":\"createdAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"updatedAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"associations\",\"kind\":\"object\",\"type\":\"Association\",\"relationName\":\"AssociationToIntegration\"},{\"name\":\"syncs\",\"kind\":\"object\",\"type\":\"Sync\",\"relationName\":\"IntegrationToSync\"},{\"name\":\"mappings\",\"kind\":\"object\",\"type\":\"IntegrationMapping\",\"relationName\":\"IntegrationToIntegrationMapping\"},{\"name\":\"processes\",\"kind\":\"object\",\"type\":\"Process\",\"relationName\":\"IntegrationToProcess\"}],\"dbName\":null},\"IntegrationMapping\":{\"fields\":[{\"name\":\"id\",\"kind\":\"scalar\",\"type\":\"Int\"},{\"name\":\"integrationId\",\"kind\":\"scalar\",\"type\":\"Int\"},{\"name\":\"integration\",\"kind\":\"object\",\"type\":\"Integration\",\"relationName\":\"IntegrationToIntegrationMapping\"},{\"name\":\"sourceId\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"mapping\",\"kind\":\"scalar\",\"type\":\"Json\"},{\"name\":\"createdAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"updatedAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"}],\"dbName\":null},\"Sync\":{\"fields\":[{\"name\":\"id\",\"kind\":\"scalar\",\"type\":\"Int\"},{\"name\":\"integrationId\",\"kind\":\"scalar\",\"type\":\"Int\"},{\"name\":\"integration\",\"kind\":\"object\",\"type\":\"Integration\",\"relationName\":\"IntegrationToSync\"},{\"name\":\"entities\",\"kind\":\"object\",\"type\":\"Entity\",\"relationName\":\"EntityToSync\"},{\"name\":\"hash\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"name\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"dataIdentifiers\",\"kind\":\"object\",\"type\":\"DataIdentifier\",\"relationName\":\"DataIdentifierToSync\"}],\"dbName\":null},\"DataIdentifier\":{\"fields\":[{\"name\":\"id\",\"kind\":\"scalar\",\"type\":\"Int\"},{\"name\":\"syncId\",\"kind\":\"scalar\",\"type\":\"Int\"},{\"name\":\"sync\",\"kind\":\"object\",\"type\":\"Sync\",\"relationName\":\"DataIdentifierToSync\"},{\"name\":\"entityId\",\"kind\":\"scalar\",\"type\":\"Int\"},{\"name\":\"entity\",\"kind\":\"object\",\"type\":\"Entity\",\"relationName\":\"DataIdentifierToEntity\"},{\"name\":\"idData\",\"kind\":\"scalar\",\"type\":\"Json\"},{\"name\":\"hash\",\"kind\":\"scalar\",\"type\":\"String\"}],\"dbName\":null},\"Association\":{\"fields\":[{\"name\":\"id\",\"kind\":\"scalar\",\"type\":\"Int\"},{\"name\":\"integrationId\",\"kind\":\"scalar\",\"type\":\"Int\"},{\"name\":\"integration\",\"kind\":\"object\",\"type\":\"Integration\",\"relationName\":\"AssociationToIntegration\"},{\"name\":\"name\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"type\",\"kind\":\"enum\",\"type\":\"AssociationType\"},{\"name\":\"primaryObject\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"objects\",\"kind\":\"object\",\"type\":\"AssociationObject\",\"relationName\":\"AssociationToAssociationObject\"}],\"dbName\":null},\"AssociationObject\":{\"fields\":[{\"name\":\"id\",\"kind\":\"scalar\",\"type\":\"Int\"},{\"name\":\"associationId\",\"kind\":\"scalar\",\"type\":\"Int\"},{\"name\":\"association\",\"kind\":\"object\",\"type\":\"Association\",\"relationName\":\"AssociationToAssociationObject\"},{\"name\":\"entityId\",\"kind\":\"scalar\",\"type\":\"Int\"},{\"name\":\"entity\",\"kind\":\"object\",\"type\":\"Entity\",\"relationName\":\"AssociationObjectToEntity\"},{\"name\":\"objectType\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"objId\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"metadata\",\"kind\":\"scalar\",\"type\":\"Json\"}],\"dbName\":null},\"Process\":{\"fields\":[{\"name\":\"id\",\"kind\":\"scalar\",\"type\":\"Int\"},{\"name\":\"userId\",\"kind\":\"scalar\",\"type\":\"Int\"},{\"name\":\"user\",\"kind\":\"object\",\"type\":\"User\",\"relationName\":\"ProcessToUser\"},{\"name\":\"integrationId\",\"kind\":\"scalar\",\"type\":\"Int\"},{\"name\":\"integration\",\"kind\":\"object\",\"type\":\"Integration\",\"relationName\":\"IntegrationToProcess\"},{\"name\":\"name\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"type\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"state\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"context\",\"kind\":\"scalar\",\"type\":\"Json\"},{\"name\":\"results\",\"kind\":\"scalar\",\"type\":\"Json\"},{\"name\":\"parentProcessId\",\"kind\":\"scalar\",\"type\":\"Int\"},{\"name\":\"parentProcess\",\"kind\":\"object\",\"type\":\"Process\",\"relationName\":\"ProcessHierarchy\"},{\"name\":\"childProcesses\",\"kind\":\"object\",\"type\":\"Process\",\"relationName\":\"ProcessHierarchy\"},{\"name\":\"createdAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"updatedAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"}],\"dbName\":null},\"State\":{\"fields\":[{\"name\":\"id\",\"kind\":\"scalar\",\"type\":\"Int\"},{\"name\":\"state\",\"kind\":\"scalar\",\"type\":\"Json\"}],\"dbName\":null},\"WebsocketConnection\":{\"fields\":[{\"name\":\"id\",\"kind\":\"scalar\",\"type\":\"Int\"},{\"name\":\"connectionId\",\"kind\":\"scalar\",\"type\":\"String\"}],\"dbName\":null},\"AdminScriptExecution\":{\"fields\":[{\"name\":\"id\",\"kind\":\"scalar\",\"type\":\"Int\"},{\"name\":\"name\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"type\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"state\",\"kind\":\"enum\",\"type\":\"AdminScriptExecutionState\"},{\"name\":\"context\",\"kind\":\"scalar\",\"type\":\"Json\"},{\"name\":\"results\",\"kind\":\"scalar\",\"type\":\"Json\"},{\"name\":\"parentExecutionId\",\"kind\":\"scalar\",\"type\":\"Int\"},{\"name\":\"parentExecution\",\"kind\":\"object\",\"type\":\"AdminScriptExecution\",\"relationName\":\"AdminScriptExecutionHierarchy\"},{\"name\":\"childExecutions\",\"kind\":\"object\",\"type\":\"AdminScriptExecution\",\"relationName\":\"AdminScriptExecutionHierarchy\"},{\"name\":\"createdAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"updatedAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"}],\"dbName\":null},\"ScriptSchedule\":{\"fields\":[{\"name\":\"id\",\"kind\":\"scalar\",\"type\":\"Int\"},{\"name\":\"scriptName\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"enabled\",\"kind\":\"scalar\",\"type\":\"Boolean\"},{\"name\":\"cronExpression\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"timezone\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"lastTriggeredAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"nextTriggerAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"externalScheduleId\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"externalScheduleName\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"createdAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"updatedAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"}],\"dbName\":null},\"UsageCounter\":{\"fields\":[{\"name\":\"id\",\"kind\":\"scalar\",\"type\":\"Int\"},{\"name\":\"integrationId\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"integrationType\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"metric\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"window\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"value\",\"kind\":\"scalar\",\"type\":\"Int\"},{\"name\":\"createdAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"updatedAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"}],\"dbName\":null}},\"enums\":{},\"types\":{}}")
|
|
389
|
+
config.runtimeDataModel = JSON.parse("{\"models\":{\"User\":{\"fields\":[{\"name\":\"id\",\"kind\":\"scalar\",\"type\":\"Int\"},{\"name\":\"type\",\"kind\":\"enum\",\"type\":\"UserType\"},{\"name\":\"createdAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"updatedAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"email\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"username\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"hashword\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"appUserId\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"organizationId\",\"kind\":\"scalar\",\"type\":\"Int\"},{\"name\":\"organization\",\"kind\":\"object\",\"type\":\"User\",\"relationName\":\"OrgMembers\"},{\"name\":\"members\",\"kind\":\"object\",\"type\":\"User\",\"relationName\":\"OrgMembers\"},{\"name\":\"appOrgId\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"name\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"tokens\",\"kind\":\"object\",\"type\":\"Token\",\"relationName\":\"TokenToUser\"},{\"name\":\"credentials\",\"kind\":\"object\",\"type\":\"Credential\",\"relationName\":\"CredentialToUser\"},{\"name\":\"entities\",\"kind\":\"object\",\"type\":\"Entity\",\"relationName\":\"EntityToUser\"},{\"name\":\"integrations\",\"kind\":\"object\",\"type\":\"Integration\",\"relationName\":\"IntegrationToUser\"},{\"name\":\"processes\",\"kind\":\"object\",\"type\":\"Process\",\"relationName\":\"ProcessToUser\"}],\"dbName\":null},\"Token\":{\"fields\":[{\"name\":\"id\",\"kind\":\"scalar\",\"type\":\"Int\"},{\"name\":\"token\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"created\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"expires\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"userId\",\"kind\":\"scalar\",\"type\":\"Int\"},{\"name\":\"user\",\"kind\":\"object\",\"type\":\"User\",\"relationName\":\"TokenToUser\"}],\"dbName\":null},\"Credential\":{\"fields\":[{\"name\":\"id\",\"kind\":\"scalar\",\"type\":\"Int\"},{\"name\":\"userId\",\"kind\":\"scalar\",\"type\":\"Int\"},{\"name\":\"user\",\"kind\":\"object\",\"type\":\"User\",\"relationName\":\"CredentialToUser\"},{\"name\":\"authIsValid\",\"kind\":\"scalar\",\"type\":\"Boolean\"},{\"name\":\"externalId\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"data\",\"kind\":\"scalar\",\"type\":\"Json\"},{\"name\":\"createdAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"updatedAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"entities\",\"kind\":\"object\",\"type\":\"Entity\",\"relationName\":\"CredentialToEntity\"}],\"dbName\":null},\"Entity\":{\"fields\":[{\"name\":\"id\",\"kind\":\"scalar\",\"type\":\"Int\"},{\"name\":\"credentialId\",\"kind\":\"scalar\",\"type\":\"Int\"},{\"name\":\"credential\",\"kind\":\"object\",\"type\":\"Credential\",\"relationName\":\"CredentialToEntity\"},{\"name\":\"userId\",\"kind\":\"scalar\",\"type\":\"Int\"},{\"name\":\"user\",\"kind\":\"object\",\"type\":\"User\",\"relationName\":\"EntityToUser\"},{\"name\":\"name\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"moduleName\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"externalId\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"data\",\"kind\":\"scalar\",\"type\":\"Json\"},{\"name\":\"createdAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"updatedAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"integrations\",\"kind\":\"object\",\"type\":\"Integration\",\"relationName\":\"EntityToIntegration\"},{\"name\":\"syncs\",\"kind\":\"object\",\"type\":\"Sync\",\"relationName\":\"EntityToSync\"},{\"name\":\"dataIdentifiers\",\"kind\":\"object\",\"type\":\"DataIdentifier\",\"relationName\":\"DataIdentifierToEntity\"},{\"name\":\"associationObjects\",\"kind\":\"object\",\"type\":\"AssociationObject\",\"relationName\":\"AssociationObjectToEntity\"}],\"dbName\":null},\"Integration\":{\"fields\":[{\"name\":\"id\",\"kind\":\"scalar\",\"type\":\"Int\"},{\"name\":\"userId\",\"kind\":\"scalar\",\"type\":\"Int\"},{\"name\":\"user\",\"kind\":\"object\",\"type\":\"User\",\"relationName\":\"IntegrationToUser\"},{\"name\":\"status\",\"kind\":\"enum\",\"type\":\"IntegrationStatus\"},{\"name\":\"config\",\"kind\":\"scalar\",\"type\":\"Json\"},{\"name\":\"version\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"entities\",\"kind\":\"object\",\"type\":\"Entity\",\"relationName\":\"EntityToIntegration\"},{\"name\":\"errors\",\"kind\":\"scalar\",\"type\":\"Json\"},{\"name\":\"warnings\",\"kind\":\"scalar\",\"type\":\"Json\"},{\"name\":\"info\",\"kind\":\"scalar\",\"type\":\"Json\"},{\"name\":\"logs\",\"kind\":\"scalar\",\"type\":\"Json\"},{\"name\":\"createdAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"updatedAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"associations\",\"kind\":\"object\",\"type\":\"Association\",\"relationName\":\"AssociationToIntegration\"},{\"name\":\"syncs\",\"kind\":\"object\",\"type\":\"Sync\",\"relationName\":\"IntegrationToSync\"},{\"name\":\"mappings\",\"kind\":\"object\",\"type\":\"IntegrationMapping\",\"relationName\":\"IntegrationToIntegrationMapping\"},{\"name\":\"processes\",\"kind\":\"object\",\"type\":\"Process\",\"relationName\":\"IntegrationToProcess\"}],\"dbName\":null},\"IntegrationMapping\":{\"fields\":[{\"name\":\"id\",\"kind\":\"scalar\",\"type\":\"Int\"},{\"name\":\"integrationId\",\"kind\":\"scalar\",\"type\":\"Int\"},{\"name\":\"integration\",\"kind\":\"object\",\"type\":\"Integration\",\"relationName\":\"IntegrationToIntegrationMapping\"},{\"name\":\"sourceId\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"mapping\",\"kind\":\"scalar\",\"type\":\"Json\"},{\"name\":\"createdAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"updatedAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"}],\"dbName\":null},\"Sync\":{\"fields\":[{\"name\":\"id\",\"kind\":\"scalar\",\"type\":\"Int\"},{\"name\":\"integrationId\",\"kind\":\"scalar\",\"type\":\"Int\"},{\"name\":\"integration\",\"kind\":\"object\",\"type\":\"Integration\",\"relationName\":\"IntegrationToSync\"},{\"name\":\"entities\",\"kind\":\"object\",\"type\":\"Entity\",\"relationName\":\"EntityToSync\"},{\"name\":\"hash\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"name\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"dataIdentifiers\",\"kind\":\"object\",\"type\":\"DataIdentifier\",\"relationName\":\"DataIdentifierToSync\"}],\"dbName\":null},\"DataIdentifier\":{\"fields\":[{\"name\":\"id\",\"kind\":\"scalar\",\"type\":\"Int\"},{\"name\":\"syncId\",\"kind\":\"scalar\",\"type\":\"Int\"},{\"name\":\"sync\",\"kind\":\"object\",\"type\":\"Sync\",\"relationName\":\"DataIdentifierToSync\"},{\"name\":\"entityId\",\"kind\":\"scalar\",\"type\":\"Int\"},{\"name\":\"entity\",\"kind\":\"object\",\"type\":\"Entity\",\"relationName\":\"DataIdentifierToEntity\"},{\"name\":\"idData\",\"kind\":\"scalar\",\"type\":\"Json\"},{\"name\":\"hash\",\"kind\":\"scalar\",\"type\":\"String\"}],\"dbName\":null},\"Association\":{\"fields\":[{\"name\":\"id\",\"kind\":\"scalar\",\"type\":\"Int\"},{\"name\":\"integrationId\",\"kind\":\"scalar\",\"type\":\"Int\"},{\"name\":\"integration\",\"kind\":\"object\",\"type\":\"Integration\",\"relationName\":\"AssociationToIntegration\"},{\"name\":\"name\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"type\",\"kind\":\"enum\",\"type\":\"AssociationType\"},{\"name\":\"primaryObject\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"objects\",\"kind\":\"object\",\"type\":\"AssociationObject\",\"relationName\":\"AssociationToAssociationObject\"}],\"dbName\":null},\"AssociationObject\":{\"fields\":[{\"name\":\"id\",\"kind\":\"scalar\",\"type\":\"Int\"},{\"name\":\"associationId\",\"kind\":\"scalar\",\"type\":\"Int\"},{\"name\":\"association\",\"kind\":\"object\",\"type\":\"Association\",\"relationName\":\"AssociationToAssociationObject\"},{\"name\":\"entityId\",\"kind\":\"scalar\",\"type\":\"Int\"},{\"name\":\"entity\",\"kind\":\"object\",\"type\":\"Entity\",\"relationName\":\"AssociationObjectToEntity\"},{\"name\":\"objectType\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"objId\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"metadata\",\"kind\":\"scalar\",\"type\":\"Json\"}],\"dbName\":null},\"Process\":{\"fields\":[{\"name\":\"id\",\"kind\":\"scalar\",\"type\":\"Int\"},{\"name\":\"userId\",\"kind\":\"scalar\",\"type\":\"Int\"},{\"name\":\"user\",\"kind\":\"object\",\"type\":\"User\",\"relationName\":\"ProcessToUser\"},{\"name\":\"integrationId\",\"kind\":\"scalar\",\"type\":\"Int\"},{\"name\":\"integration\",\"kind\":\"object\",\"type\":\"Integration\",\"relationName\":\"IntegrationToProcess\"},{\"name\":\"name\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"type\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"state\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"context\",\"kind\":\"scalar\",\"type\":\"Json\"},{\"name\":\"results\",\"kind\":\"scalar\",\"type\":\"Json\"},{\"name\":\"parentProcessId\",\"kind\":\"scalar\",\"type\":\"Int\"},{\"name\":\"parentProcess\",\"kind\":\"object\",\"type\":\"Process\",\"relationName\":\"ProcessHierarchy\"},{\"name\":\"childProcesses\",\"kind\":\"object\",\"type\":\"Process\",\"relationName\":\"ProcessHierarchy\"},{\"name\":\"createdAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"updatedAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"}],\"dbName\":null},\"State\":{\"fields\":[{\"name\":\"id\",\"kind\":\"scalar\",\"type\":\"Int\"},{\"name\":\"state\",\"kind\":\"scalar\",\"type\":\"Json\"}],\"dbName\":null},\"WebsocketConnection\":{\"fields\":[{\"name\":\"id\",\"kind\":\"scalar\",\"type\":\"Int\"},{\"name\":\"connectionId\",\"kind\":\"scalar\",\"type\":\"String\"}],\"dbName\":null},\"AdminScriptExecution\":{\"fields\":[{\"name\":\"id\",\"kind\":\"scalar\",\"type\":\"Int\"},{\"name\":\"name\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"type\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"state\",\"kind\":\"enum\",\"type\":\"AdminScriptExecutionState\"},{\"name\":\"context\",\"kind\":\"scalar\",\"type\":\"Json\"},{\"name\":\"results\",\"kind\":\"scalar\",\"type\":\"Json\"},{\"name\":\"parentExecutionId\",\"kind\":\"scalar\",\"type\":\"Int\"},{\"name\":\"parentExecution\",\"kind\":\"object\",\"type\":\"AdminScriptExecution\",\"relationName\":\"AdminScriptExecutionHierarchy\"},{\"name\":\"childExecutions\",\"kind\":\"object\",\"type\":\"AdminScriptExecution\",\"relationName\":\"AdminScriptExecutionHierarchy\"},{\"name\":\"createdAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"updatedAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"}],\"dbName\":null},\"ScriptSchedule\":{\"fields\":[{\"name\":\"id\",\"kind\":\"scalar\",\"type\":\"Int\"},{\"name\":\"scriptName\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"enabled\",\"kind\":\"scalar\",\"type\":\"Boolean\"},{\"name\":\"cronExpression\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"timezone\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"lastTriggeredAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"nextTriggerAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"externalScheduleId\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"externalScheduleName\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"createdAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"updatedAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"}],\"dbName\":null},\"UsageCounter\":{\"fields\":[{\"name\":\"id\",\"kind\":\"scalar\",\"type\":\"Int\"},{\"name\":\"integrationId\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"integrationType\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"metric\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"window\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"value\",\"kind\":\"scalar\",\"type\":\"BigInt\"},{\"name\":\"createdAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"updatedAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"}],\"dbName\":null}},\"enums\":{},\"types\":{}}")
|
|
390
390
|
defineDmmfProperty(exports.Prisma, config.runtimeDataModel)
|
|
391
391
|
config.engineWasm = {
|
|
392
392
|
getRuntime: async () => require('./query_engine_bg.js'),
|
|
@@ -25,7 +25,7 @@ class IntegrationEventDispatcher {
|
|
|
25
25
|
}
|
|
26
26
|
|
|
27
27
|
/**
|
|
28
|
-
* Resolve + invoke a handler, auto-instrumented
|
|
28
|
+
* Resolve + invoke a handler, auto-instrumented. This
|
|
29
29
|
* is the seam for queue/webhook/defined-route dispatch; the `this.on` path
|
|
30
30
|
* (user actions, lifecycle) is instrumented in IntegrationBase.send().
|
|
31
31
|
*/
|
package/index.js
CHANGED
|
@@ -79,7 +79,7 @@ class IntegrationBase {
|
|
|
79
79
|
// Tier 3 Integration Extensions — see packages/core/integrations/EXTENSIONS.md
|
|
80
80
|
// Shape: { [bindingName]: { extension, handlers?: { [eventName]: methodName } } }
|
|
81
81
|
extensions: {},
|
|
82
|
-
// Usage-counter opt-in
|
|
82
|
+
// Usage-counter opt-in. Declaring a canonical key opts into the
|
|
83
83
|
// cross-integration comparison report + durable rollup; custom keys are
|
|
84
84
|
// comparable within this integration type. Shape:
|
|
85
85
|
// usage: { canonical: ['records.synced', ...], custom: { 'deals.enriched': { unit, label } } }
|
|
@@ -108,7 +108,7 @@ class IntegrationBase {
|
|
|
108
108
|
this.messages = { errors: [], warnings: [] };
|
|
109
109
|
this._isHydrated = false;
|
|
110
110
|
|
|
111
|
-
// Telemetry
|
|
111
|
+
// Telemetry: every instance carries the service so integration
|
|
112
112
|
// code can call `this.telemetry.*`. Extracted before the record check so
|
|
113
113
|
// passing only `telemetry` never triggers a hollow hydration. Bound to
|
|
114
114
|
// this instance so emissions auto-carry `integration_type` for the usage
|
|
@@ -222,6 +222,25 @@ class IntegrationBase {
|
|
|
222
222
|
};
|
|
223
223
|
|
|
224
224
|
this._isHydrated = Boolean(this.id);
|
|
225
|
+
|
|
226
|
+
// Log the instance-open exactly once per hydrated
|
|
227
|
+
// instance, carrying the standard identifier set — so an integration is
|
|
228
|
+
// visible in telemetry even on a path that never dispatches a handler.
|
|
229
|
+
// High-cardinality ids ride the bus context (3rd arg), never metric
|
|
230
|
+
// labels; only the bounded integration_type is an attribute.
|
|
231
|
+
if (this._isHydrated && !this._instantiationLogged) {
|
|
232
|
+
this._instantiationLogged = true;
|
|
233
|
+
try {
|
|
234
|
+
this.telemetry.event(
|
|
235
|
+
'frigg.integration.instantiated',
|
|
236
|
+
{ integration_type: this.constructor?.Definition?.name },
|
|
237
|
+
this.getTelemetryContext()
|
|
238
|
+
);
|
|
239
|
+
} catch (_) {
|
|
240
|
+
// Telemetry must never break integration hydration.
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
225
244
|
return this;
|
|
226
245
|
}
|
|
227
246
|
|
|
@@ -230,7 +249,7 @@ class IntegrationBase {
|
|
|
230
249
|
}
|
|
231
250
|
|
|
232
251
|
/**
|
|
233
|
-
* Standard telemetry identifier set
|
|
252
|
+
* Standard telemetry identifier set. Assembled from the
|
|
234
253
|
* hydrated record (integrationId, userId, version), the static Definition
|
|
235
254
|
* (integrationType, version fallback), and the environment (stage, appName).
|
|
236
255
|
* High-cardinality ids (integrationId, userId) ride span baggage only — never
|
|
@@ -793,7 +812,7 @@ class IntegrationBase {
|
|
|
793
812
|
`Event ${event} is not defined in the Integration event object`
|
|
794
813
|
);
|
|
795
814
|
}
|
|
796
|
-
// Auto-instrument
|
|
815
|
+
// Auto-instrument. This is the seam for user
|
|
797
816
|
// actions, config-options, and lifecycle events dispatched via `this.on`
|
|
798
817
|
// (the queue/webhook/route paths go through IntegrationEventDispatcher).
|
|
799
818
|
return instrumentHandler(
|
|
@@ -44,8 +44,18 @@ class Requester extends Delegate {
|
|
|
44
44
|
// Instance methods can use this.fetch without differentiating
|
|
45
45
|
this.fetch = get(params, 'fetch', fetch);
|
|
46
46
|
|
|
47
|
-
// Telemetry
|
|
47
|
+
// Telemetry. Defaults to the process singleton; overridable
|
|
48
48
|
// for tests. Outbound requests are instrumented in `_request`.
|
|
49
|
+
//
|
|
50
|
+
// Attribution boundary: the `frigg.apimodule.requests` OTel metric fires
|
|
51
|
+
// for EVERY request (observability is complete). The durable per-
|
|
52
|
+
// integration `api.requests` usage counter, however, only rolls up
|
|
53
|
+
// requests carrying an integration context — set by the handler seams via
|
|
54
|
+
// AsyncLocalStorage (dispatched USER_ACTION/CRON/QUEUE/WEBHOOK paths). A
|
|
55
|
+
// request made before an integration exists (OAuth/token exchange, entity
|
|
56
|
+
// discovery during connection setup) has no integration to attribute to,
|
|
57
|
+
// so it is observable but not rolled up. Pass a context-bound `telemetry`
|
|
58
|
+
// (an integration's `this.telemetry`) to attribute out-of-band requests.
|
|
49
59
|
this.telemetry = (params && params.telemetry) || getTelemetry();
|
|
50
60
|
}
|
|
51
61
|
|
|
@@ -93,7 +103,7 @@ class Requester extends Delegate {
|
|
|
93
103
|
};
|
|
94
104
|
|
|
95
105
|
/**
|
|
96
|
-
* Instrumenting entry point
|
|
106
|
+
* Instrumenting entry point. Wraps the whole logical request —
|
|
97
107
|
* including retry/refresh recursion — in a single span + one
|
|
98
108
|
* `frigg.apimodule.requests` counter, emitted on the `i === 0` boundary so
|
|
99
109
|
* retries are never double-counted. The full URL rides the span only; the
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@friggframework/core",
|
|
3
3
|
"prettier": "@friggframework/prettier-config",
|
|
4
|
-
"version": "2.0.0--canary.622.
|
|
4
|
+
"version": "2.0.0--canary.622.faffe5f.0",
|
|
5
5
|
"dependencies": {
|
|
6
6
|
"@aws-sdk/client-apigatewaymanagementapi": "^3.588.0",
|
|
7
7
|
"@aws-sdk/client-kms": "^3.588.0",
|
|
@@ -45,9 +45,9 @@
|
|
|
45
45
|
}
|
|
46
46
|
},
|
|
47
47
|
"devDependencies": {
|
|
48
|
-
"@friggframework/eslint-config": "2.0.0--canary.622.
|
|
49
|
-
"@friggframework/prettier-config": "2.0.0--canary.622.
|
|
50
|
-
"@friggframework/test": "2.0.0--canary.622.
|
|
48
|
+
"@friggframework/eslint-config": "2.0.0--canary.622.faffe5f.0",
|
|
49
|
+
"@friggframework/prettier-config": "2.0.0--canary.622.faffe5f.0",
|
|
50
|
+
"@friggframework/test": "2.0.0--canary.622.faffe5f.0",
|
|
51
51
|
"@prisma/client": "^6.19.3",
|
|
52
52
|
"@types/lodash": "4.17.15",
|
|
53
53
|
"@typescript-eslint/eslint-plugin": "^8.0.0",
|
|
@@ -87,5 +87,5 @@
|
|
|
87
87
|
"publishConfig": {
|
|
88
88
|
"access": "public"
|
|
89
89
|
},
|
|
90
|
-
"gitHead": "
|
|
90
|
+
"gitHead": "faffe5ff8b3fea6bbb46a52502cbd5b5ecb35559"
|
|
91
91
|
}
|
|
@@ -442,13 +442,15 @@ model UsageCounter {
|
|
|
442
442
|
integrationType String
|
|
443
443
|
metric String
|
|
444
444
|
window String // e.g. "day:2026-07-05" or "hour:2026-07-05T14"
|
|
445
|
-
value
|
|
445
|
+
value BigInt @default(0) // BigInt: a busy day/hour counter can exceed 32-bit
|
|
446
446
|
|
|
447
447
|
createdAt DateTime @default(now())
|
|
448
448
|
updatedAt DateTime @updatedAt
|
|
449
449
|
|
|
450
450
|
@@unique([integrationId, integrationType, metric, window])
|
|
451
|
-
|
|
452
|
-
@@index([
|
|
451
|
+
// Covers totals() (metric + window prefix/range, no integrationType filter).
|
|
452
|
+
@@index([metric, window])
|
|
453
|
+
// Covers series() (metric + integrationType + window range).
|
|
454
|
+
@@index([metric, integrationType, window])
|
|
453
455
|
@@map("UsageCounter")
|
|
454
456
|
}
|
|
@@ -9,7 +9,7 @@ CREATE TABLE "UsageCounter" (
|
|
|
9
9
|
"integrationType" TEXT NOT NULL,
|
|
10
10
|
"metric" TEXT NOT NULL,
|
|
11
11
|
"window" TEXT NOT NULL,
|
|
12
|
-
"value"
|
|
12
|
+
"value" BIGINT NOT NULL DEFAULT 0,
|
|
13
13
|
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
14
14
|
"updatedAt" TIMESTAMP(3) NOT NULL,
|
|
15
15
|
|
|
@@ -20,7 +20,7 @@ CREATE TABLE "UsageCounter" (
|
|
|
20
20
|
CREATE UNIQUE INDEX "UsageCounter_integrationId_integrationType_metric_window_key" ON "UsageCounter"("integrationId", "integrationType", "metric", "window");
|
|
21
21
|
|
|
22
22
|
-- CreateIndex
|
|
23
|
-
CREATE INDEX "
|
|
23
|
+
CREATE INDEX "UsageCounter_metric_window_idx" ON "UsageCounter"("metric", "window");
|
|
24
24
|
|
|
25
25
|
-- CreateIndex
|
|
26
|
-
CREATE INDEX "
|
|
26
|
+
CREATE INDEX "UsageCounter_metric_integrationType_window_idx" ON "UsageCounter"("metric", "integrationType", "window");
|
|
@@ -424,12 +424,14 @@ model UsageCounter {
|
|
|
424
424
|
integrationType String
|
|
425
425
|
metric String
|
|
426
426
|
window String // e.g. "day:2026-07-05" or "hour:2026-07-05T14"
|
|
427
|
-
value
|
|
427
|
+
value BigInt @default(0) // BigInt: a busy day/hour counter can exceed 32-bit
|
|
428
428
|
|
|
429
429
|
createdAt DateTime @default(now())
|
|
430
430
|
updatedAt DateTime @updatedAt
|
|
431
431
|
|
|
432
432
|
@@unique([integrationId, integrationType, metric, window])
|
|
433
|
-
|
|
434
|
-
@@index([
|
|
433
|
+
// Covers totals() (metric + window prefix/range, no integrationType filter).
|
|
434
|
+
@@index([metric, window])
|
|
435
|
+
// Covers series() (metric + integrationType + window range).
|
|
436
|
+
@@index([metric, integrationType, window])
|
|
435
437
|
}
|
|
@@ -40,7 +40,7 @@ function buildTypeLabels() {
|
|
|
40
40
|
|
|
41
41
|
function createReportingRouter() {
|
|
42
42
|
const reportingRepository = createReportingRepository();
|
|
43
|
-
//
|
|
43
|
+
// Enrich the report with feature-usage columns read from
|
|
44
44
|
// the Frigg-owned usage store. Best-effort — a failure to construct it never
|
|
45
45
|
// blocks the structural report.
|
|
46
46
|
let usageRepository = null;
|
|
@@ -26,7 +26,7 @@ class ListIntegrationsReport {
|
|
|
26
26
|
throw new Error('reportingRepository is required');
|
|
27
27
|
}
|
|
28
28
|
this.reportingRepository = reportingRepository;
|
|
29
|
-
// Optional
|
|
29
|
+
// Optional usage store (the ADR-010 hand-off). When present, the
|
|
30
30
|
// report's byType buckets are enriched with feature-usage columns read
|
|
31
31
|
// ONLY from this store — never an external APM.
|
|
32
32
|
this.usageRepository = usageRepository || null;
|
|
@@ -87,7 +87,7 @@ class ListIntegrationsReport {
|
|
|
87
87
|
bucket.byStatus[statusKey] = (bucket.byStatus[statusKey] ?? 0) + 1;
|
|
88
88
|
}
|
|
89
89
|
|
|
90
|
-
// Additive usage columns
|
|
90
|
+
// Additive usage columns. Read per-type totals for each
|
|
91
91
|
// canonical counter; a type with no rows reads as 0. Guarded so a usage
|
|
92
92
|
// store failure never breaks the structural report.
|
|
93
93
|
if (this.usageRepository) {
|
package/telemetry/README.md
CHANGED
|
@@ -83,6 +83,13 @@ context only.
|
|
|
83
83
|
Request URLs are redacted (query string + userinfo stripped) before they touch a
|
|
84
84
|
span, so credentials in query params never leak.
|
|
85
85
|
|
|
86
|
+
> **Usage-attribution boundary.** The OTel metrics above fire for *every* seam
|
|
87
|
+
> invocation. The durable per-integration **usage** rollup, though, only counts
|
|
88
|
+
> emissions that carry an integration context — set by the handler seams. Requests
|
|
89
|
+
> an API module makes *before an integration exists* (OAuth/token exchange, entity
|
|
90
|
+
> discovery during connection setup) are observable in traces but not attributed to
|
|
91
|
+
> an `api.requests` usage counter (there is no integration to attribute them to).
|
|
92
|
+
|
|
86
93
|
## Custom metrics (integration code)
|
|
87
94
|
|
|
88
95
|
Every integration instance carries `this.telemetry` (auto-tagged with its
|
|
@@ -213,6 +220,11 @@ await frigg.usage.northStar({ integrationType: 'hubspot', since: daysAgo(30) });
|
|
|
213
220
|
Or read it like any counter once you know the key:
|
|
214
221
|
`frigg.usage.totals({ metric: 'contacts_synced' })`; trends via `frigg.usage.series({ metric })`.
|
|
215
222
|
|
|
223
|
+
> `northStar` config is injected at the composition root — the caller that owns
|
|
224
|
+
> the app definition passes it in: `createFriggCommands({ integrationClass, northStar })`
|
|
225
|
+
> (the application layer never reaches up to load it). Omitted → `northStar()`
|
|
226
|
+
> returns `null`.
|
|
227
|
+
|
|
216
228
|
## Plugin / extension tap
|
|
217
229
|
|
|
218
230
|
Telemetry flows onto an internal event stream (independent of OTel export, so
|
|
@@ -280,19 +292,23 @@ frigg.usage.totals / series ◄── UsageCounter store ──► reporting usa
|
|
|
280
292
|
- **Flush is Lambda-safe:** `create-handler` awaits a bounded `forceFlush` in a
|
|
281
293
|
`finally` (background timers can't fire once the container freezes).
|
|
282
294
|
- **Usage accuracy is approximate:** at-least-once delivery means a retried
|
|
283
|
-
handler could double-count
|
|
284
|
-
> 1`)
|
|
295
|
+
handler could double-count. The invocation buffer is discarded only when the
|
|
296
|
+
**whole** SQS batch is a redelivery (`ApproximateReceiveCount > 1`); a mixed
|
|
297
|
+
batch flushes so a redelivered sibling never drops a fresh record's counts.
|
|
285
298
|
|
|
286
299
|
## Caveats / current limitations
|
|
287
300
|
|
|
288
301
|
- **Usage persistence requires a DB-connected handler.** DB-free handlers (e.g.
|
|
289
302
|
the raw webhook-receipt route) can't write; `webhooks.received` is counted at
|
|
290
303
|
the DB-connected `ON_WEBHOOK` queue dispatch instead.
|
|
291
|
-
- **DocumentDB**
|
|
292
|
-
not yet
|
|
304
|
+
- **DocumentDB** uses a raw-command adapter (`$runCommandRaw`) for increment and
|
|
305
|
+
aggregate; command shapes are unit-tested but not yet run against a real cluster.
|
|
293
306
|
- **Retention:** the `UsageCounter` table has no pruning yet — hour-grain rows
|
|
294
|
-
accumulate. Add a scheduled prune for high-volume deployments.
|
|
295
|
-
|
|
307
|
+
accumulate. Add a scheduled prune for high-volume deployments. (Read paths are
|
|
308
|
+
covered by composite indexes `(metric, window)` and `(metric, integrationType,
|
|
309
|
+
window)`.)
|
|
310
|
+
- Metric `value` is a `BigInt` per `(integrationId, integrationType, metric,
|
|
311
|
+
window)` row; reads coerce the sum to a JS Number (safe below 2^53).
|
|
296
312
|
|
|
297
313
|
## See also
|
|
298
314
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Wrap a telemetry service so an integration instance's emissions automatically
|
|
3
|
-
* carry its `integration_type
|
|
3
|
+
* carry its `integration_type`. This lets developers write
|
|
4
4
|
* `this.telemetry.count('records.synced', n, { entity })` with no per-call
|
|
5
5
|
* boilerplate, while the usage rollup still attributes the counter to the right
|
|
6
6
|
* integration type.
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Canonical usage-counter vocabulary
|
|
2
|
+
* Canonical usage-counter vocabulary.
|
|
3
3
|
*
|
|
4
4
|
* These core-owned, versioned keys are the ONLY metrics guaranteed comparable
|
|
5
5
|
* *across* integration types — they power ADR-010's apples-to-apples comparison
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Builds OpenTelemetry span/metric exporters from an app-definition descriptor
|
|
3
|
-
*
|
|
2
|
+
* Builds OpenTelemetry span/metric exporters from an app-definition descriptor.
|
|
3
|
+
* All OTel packages are lazy-`require`d here so the no-op
|
|
4
4
|
* path (which never calls this) loads zero OTel modules.
|
|
5
5
|
*
|
|
6
6
|
* Supported descriptor `type`s:
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Wraps a single integration handler invocation with a span + the
|
|
3
|
-
* `frigg.handler.invocations` counter
|
|
3
|
+
* `frigg.handler.invocations` counter. Shared by the two
|
|
4
4
|
* dispatch seams — `IntegrationBase.send()` and `IntegrationEventDispatcher` —
|
|
5
5
|
* so both paths are instrumented identically.
|
|
6
6
|
*
|
|
@@ -8,7 +8,7 @@ const {
|
|
|
8
8
|
* No-op telemetry implementation.
|
|
9
9
|
*
|
|
10
10
|
* This is the default when no OTel exporter is configured, so telemetry "rides
|
|
11
|
-
* for free"
|
|
11
|
+
* for free": integration code can call `this.telemetry.*`
|
|
12
12
|
* unconditionally and it costs nothing on the OTel side. Critically, this module
|
|
13
13
|
* imports **zero** OpenTelemetry packages — the no-op path never loads the OTel
|
|
14
14
|
* SDK (guarded by a require-graph test), protecting Lambda cold-start.
|
package/telemetry/north-star.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Adopter North Star metric
|
|
2
|
+
* Adopter North Star metric. A North Star simply references
|
|
3
3
|
* a usage-counter key (canonical or custom); its value is then read like any
|
|
4
4
|
* other counter via `frigg.usage.totals({ metric: northStarKey })`. It is
|
|
5
5
|
* populated either by (a) direct emission of that counter, or (b) derived from a
|
|
@@ -47,7 +47,7 @@ function ensureContextManager() {
|
|
|
47
47
|
}
|
|
48
48
|
|
|
49
49
|
/**
|
|
50
|
-
* The OpenTelemetry-backed telemetry service
|
|
50
|
+
* The OpenTelemetry-backed telemetry service. Reached only
|
|
51
51
|
* when a real exporter is configured; `createTelemetry` returns the no-op
|
|
52
52
|
* otherwise. `BatchSpanProcessor` is used uniformly — spans are delivered by the
|
|
53
53
|
* bounded `forceFlush()` the Lambda handler awaits before the container freezes
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
const { wireTelemetrySubscribers } = require('./plugin-subscribers');
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
* Process-wide wiring of adopter-declared telemetry subscribers
|
|
5
|
-
*
|
|
4
|
+
* Process-wide wiring of adopter-declared telemetry subscribers. Built once per
|
|
5
|
+
* cold start and reused for the life of the Lambda
|
|
6
6
|
* container, mirroring the usage-rollup singleton. Independent of the usage
|
|
7
7
|
* rollup: adopter taps wire even when no integration declares `Definition.usage`.
|
|
8
8
|
* Never throws — a bad/absent app definition just means no adopter taps.
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Declarative plugin/extension telemetry taps
|
|
2
|
+
* Declarative plugin/extension telemetry taps.
|
|
3
3
|
*
|
|
4
4
|
* The built-in usage rollup and North Star derivation subscribe to the telemetry
|
|
5
5
|
* bus in framework code; this lets an ADOPTER subscribe declaratively from the
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Resolves and validates the telemetry section of an app definition
|
|
3
|
-
*
|
|
3
|
+
* into a normalized config the TelemetryService and
|
|
4
4
|
* usage rollup consume: `{ exporter, northStar, sampleRatio }`.
|
|
5
5
|
*
|
|
6
6
|
* Exporter default is keyed off STAGE: only a genuinely LOCAL run (STAGE=local)
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
const { AsyncLocalStorage } = require('node:async_hooks');
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
* Ambient telemetry context
|
|
4
|
+
* Ambient telemetry context. A single process-wide AsyncLocalStorage
|
|
5
5
|
* holds the standard identifiers ({integrationId, integrationType, userId,
|
|
6
6
|
* version, ...}) for the duration of a handler, set once at the handler seam via
|
|
7
7
|
* `telemetry.withContext(...)`. Any emission during that async scope — including
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* TelemetryEventBus — the internal event stream telemetry flows onto
|
|
3
|
-
*
|
|
2
|
+
* TelemetryEventBus — the internal event stream telemetry flows onto.
|
|
3
|
+
* Plugins/extensions and the built-in usage rollup
|
|
4
4
|
* subscribe here; it is deliberately independent of OTel export (Decision 7),
|
|
5
5
|
* so usage counters persist even when the OTel exporter is a no-op.
|
|
6
6
|
*
|
|
@@ -12,7 +12,7 @@ function isNoOpExporter(exporter) {
|
|
|
12
12
|
}
|
|
13
13
|
|
|
14
14
|
/**
|
|
15
|
-
* Create the vendor-neutral telemetry service
|
|
15
|
+
* Create the vendor-neutral telemetry service.
|
|
16
16
|
*
|
|
17
17
|
* Integration code uses the returned object (`this.telemetry`) and never imports
|
|
18
18
|
* a backend SDK. When no exporter is configured the service is a pure no-op that
|
|
@@ -26,8 +26,8 @@ function isNoOpExporter(exporter) {
|
|
|
26
26
|
* @param {object} [options.bus] Event bus to reuse (defaults to a fresh one).
|
|
27
27
|
*/
|
|
28
28
|
function createTelemetry(options = {}) {
|
|
29
|
-
// The internal event stream is always on and independent of OTel export
|
|
30
|
-
//
|
|
29
|
+
// The internal event stream is always on and independent of OTel export:
|
|
30
|
+
// the usage rollup + plugin taps must work even when
|
|
31
31
|
// the OTel exporter is a no-op. The bus is pure JS, so this does not load
|
|
32
32
|
// any OTel module on the no-op path.
|
|
33
33
|
const bus = options.bus || createTelemetryEventBus();
|
|
@@ -2,7 +2,7 @@ const { createTelemetry } = require('./telemetry-service');
|
|
|
2
2
|
const { createNoOpTelemetry } = require('./no-op-telemetry');
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
|
-
* Process-wide telemetry singleton
|
|
5
|
+
* Process-wide telemetry singleton. Created lazily on first use and
|
|
6
6
|
* reused for the life of the Lambda container, so the OTel SDK initialises at
|
|
7
7
|
* most once per cold start. Building it never throws — any failure (e.g. no app
|
|
8
8
|
* definition on disk in a unit test) falls back to the no-op so a handler is
|