@friggframework/core 2.0.0--canary.663.8de669c.0 → 2.0.0--canary.686.821f8a3.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.
Files changed (188) hide show
  1. package/CHANGELOG.md +11 -0
  2. package/CLAUDE.md +28 -0
  3. package/README.md +105 -0
  4. package/application/commands/integration-commands.js +20 -10
  5. package/application/commands/scheduler-commands.js +11 -7
  6. package/application/commands/user-commands.js +17 -5
  7. package/application/index.js +6 -2
  8. package/core/Worker.js +31 -7
  9. package/core/create-handler.js +9 -0
  10. package/credential/repositories/credential-repository-documentdb.js +34 -0
  11. package/credential/repositories/credential-repository-interface.js +15 -0
  12. package/credential/repositories/credential-repository-mongo.js +23 -0
  13. package/credential/repositories/credential-repository-postgres.js +28 -0
  14. package/credential/use-cases/credential-operations-for-user.js +202 -0
  15. package/credential/use-cases/reauthorize-credential-for-user.js +73 -0
  16. package/database/config.js +48 -24
  17. package/database/documentdb-utils.js +6 -1
  18. package/database/encryption/README.md +2 -0
  19. package/database/encryption/encryption-config.js +10 -31
  20. package/database/encryption/encryption-schema-registry.js +121 -24
  21. package/database/encryption/field-encryption-service.js +69 -5
  22. package/database/encryption/test-support/fake-ciphertext.js +15 -0
  23. package/database/organization-where.js +18 -0
  24. package/database/prisma.js +4 -2
  25. package/database/unique-violation.js +25 -0
  26. package/extensions/bootstrap-runner.js +43 -0
  27. package/extensions/extension-loader.js +187 -0
  28. package/extensions/index.js +41 -0
  29. package/extensions/initialize-app.js +123 -0
  30. package/extensions/route-mounter.js +103 -0
  31. package/extensions/schema-composer.js +188 -0
  32. package/generated/prisma-mongodb/edge.js +43 -4
  33. package/generated/prisma-mongodb/index-browser.js +40 -1
  34. package/generated/prisma-mongodb/index.d.ts +4761 -133
  35. package/generated/prisma-mongodb/index.js +43 -4
  36. package/generated/prisma-mongodb/package.json +1 -1
  37. package/generated/prisma-mongodb/schema.prisma +92 -0
  38. package/generated/prisma-mongodb/wasm.js +43 -4
  39. package/generated/prisma-postgresql/edge.js +43 -4
  40. package/generated/prisma-postgresql/index-browser.js +40 -1
  41. package/generated/prisma-postgresql/index.d.ts +4961 -28
  42. package/generated/prisma-postgresql/index.js +43 -4
  43. package/generated/prisma-postgresql/package.json +1 -1
  44. package/generated/prisma-postgresql/schema.prisma +87 -0
  45. package/generated/prisma-postgresql/wasm.js +43 -4
  46. package/handlers/app-definition-loader.js +121 -19
  47. package/handlers/app-handler-helpers.js +25 -3
  48. package/handlers/backend-utils.js +31 -25
  49. package/handlers/middleware/admin-auth.js +14 -1
  50. package/handlers/routers/auth.js +46 -10
  51. package/handlers/routers/health.js +13 -4
  52. package/handlers/routers/integration-defined-routers.js +144 -92
  53. package/handlers/routers/integration-webhook-routers.js +102 -64
  54. package/handlers/workers/integration-defined-workers.js +5 -2
  55. package/index.js +65 -1
  56. package/infrastructure/scheduler/index.js +19 -6
  57. package/infrastructure/scheduler/netlify-scheduler-adapter.js +216 -0
  58. package/infrastructure/scheduler/scheduled-job-repository.js +118 -0
  59. package/infrastructure/scheduler/scheduler-service-factory.js +55 -7
  60. package/integrations/index.js +1 -0
  61. package/integrations/integration-router.js +131 -96
  62. package/integrations/repositories/integration-not-found.js +14 -0
  63. package/integrations/repositories/integration-repository-documentdb.js +35 -3
  64. package/integrations/repositories/integration-repository-interface.js +17 -1
  65. package/integrations/repositories/integration-repository-mongo.js +59 -76
  66. package/integrations/repositories/integration-repository-postgres.js +62 -76
  67. package/integrations/schedule-config.js +80 -0
  68. package/integrations/tests/doubles/test-integration-repository.js +12 -1
  69. package/integrations/use-cases/create-integration.js +55 -20
  70. package/integrations/use-cases/dispatch-ongoing-syncs.js +100 -0
  71. package/integrations/use-cases/get-integrations-for-user.js +80 -40
  72. package/integrations/use-cases/index.js +7 -1
  73. package/integrations/use-cases/integration-operations-for-user.js +256 -0
  74. package/integrations/use-cases/resolve-integration-ownership.js +177 -0
  75. package/integrations/utils/map-integration-dto.js +43 -14
  76. package/management-api/create-management-api-router.js +49 -0
  77. package/management-api/error-response.js +116 -0
  78. package/management-api/management-api-config.js +151 -0
  79. package/management-api/meta-router.js +76 -0
  80. package/management-api/openapi/build-openapi.js +144 -0
  81. package/management-api/openapi/components.js +408 -0
  82. package/management-api/openapi/v1-operations.js +83 -0
  83. package/management-api/openapi/v2-operations.js +264 -0
  84. package/management-api/route-registry.js +411 -0
  85. package/management-api/test-support/http-client.js +39 -0
  86. package/management-api/use-cases/get-management-api-meta.js +57 -0
  87. package/management-api/v1-deprecation.js +120 -0
  88. package/management-api/v2/create-v2-router.js +85 -0
  89. package/management-api/v2/dependencies.js +281 -0
  90. package/management-api/v2/dto.js +143 -0
  91. package/management-api/v2/handlers/authorization.js +94 -0
  92. package/management-api/v2/handlers/credentials.js +33 -0
  93. package/management-api/v2/handlers/entities.js +77 -0
  94. package/management-api/v2/handlers/integrations.js +128 -0
  95. package/management-api/v2/handlers/proxy.js +34 -0
  96. package/management-api/v2/request.js +85 -0
  97. package/management-api/v2/test-support/fake-dependencies.js +103 -0
  98. package/modules/domain/authorization-session.js +144 -0
  99. package/modules/entity-scope.js +15 -0
  100. package/modules/entity-type-registry.js +311 -0
  101. package/modules/index.js +14 -0
  102. package/modules/module-factory.js +12 -0
  103. package/modules/module.js +53 -2
  104. package/modules/proxy/proxy-errors.js +20 -0
  105. package/modules/proxy/proxy-path.js +179 -0
  106. package/modules/proxy/proxy-policy.js +248 -0
  107. package/modules/repositories/authorization-session-repository-documentdb.js +112 -0
  108. package/modules/repositories/authorization-session-repository-factory.js +31 -0
  109. package/modules/repositories/authorization-session-repository-interface.js +47 -0
  110. package/modules/repositories/authorization-session-repository-prisma.js +87 -0
  111. package/modules/repositories/entity-not-found.js +18 -0
  112. package/modules/repositories/module-repository-documentdb.js +51 -6
  113. package/modules/repositories/module-repository-interface.js +19 -2
  114. package/modules/repositories/module-repository-mongo.js +67 -74
  115. package/modules/repositories/module-repository-postgres.js +66 -74
  116. package/modules/repositories/module-repository.js +2 -1
  117. package/modules/requester/proxy-request.js +244 -0
  118. package/modules/requester/requester.js +27 -2
  119. package/modules/scope-config.js +192 -0
  120. package/modules/scope-policy.js +112 -0
  121. package/modules/scope-resolution.js +288 -0
  122. package/modules/scope-visibility.js +247 -0
  123. package/modules/tests/doubles/in-memory-authorization-session-repository.js +45 -0
  124. package/modules/tests/doubles/ownership-matching.js +18 -0
  125. package/modules/use-cases/authorize-entity-for-user.js +61 -0
  126. package/modules/use-cases/create-entity-from-credential.js +113 -0
  127. package/modules/use-cases/entity-operations-for-user.js +162 -0
  128. package/modules/use-cases/entity-types.js +139 -0
  129. package/modules/use-cases/execute-entity-proxy-request.js +286 -0
  130. package/modules/use-cases/get-authorization-requirements.js +100 -0
  131. package/modules/use-cases/get-entities-for-user.js +26 -13
  132. package/modules/use-cases/get-entity-module-for-user.js +113 -0
  133. package/modules/use-cases/get-entity-options-by-id.js +10 -4
  134. package/modules/use-cases/get-entity-options-by-type.js +1 -1
  135. package/modules/use-cases/get-module-instance-from-type.js +1 -1
  136. package/modules/use-cases/get-module.js +10 -4
  137. package/modules/use-cases/manage-access.js +21 -0
  138. package/modules/use-cases/multi-step-authorization.js +329 -0
  139. package/modules/use-cases/process-authorization-callback.js +53 -19
  140. package/modules/use-cases/refresh-entity-options.js +10 -4
  141. package/modules/use-cases/test-module-auth.js +10 -4
  142. package/package.json +6 -5
  143. package/prisma-mongodb/schema.prisma +92 -0
  144. package/prisma-postgresql/migrations/20261003000000_add_authorization_session/migration.sql +28 -0
  145. package/prisma-postgresql/migrations/20261015000000_add_organization_membership/migration.sql +26 -0
  146. package/prisma-postgresql/migrations/20261016000000_add_entity_integration_organization/migration.sql +25 -0
  147. package/prisma-postgresql/migrations/20261017000000_add_scheduled_job/migration.sql +28 -0
  148. package/prisma-postgresql/schema.prisma +87 -0
  149. package/providers/index.js +23 -0
  150. package/providers/resolve-provider.js +93 -0
  151. package/providers/runtime-detection.js +124 -0
  152. package/queues/index.js +25 -0
  153. package/queues/providers/index.js +11 -0
  154. package/queues/providers/netlify-background-provider.js +150 -0
  155. package/queues/providers/qstash-queue-provider.js +219 -0
  156. package/queues/providers/sqs-queue-provider.js +70 -0
  157. package/queues/queue-client-interface.js +55 -0
  158. package/queues/queue-message-auth.js +193 -0
  159. package/queues/queue-provider-factory.js +101 -0
  160. package/queues/queue-provider.js +65 -0
  161. package/queues/sqs-queue-client.js +81 -0
  162. package/types/index.d.ts +2 -0
  163. package/types/modules/index.d.ts +197 -0
  164. package/types/user/index.d.ts +134 -0
  165. package/user/adopter-jwt-config.js +177 -0
  166. package/user/adopter-jwt-errors.js +59 -0
  167. package/user/adopter-token-verifier.js +122 -0
  168. package/user/app-user-helpers.js +97 -0
  169. package/user/create-user-authentication.js +64 -0
  170. package/user/membership.js +51 -0
  171. package/user/repositories/organization-membership-record.js +19 -0
  172. package/user/repositories/organization-membership-repository-documentdb.js +106 -0
  173. package/user/repositories/organization-membership-repository-factory.js +42 -0
  174. package/user/repositories/organization-membership-repository-interface.js +57 -0
  175. package/user/repositories/organization-membership-repository-mongo.js +13 -0
  176. package/user/repositories/organization-membership-repository-postgres.js +29 -0
  177. package/user/repositories/organization-membership-repository-prisma.js +81 -0
  178. package/user/repositories/user-repository-documentdb.js +27 -2
  179. package/user/repositories/user-repository-interface.js +18 -0
  180. package/user/repositories/user-repository-mongo.js +24 -0
  181. package/user/repositories/user-repository-postgres.js +18 -0
  182. package/user/tests/doubles/adopter-jwt-fixtures.js +220 -0
  183. package/user/use-cases/authenticate-user.js +7 -1
  184. package/user/use-cases/ensure-organization-membership.js +92 -0
  185. package/user/use-cases/find-individual-users-by-organization.js +54 -0
  186. package/user/use-cases/get-user-from-adopter-jwt.js +95 -124
  187. package/user/use-cases/get-user-from-x-frigg-headers.js +83 -80
  188. package/user/user.js +64 -1
@@ -56,6 +56,14 @@ model User {
56
56
  integrations Integration[]
57
57
  processes Process[]
58
58
 
59
+ // Organization memberships (ADR-056 §3.6). `organizationId` above is the legacy
60
+ // single-org pointer: set on a person's first link, never moved afterwards, and
61
+ // read as an implicit membership when no row exists for it.
62
+ // As an individual: the organizations this person belongs to.
63
+ memberships OrganizationMembership[] @relation("IndividualMemberships")
64
+ // As an organization: the people who belong to it.
65
+ orgMembers OrganizationMembership[] @relation("OrganizationMemberships")
66
+
59
67
  @@unique([username, appUserId])
60
68
  @@index([type])
61
69
  @@index([appUserId])
@@ -123,6 +131,14 @@ model Entity {
123
131
  moduleName String?
124
132
  externalId String?
125
133
 
134
+ // Organization this entity was connected in (ADR-056 §3.6): the active
135
+ // organization for `individual`, the owner for `organization`, null for
136
+ // `global`. Null on an `individual` entity is also what every row written
137
+ // before this column existed has, and what `individualAcross: 'all'` writes.
138
+ // Deliberately not a relation: a deleted organization must leave its rows
139
+ // invisible, not turn them into "unscoped" ones (SetNull would do that).
140
+ organizationId Int?
141
+
126
142
  data Json @default("{}")
127
143
 
128
144
  createdAt DateTime @default(now())
@@ -136,6 +152,7 @@ model Entity {
136
152
  associationObjects AssociationObject[]
137
153
 
138
154
  @@index([userId])
155
+ @@index([userId, organizationId])
139
156
  @@index([externalId])
140
157
  @@index([moduleName])
141
158
  @@index([credentialId])
@@ -152,6 +169,11 @@ model Integration {
152
169
  user User? @relation(fields: [userId], references: [id], onDelete: Cascade)
153
170
  status IntegrationStatus @default(IN_CREATION)
154
171
 
172
+ // Organization this integration was created in (ADR-056 §3.6): the active
173
+ // organization at create time. Null for rows that predate the column. Not a
174
+ // relation, for the same reason as Entity.organizationId.
175
+ organizationId Int?
176
+
155
177
  // Configuration and version
156
178
  config Json? // Integration configuration object
157
179
  version String?
@@ -175,6 +197,7 @@ model Integration {
175
197
  processes Process[]
176
198
 
177
199
  @@index([userId])
200
+ @@index([userId, organizationId])
178
201
  @@index([status])
179
202
  }
180
203
 
@@ -434,3 +457,67 @@ model UsageCounter {
434
457
  // Covers series() (metric + integrationType + window range).
435
458
  @@index([metric, integrationType, window])
436
459
  }
460
+
461
+ // ============================================================================
462
+ // MANAGEMENT API MODELS
463
+ // ============================================================================
464
+
465
+ /// Multi-step authorization state for Management API v2 (/api/v2/authorize,
466
+ /// /api/v2/credentials/:id/reauthorize). Short-lived: expired rows are removed
467
+ /// opportunistically. stepData is encrypted at rest.
468
+ model AuthorizationSession {
469
+ id Int @id @default(autoincrement())
470
+ sessionId String @unique
471
+ userId String
472
+ entityType String
473
+ credentialId String?
474
+ currentStep Int @default(1)
475
+ maxSteps Int
476
+ stepData Json @default("{}")
477
+ expiresAt DateTime
478
+ completed Boolean @default(false)
479
+
480
+ createdAt DateTime @default(now())
481
+ updatedAt DateTime @updatedAt
482
+
483
+ @@index([userId, entityType])
484
+ @@index([expiresAt])
485
+ }
486
+
487
+ // ============================================================================
488
+ // ORGANIZATION MEMBERSHIP (ADR-056 §3.6)
489
+ // ============================================================================
490
+
491
+ /// One person's membership of one organization. Unique on the pair, so adding a
492
+ /// membership is an idempotent upsert. `orgRole` is the organization role the
493
+ /// adopter token carried when the membership was created (MEMBER when it named none).
494
+ model OrganizationMembership {
495
+ id Int @id @default(autoincrement())
496
+ individualUserId Int
497
+ organizationUserId Int
498
+ orgRole String @default("MEMBER")
499
+ createdAt DateTime @default(now())
500
+
501
+ individualUser User @relation("IndividualMemberships", fields: [individualUserId], references: [id], onDelete: Cascade, onUpdate: NoAction)
502
+ organizationUser User @relation("OrganizationMemberships", fields: [organizationUserId], references: [id], onDelete: Cascade, onUpdate: NoAction)
503
+
504
+ @@unique([individualUserId, organizationUserId])
505
+ @@index([organizationUserId])
506
+ }
507
+
508
+ /// One-time scheduled jobs for poll-and-dispatch scheduling pattern.
509
+ /// Used by providers without native one-time scheduling APIs (e.g., Netlify).
510
+ /// A cron function queries for due jobs and dispatches them to a queue.
511
+ model ScheduledJob {
512
+ id Int @id @default(autoincrement())
513
+ scheduleName String @unique
514
+ scheduledAt DateTime
515
+ queueResourceId String
516
+ payload Json?
517
+ state String @default("PENDING")
518
+
519
+ createdAt DateTime @default(now())
520
+ updatedAt DateTime @updatedAt
521
+
522
+ @@index([state, scheduledAt])
523
+ }
@@ -132,6 +132,7 @@ exports.Prisma.EntityScalarFieldEnum = {
132
132
  name: 'name',
133
133
  moduleName: 'moduleName',
134
134
  externalId: 'externalId',
135
+ organizationId: 'organizationId',
135
136
  data: 'data',
136
137
  createdAt: 'createdAt',
137
138
  updatedAt: 'updatedAt'
@@ -141,6 +142,7 @@ exports.Prisma.IntegrationScalarFieldEnum = {
141
142
  id: 'id',
142
143
  userId: 'userId',
143
144
  status: 'status',
145
+ organizationId: 'organizationId',
144
146
  config: 'config',
145
147
  version: 'version',
146
148
  errors: 'errors',
@@ -253,6 +255,40 @@ exports.Prisma.UsageCounterScalarFieldEnum = {
253
255
  updatedAt: 'updatedAt'
254
256
  };
255
257
 
258
+ exports.Prisma.AuthorizationSessionScalarFieldEnum = {
259
+ id: 'id',
260
+ sessionId: 'sessionId',
261
+ userId: 'userId',
262
+ entityType: 'entityType',
263
+ credentialId: 'credentialId',
264
+ currentStep: 'currentStep',
265
+ maxSteps: 'maxSteps',
266
+ stepData: 'stepData',
267
+ expiresAt: 'expiresAt',
268
+ completed: 'completed',
269
+ createdAt: 'createdAt',
270
+ updatedAt: 'updatedAt'
271
+ };
272
+
273
+ exports.Prisma.OrganizationMembershipScalarFieldEnum = {
274
+ id: 'id',
275
+ individualUserId: 'individualUserId',
276
+ organizationUserId: 'organizationUserId',
277
+ orgRole: 'orgRole',
278
+ createdAt: 'createdAt'
279
+ };
280
+
281
+ exports.Prisma.ScheduledJobScalarFieldEnum = {
282
+ id: 'id',
283
+ scheduleName: 'scheduleName',
284
+ scheduledAt: 'scheduledAt',
285
+ queueResourceId: 'queueResourceId',
286
+ payload: 'payload',
287
+ state: 'state',
288
+ createdAt: 'createdAt',
289
+ updatedAt: 'updatedAt'
290
+ };
291
+
256
292
  exports.Prisma.SortOrder = {
257
293
  asc: 'asc',
258
294
  desc: 'desc'
@@ -326,7 +362,10 @@ exports.Prisma.ModelName = {
326
362
  WebsocketConnection: 'WebsocketConnection',
327
363
  AdminScriptExecution: 'AdminScriptExecution',
328
364
  ScriptSchedule: 'ScriptSchedule',
329
- UsageCounter: 'UsageCounter'
365
+ UsageCounter: 'UsageCounter',
366
+ AuthorizationSession: 'AuthorizationSession',
367
+ OrganizationMembership: 'OrganizationMembership',
368
+ ScheduledJob: 'ScheduledJob'
330
369
  };
331
370
  /**
332
371
  * Create the Client
@@ -380,13 +419,13 @@ const config = {
380
419
  }
381
420
  }
382
421
  },
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/// Durable usage counters, deliberately isolated: no userId and no foreign key\n/// to Integration, so a user-scoped query can never return a usage row and usage\n/// history survives integration deletion.\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": "479850322beddd1dd3603ea62e13dd64fd3fb9c7b99da74bb7f40fe6450e65ae",
422
+ "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 // Organization memberships (ADR-056 §3.6). `organizationId` above is the legacy\n // single-org pointer: set on a person's first link, never moved afterwards, and\n // read as an implicit membership when no row exists for it.\n // As an individual: the organizations this person belongs to.\n memberships OrganizationMembership[] @relation(\"IndividualMemberships\")\n // As an organization: the people who belong to it.\n orgMembers OrganizationMembership[] @relation(\"OrganizationMemberships\")\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 // Organization this entity was connected in (ADR-056 §3.6): the active\n // organization for `individual`, the owner for `organization`, null for\n // `global`. Null on an `individual` entity is also what every row written\n // before this column existed has, and what `individualAcross: 'all'` writes.\n // Deliberately not a relation: a deleted organization must leave its rows\n // invisible, not turn them into \"unscoped\" ones (SetNull would do that).\n organizationId Int?\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([userId, organizationId])\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 // Organization this integration was created in (ADR-056 §3.6): the active\n // organization at create time. Null for rows that predate the column. Not a\n // relation, for the same reason as Entity.organizationId.\n organizationId Int?\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([userId, organizationId])\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/// Durable usage counters, deliberately isolated: no userId and no foreign key\n/// to Integration, so a user-scoped query can never return a usage row and usage\n/// history survives integration deletion.\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\n// ============================================================================\n// MANAGEMENT API MODELS\n// ============================================================================\n\n/// Multi-step authorization state for Management API v2 (/api/v2/authorize,\n/// /api/v2/credentials/:id/reauthorize). Short-lived: expired rows are removed\n/// opportunistically. stepData is encrypted at rest.\nmodel AuthorizationSession {\n id Int @id @default(autoincrement())\n sessionId String @unique\n userId String\n entityType String\n credentialId String?\n currentStep Int @default(1)\n maxSteps Int\n stepData Json @default(\"{}\")\n expiresAt DateTime\n completed Boolean @default(false)\n\n createdAt DateTime @default(now())\n updatedAt DateTime @updatedAt\n\n @@index([userId, entityType])\n @@index([expiresAt])\n}\n\n// ============================================================================\n// ORGANIZATION MEMBERSHIP (ADR-056 §3.6)\n// ============================================================================\n\n/// One person's membership of one organization. Unique on the pair, so adding a\n/// membership is an idempotent upsert. `orgRole` is the organization role the\n/// adopter token carried when the membership was created (MEMBER when it named none).\nmodel OrganizationMembership {\n id Int @id @default(autoincrement())\n individualUserId Int\n organizationUserId Int\n orgRole String @default(\"MEMBER\")\n createdAt DateTime @default(now())\n\n individualUser User @relation(\"IndividualMemberships\", fields: [individualUserId], references: [id], onDelete: Cascade, onUpdate: NoAction)\n organizationUser User @relation(\"OrganizationMemberships\", fields: [organizationUserId], references: [id], onDelete: Cascade, onUpdate: NoAction)\n\n @@unique([individualUserId, organizationUserId])\n @@index([organizationUserId])\n}\n\n/// One-time scheduled jobs for poll-and-dispatch scheduling pattern.\n/// Used by providers without native one-time scheduling APIs (e.g., Netlify).\n/// A cron function queries for due jobs and dispatches them to a queue.\nmodel ScheduledJob {\n id Int @id @default(autoincrement())\n scheduleName String @unique\n scheduledAt DateTime\n queueResourceId String\n payload Json?\n state String @default(\"PENDING\")\n\n createdAt DateTime @default(now())\n updatedAt DateTime @updatedAt\n\n @@index([state, scheduledAt])\n}\n",
423
+ "inlineSchemaHash": "cd83debc50898262e61b16804eb39c638e3cbc9f3d95de5c6f15940722985a72",
385
424
  "copyEngine": true
386
425
  }
387
426
  config.dirname = '/'
388
427
 
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\":{}}")
428
+ 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\"},{\"name\":\"memberships\",\"kind\":\"object\",\"type\":\"OrganizationMembership\",\"relationName\":\"IndividualMemberships\"},{\"name\":\"orgMembers\",\"kind\":\"object\",\"type\":\"OrganizationMembership\",\"relationName\":\"OrganizationMemberships\"}],\"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\":\"organizationId\",\"kind\":\"scalar\",\"type\":\"Int\"},{\"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\":\"organizationId\",\"kind\":\"scalar\",\"type\":\"Int\"},{\"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},\"AuthorizationSession\":{\"fields\":[{\"name\":\"id\",\"kind\":\"scalar\",\"type\":\"Int\"},{\"name\":\"sessionId\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"userId\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"entityType\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"credentialId\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"currentStep\",\"kind\":\"scalar\",\"type\":\"Int\"},{\"name\":\"maxSteps\",\"kind\":\"scalar\",\"type\":\"Int\"},{\"name\":\"stepData\",\"kind\":\"scalar\",\"type\":\"Json\"},{\"name\":\"expiresAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"completed\",\"kind\":\"scalar\",\"type\":\"Boolean\"},{\"name\":\"createdAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"updatedAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"}],\"dbName\":null},\"OrganizationMembership\":{\"fields\":[{\"name\":\"id\",\"kind\":\"scalar\",\"type\":\"Int\"},{\"name\":\"individualUserId\",\"kind\":\"scalar\",\"type\":\"Int\"},{\"name\":\"organizationUserId\",\"kind\":\"scalar\",\"type\":\"Int\"},{\"name\":\"orgRole\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"createdAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"individualUser\",\"kind\":\"object\",\"type\":\"User\",\"relationName\":\"IndividualMemberships\"},{\"name\":\"organizationUser\",\"kind\":\"object\",\"type\":\"User\",\"relationName\":\"OrganizationMemberships\"}],\"dbName\":null},\"ScheduledJob\":{\"fields\":[{\"name\":\"id\",\"kind\":\"scalar\",\"type\":\"Int\"},{\"name\":\"scheduleName\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"scheduledAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"queueResourceId\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"payload\",\"kind\":\"scalar\",\"type\":\"Json\"},{\"name\":\"state\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"createdAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"updatedAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"}],\"dbName\":null}},\"enums\":{},\"types\":{}}")
390
429
  defineDmmfProperty(exports.Prisma, config.runtimeDataModel)
391
430
  config.engineWasm = {
392
431
  getRuntime: async () => require('./query_engine_bg.js'),
@@ -2,21 +2,26 @@ const { findNearestBackendPackageJson } = require('@friggframework/core/utils');
2
2
  const path = require('node:path');
3
3
  const fs = require('fs-extra');
4
4
  const { resolveTelemetryConfig } = require('../telemetry/telemetry-config');
5
+ const { assertAdopterJwtConfig } = require('../user/adopter-jwt-config');
5
6
  const { registerDeniedKeys } = require('../logs/denied-keys');
6
7
  const {
7
8
  extractCredentialFieldsFromModules,
8
9
  } = require('../database/encryption/encryption-schema-registry');
9
10
  const {
10
- getModulesDefinitionFromIntegrationClasses,
11
- } = require('../integrations/utils/map-integration-dto');
11
+ getEntityTypeRegistry,
12
+ getEntityTypeDefinitions,
13
+ } = require('../modules/entity-type-registry');
14
+ const { resolveOwnershipConfig } = require('../modules/entity-scope');
15
+ const {
16
+ resolveManagementApiConfig,
17
+ } = require('../management-api/management-api-config');
18
+ const { resolveScheduleConfig } = require('../integrations/schedule-config');
12
19
 
13
20
  // Handlers without a database never load the encryption registry, so the
14
21
  // credential leaf keys are registered for redaction here too.
15
- function registerCredentialLogKeys(appDefinition, integrations) {
22
+ function registerCredentialLogKeys(appDefinition, moduleDefinitions) {
16
23
  try {
17
- extractCredentialFieldsFromModules(
18
- getModulesDefinitionFromIntegrationClasses(integrations)
19
- );
24
+ extractCredentialFieldsFromModules(moduleDefinitions);
20
25
  } catch {
21
26
  // An odd integration shape must not stop the app from loading.
22
27
  }
@@ -29,18 +34,53 @@ function registerCredentialLogKeys(appDefinition, integrations) {
29
34
  }
30
35
 
31
36
  /**
32
- * Loads the App definition from the nearest backend package
33
- * @function loadAppDefinition
34
- * @description Searches for the nearest backend package.json, loads the corresponding index.js file,
35
- * and extracts the application definition containing integrations and user configuration.
36
- * @returns {{integrations: Array<object>, userConfig: object | null, adminScripts: Array<object>, reports: Array<object>, admin: object, telemetry: object, logging: object | null}} An object containing the application definition.
37
- * @throws {Error} Throws error if backend package.json cannot be found.
38
- * @throws {Error} Throws error if index.js file cannot be found in the backend directory.
39
- * @example
40
- * const { integrations, userConfig, telemetry } = loadAppDefinition();
41
- * console.log(`Found ${integrations.length} integrations`);
37
+ * The definition handed to setAppDefinition(), or null when the backend is
38
+ * discovered from process.cwd().
42
39
  */
43
- function loadAppDefinition() {
40
+ let presetDefinition = null;
41
+
42
+ /**
43
+ * Pre-set the app definition so loadAppDefinition() does not discover the
44
+ * backend through process.cwd().
45
+ *
46
+ * Platforms such as Netlify need this: the function bundler (nft/esbuild)
47
+ * only traces static require() calls, and at runtime process.cwd() is the
48
+ * function root, not the backend. A generated function entry point therefore
49
+ * requires the backend's index.js statically and passes its Definition here.
50
+ *
51
+ * The preset definition is validated and derived exactly like a discovered
52
+ * one: adopter-JWT config, the entity registry, ownership and the Management
53
+ * API config all apply, so a Netlify function and an AWS Lambda load the
54
+ * same app the same way. Pass null to clear it.
55
+ *
56
+ * @param {Object|null} definition - the backend's `Definition` export
57
+ */
58
+ function setAppDefinition(definition) {
59
+ if (
60
+ definition !== null &&
61
+ (typeof definition !== 'object' || Array.isArray(definition))
62
+ ) {
63
+ throw new TypeError(
64
+ 'setAppDefinition expects the app Definition object (or null)'
65
+ );
66
+ }
67
+ presetDefinition = definition;
68
+ }
69
+
70
+ /**
71
+ * The definition preset with setAppDefinition(), unvalidated, or null.
72
+ *
73
+ * For low-level readers that only need one raw field before the app is
74
+ * fully loaded (the database type, the custom encryption schema) and keep
75
+ * their own process.cwd() discovery as the fallback.
76
+ *
77
+ * @returns {Object|null}
78
+ */
79
+ function getPresetAppDefinition() {
80
+ return presetDefinition;
81
+ }
82
+
83
+ function discoverAppDefinition() {
44
84
  const backendPath = findNearestBackendPackageJson();
45
85
  if (!backendPath) {
46
86
  throw new Error('Could not find backend package.json');
@@ -53,18 +93,72 @@ function loadAppDefinition() {
53
93
  }
54
94
 
55
95
  const backendJsFile = require(backendFilePath);
56
- const appDefinition = backendJsFile.Definition;
96
+ return backendJsFile.Definition;
97
+ }
98
+
99
+ /**
100
+ * Loads the App definition: the one preset with setAppDefinition(), or the
101
+ * nearest backend package's index.js.
102
+ * @function loadAppDefinition
103
+ * @description Uses the definition preset by setAppDefinition() when there is one; otherwise searches for the nearest backend package.json, loads the corresponding index.js file,
104
+ * and extracts the application definition containing integrations and user configuration.
105
+ * @returns {{appDefinition: object, integrations: Array<object>, entities: object | null, entityTypeRegistry: Map<string, object>, moduleDefinitions: Array<object>, ownership: {scope: ({default: string, allow: string[]}|null), individualAcross: string}, userConfig: object | null, adminScripts: Array<object>, reports: Array<object>, admin: object, telemetry: object, logging: object | null, managementApi: object}} An object containing the application definition. `appDefinition` is the raw Definition object.
106
+ * @throws {Error} Throws error if backend package.json cannot be found.
107
+ * @throws {Error} Throws error if index.js file cannot be found in the backend directory.
108
+ * @throws {Error} Throws error if user.authModes.adopterJwt is enabled without a usable user.jwtConfig.
109
+ * @throws {OwnershipConfigError} Throws if an ownership or scope setting is invalid (ADR-056 §3.1).
110
+ * @throws {ManagementApiConfigError} Throws if `managementApi.proxy.scopes` is invalid (ADR-056 §3.5).
111
+ * @throws {ScheduleConfigError} Throws if an integration's `Definition.schedule` is invalid.
112
+ * @example
113
+ * const { integrations, userConfig, telemetry } = loadAppDefinition();
114
+ * console.log(`Found ${integrations.length} integrations`);
115
+ */
116
+ function loadAppDefinition() {
117
+ const appDefinition = presetDefinition || discoverAppDefinition();
118
+ return resolveAppDefinition(appDefinition);
119
+ }
57
120
 
121
+ function resolveAppDefinition(appDefinition) {
58
122
  const {
59
123
  integrations = [],
124
+ entities = null,
60
125
  user: userConfig = null,
61
126
  adminScripts = [],
62
127
  reports = [],
63
128
  admin = {},
64
129
  logging = null,
130
+ managementApi = {},
65
131
  } = appDefinition;
66
132
 
67
- registerCredentialLogKeys(appDefinition, integrations);
133
+ // Fail at load, not on the first request: an adopter-token mode with no
134
+ // key source would otherwise start cleanly and answer every login with 500.
135
+ assertAdopterJwtConfig(userConfig);
136
+
137
+ // Fail fast: a bad `entities` registry (key/name mismatch, conflicting
138
+ // definitions, `$infer: false` with an unregistered module) must stop the
139
+ // app at load, not on the first request that needs a module.
140
+ const entityTypeRegistry = getEntityTypeRegistry(appDefinition);
141
+ const moduleDefinitions = getEntityTypeDefinitions(entityTypeRegistry);
142
+
143
+ // Same reason for entity scopes (ADR-056 §3.1): a mistyped scope, an unknown
144
+ // `individualAcross`, or an organization-owned integration that binds an
145
+ // individual entity stops the app at load.
146
+ const ownership = resolveOwnershipConfig(appDefinition, entityTypeRegistry);
147
+
148
+ // And for the Management API: `managementApi.proxy.scopes` decides whose
149
+ // credentials the entity proxy may act through (ADR-056 §3.5), so an
150
+ // unknown scope stops the app here rather than being read as a default.
151
+ resolveManagementApiConfig(managementApi);
152
+
153
+ // And for each integration's `Definition.schedule`: a mistyped scope would
154
+ // otherwise tick ONGOING_SYNC per class when per installation was meant.
155
+ for (const IntegrationClass of integrations) {
156
+ if (IntegrationClass?.Definition) {
157
+ resolveScheduleConfig(IntegrationClass.Definition);
158
+ }
159
+ }
160
+
161
+ registerCredentialLogKeys(appDefinition, moduleDefinitions);
68
162
 
69
163
  // Degrade consistently: an invalid telemetry block must never take down a
70
164
  // router bundle that loads the app definition at module scope (telemetry is
@@ -88,16 +182,24 @@ function loadAppDefinition() {
88
182
  }
89
183
 
90
184
  return {
185
+ appDefinition,
91
186
  integrations,
187
+ entities,
188
+ entityTypeRegistry,
189
+ moduleDefinitions,
190
+ ownership,
92
191
  userConfig,
93
192
  adminScripts,
94
193
  reports,
95
194
  admin,
96
195
  telemetry,
97
196
  logging,
197
+ managementApi,
98
198
  };
99
199
  }
100
200
 
101
201
  module.exports = {
102
202
  loadAppDefinition,
203
+ setAppDefinition,
204
+ getPresetAppDefinition,
103
205
  };
@@ -6,6 +6,12 @@ const bodyParser = require('body-parser');
6
6
  const cors = require('cors');
7
7
  const Boom = require('@hapi/boom');
8
8
  const serverlessHttp = require('serverless-http');
9
+ const {
10
+ API_VERSION_HEADER,
11
+ isVersionedManagementApiPath,
12
+ toBoom,
13
+ toV2ErrorBody,
14
+ } = require('../management-api/error-response');
9
15
 
10
16
  const log = getLogger('frigg.http');
11
17
 
@@ -26,11 +32,15 @@ const createApp = (applyMiddleware) => {
26
32
  if (applyMiddleware) applyMiddleware(app);
27
33
 
28
34
  // The express boundary: send the error response and log it one time.
35
+ // A Boom error keeps its status; other errors keep theirs only when they
36
+ // mark a 4xx safe to expose (body-parser's 400/413 via http-errors), so a
37
+ // malformed body is a 400, not a 500.
29
38
  app.use((err, req, res, next) => {
30
- const boomError = err.isBoom ? err : Boom.boomify(err);
39
+ const boomError = toBoom(err);
31
40
  const {
32
41
  output: { statusCode = 500 },
33
42
  } = boomError;
43
+ const versioned = isVersionedManagementApiPath(req.path);
34
44
 
35
45
  if (statusCode >= 500) {
36
46
  log.error('Request failed', {
@@ -39,7 +49,6 @@ const createApp = (applyMiddleware) => {
39
49
  invocation: summarizeExpressRequest(req),
40
50
  error: boomError,
41
51
  });
42
- res.status(statusCode).json({ error: 'Internal Server Error' });
43
52
  } else {
44
53
  // A client error needs no stack; the logger scrubs the reason.
45
54
  log.warn('Request rejected', {
@@ -47,8 +56,21 @@ const createApp = (applyMiddleware) => {
47
56
  statusCode,
48
57
  reason: boomError.message,
49
58
  });
50
- res.status(statusCode).json({ error: err.message });
51
59
  }
60
+
61
+ if (versioned) {
62
+ // Management API v2 / meta: { error: { code, message, details? } }.
63
+ if (req.path.startsWith('/api/v2')) {
64
+ res.set(API_VERSION_HEADER, '2');
65
+ }
66
+ return res.status(statusCode).json(toV2ErrorBody(boomError));
67
+ }
68
+
69
+ // v1 and other routes keep their historical body.
70
+ if (statusCode >= 500) {
71
+ return res.status(statusCode).json({ error: 'Internal Server Error' });
72
+ }
73
+ return res.status(statusCode).json({ error: err.message });
52
74
  });
53
75
 
54
76
  return app;