@friggframework/core 2.0.0--canary.548.c8ae0ca.0 → 2.0.0--canary.545.c40eca4.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 (237) hide show
  1. package/CHANGELOG.md +66 -67
  2. package/CLAUDE.md +148 -124
  3. package/README.md +165 -130
  4. package/admin-scripts/index.js +52 -0
  5. package/admin-scripts/repositories/admin-api-key-repository-documentdb.js +21 -0
  6. package/admin-scripts/repositories/admin-api-key-repository-factory.js +51 -0
  7. package/admin-scripts/repositories/admin-api-key-repository-interface.js +121 -0
  8. package/admin-scripts/repositories/admin-api-key-repository-mongo.js +155 -0
  9. package/admin-scripts/repositories/admin-api-key-repository-postgres.js +189 -0
  10. package/admin-scripts/repositories/admin-process-repository-documentdb.js +21 -0
  11. package/admin-scripts/repositories/admin-process-repository-factory.js +49 -0
  12. package/admin-scripts/repositories/admin-process-repository-interface.js +150 -0
  13. package/admin-scripts/repositories/admin-process-repository-mongo.js +213 -0
  14. package/admin-scripts/repositories/admin-process-repository-postgres.js +251 -0
  15. package/admin-scripts/repositories/script-execution-repository-documentdb.js +21 -0
  16. package/admin-scripts/repositories/script-execution-repository-factory.js +51 -0
  17. package/admin-scripts/repositories/script-execution-repository-interface.js +193 -0
  18. package/admin-scripts/repositories/script-execution-repository-mongo.js +278 -0
  19. package/admin-scripts/repositories/script-execution-repository-postgres.js +320 -0
  20. package/admin-scripts/repositories/script-schedule-repository-documentdb.js +21 -0
  21. package/admin-scripts/repositories/script-schedule-repository-factory.js +49 -0
  22. package/admin-scripts/repositories/script-schedule-repository-interface.js +108 -0
  23. package/admin-scripts/repositories/script-schedule-repository-mongo.js +179 -0
  24. package/admin-scripts/repositories/script-schedule-repository-postgres.js +210 -0
  25. package/application/commands/admin-script-commands.js +348 -0
  26. package/application/commands/integration-commands.js +12 -6
  27. package/application/index.js +5 -3
  28. package/assertions/get.js +17 -7
  29. package/associations/association.js +59 -57
  30. package/core/CLAUDE.md +222 -156
  31. package/core/Worker.js +33 -12
  32. package/credential/credential-router.js +262 -0
  33. package/credential/repositories/credential-repository-documentdb.js +71 -1
  34. package/credential/repositories/credential-repository-factory.js +4 -6
  35. package/credential/repositories/credential-repository-interface.js +8 -2
  36. package/credential/repositories/credential-repository-mongo.js +45 -4
  37. package/credential/repositories/credential-repository-postgres.js +44 -3
  38. package/credential/repositories/credential-repository.js +3 -1
  39. package/credential/use-cases/delete-credential-for-user.js +50 -0
  40. package/credential/use-cases/get-credential-for-user.js +21 -3
  41. package/credential/use-cases/list-credentials-for-user.js +31 -0
  42. package/credential/use-cases/reauthorize-credential.js +103 -0
  43. package/credential/use-cases/update-authentication-status.js +5 -2
  44. package/database/MONGODB_TRANSACTION_FIX.md +72 -52
  45. package/database/config.js +13 -59
  46. package/database/documentdb-encryption-service.js +29 -13
  47. package/database/documentdb-utils.js +21 -12
  48. package/database/encryption/README.md +53 -36
  49. package/database/encryption/encryption-schema-registry.js +29 -23
  50. package/database/encryption/field-encryption-service.js +15 -4
  51. package/database/encryption/logger.js +3 -2
  52. package/database/encryption/prisma-encryption-extension.js +5 -8
  53. package/database/prisma.js +22 -14
  54. package/database/repositories/health-check-repository-factory.js +7 -5
  55. package/database/repositories/health-check-repository-interface.js +15 -5
  56. package/database/repositories/health-check-repository-postgres.js +1 -1
  57. package/database/use-cases/check-database-health-use-case.js +3 -2
  58. package/database/use-cases/check-database-state-use-case.js +8 -7
  59. package/database/use-cases/get-database-state-via-worker-use-case.js +15 -12
  60. package/database/use-cases/get-migration-status-use-case.js +4 -2
  61. package/database/use-cases/run-database-migration-use-case.js +41 -9
  62. package/database/use-cases/test-encryption-use-case.js +4 -5
  63. package/database/use-cases/trigger-database-migration-use-case.js +9 -8
  64. package/database/utils/mongodb-collection-utils.js +11 -3
  65. package/database/utils/mongodb-schema-init.js +10 -4
  66. package/database/utils/prisma-runner.js +144 -74
  67. package/database/utils/prisma-schema-parser.js +2 -2
  68. package/docs/PROCESS_MANAGEMENT_QUEUE_SPEC.md +135 -80
  69. package/encrypt/Cryptor.js +54 -49
  70. package/encrypt/aes-encryption-key-provider.js +82 -0
  71. package/encrypt/encryption-key-provider-interface.js +50 -0
  72. package/errors/fetch-error.js +1 -1
  73. package/generated/prisma-mongodb/edge.js +59 -4
  74. package/generated/prisma-mongodb/index-browser.js +49 -1
  75. package/generated/prisma-mongodb/index.d.ts +4023 -61
  76. package/generated/prisma-mongodb/index.js +59 -4
  77. package/generated/prisma-mongodb/package.json +1 -1
  78. package/generated/prisma-mongodb/schema.prisma +87 -0
  79. package/generated/prisma-mongodb/wasm.js +59 -4
  80. package/generated/prisma-postgresql/edge.js +58 -4
  81. package/generated/prisma-postgresql/index-browser.js +48 -1
  82. package/generated/prisma-postgresql/index.d.ts +4738 -54
  83. package/generated/prisma-postgresql/index.js +58 -4
  84. package/generated/prisma-postgresql/package.json +1 -1
  85. package/generated/prisma-postgresql/schema.prisma +85 -0
  86. package/generated/prisma-postgresql/wasm.js +58 -4
  87. package/handlers/WEBHOOKS.md +104 -81
  88. package/handlers/app-definition-loader.js +53 -10
  89. package/handlers/app-handler-helpers.js +14 -3
  90. package/handlers/backend-utils.js +0 -1
  91. package/handlers/database-migration-handler.js +64 -45
  92. package/handlers/middleware/admin-auth.js +53 -0
  93. package/handlers/routers/HEALTHCHECK.md +170 -135
  94. package/handlers/routers/admin.js +474 -0
  95. package/handlers/routers/auth.js +51 -9
  96. package/handlers/routers/db-migration.handler.js +2 -3
  97. package/handlers/routers/db-migration.js +102 -66
  98. package/handlers/routers/docs.js +180 -0
  99. package/handlers/routers/health.js +84 -283
  100. package/handlers/routers/integration-defined-routers.js +44 -33
  101. package/handlers/routers/integration-webhook-routers.js +73 -52
  102. package/handlers/routers/middleware/loadUser.js +39 -0
  103. package/handlers/routers/middleware/requireAdmin.js +43 -0
  104. package/handlers/routers/middleware/requireLoggedInUser.js +19 -0
  105. package/handlers/routers/user.js +30 -14
  106. package/handlers/routers/websocket.js +18 -4
  107. package/handlers/use-cases/check-integrations-health-use-case.js +13 -6
  108. package/handlers/workers/db-migration.js +74 -35
  109. package/handlers/workers/integration-defined-workers.js +31 -20
  110. package/index.js +50 -16
  111. package/infrastructure/scheduler/index.js +15 -11
  112. package/infrastructure/scheduler/netlify-scheduler-adapter.js +185 -0
  113. package/infrastructure/scheduler/scheduler-service-factory.js +38 -31
  114. package/integrations/WEBHOOK-QUICKSTART.md +21 -14
  115. package/integrations/index.js +4 -0
  116. package/integrations/integration-base.js +9 -6
  117. package/integrations/integration-router.js +1828 -157
  118. package/integrations/options.js +47 -20
  119. package/integrations/repositories/integration-mapping-repository-factory.js +4 -8
  120. package/integrations/repositories/integration-repository-documentdb.js +53 -24
  121. package/integrations/repositories/integration-repository-factory.js +6 -6
  122. package/integrations/repositories/integration-repository-interface.js +27 -9
  123. package/integrations/repositories/integration-repository-postgres.js +3 -3
  124. package/integrations/repositories/process-repository-factory.js +4 -7
  125. package/integrations/repositories/process-repository-interface.js +9 -5
  126. package/integrations/repositories/process-repository-mongo.js +7 -3
  127. package/integrations/repositories/process-repository-postgres.js +2 -2
  128. package/integrations/tests/doubles/config-capturing-integration.js +9 -8
  129. package/integrations/tests/doubles/dummy-integration-class.js +81 -8
  130. package/integrations/tests/doubles/test-integration-repository.js +51 -11
  131. package/integrations/use-cases/create-integration.js +36 -32
  132. package/integrations/use-cases/create-process.js +20 -9
  133. package/integrations/use-cases/delete-integration-for-user.js +5 -4
  134. package/integrations/use-cases/execute-proxy-request.js +564 -0
  135. package/integrations/use-cases/get-integration-for-user.js +25 -10
  136. package/integrations/use-cases/get-integration-instance-by-definition.js +13 -7
  137. package/integrations/use-cases/get-integrations-for-user.js +4 -1
  138. package/integrations/use-cases/get-possible-integrations.js +1 -1
  139. package/integrations/use-cases/get-process.js +5 -6
  140. package/integrations/use-cases/index.js +3 -1
  141. package/integrations/use-cases/load-integration-context.js +1 -5
  142. package/integrations/use-cases/update-process-state.js +10 -8
  143. package/integrations/utils/map-integration-dto.js +9 -3
  144. package/jest-setup.js +1 -1
  145. package/jest-teardown.js +1 -1
  146. package/lambda/TimeoutCatcher.js +32 -30
  147. package/lambda/index.js +1 -1
  148. package/logs/index.js +3 -3
  149. package/modules/domain/entities/AuthorizationSession.js +131 -0
  150. package/modules/domain/entities/index.js +10 -0
  151. package/modules/module.js +4 -9
  152. package/modules/repositories/authorization-session-repository-factory.js +53 -0
  153. package/modules/repositories/authorization-session-repository-interface.js +75 -0
  154. package/modules/repositories/authorization-session-repository-mongo.js +169 -0
  155. package/modules/repositories/authorization-session-repository-postgres.js +170 -0
  156. package/modules/repositories/module-repository-documentdb.js +89 -41
  157. package/modules/repositories/module-repository-factory.js +4 -6
  158. package/modules/repositories/module-repository-interface.js +13 -0
  159. package/modules/repositories/module-repository-mongo.js +40 -5
  160. package/modules/repositories/module-repository-postgres.js +42 -5
  161. package/modules/repositories/module-repository.js +28 -0
  162. package/modules/requester/api-key.js +0 -2
  163. package/modules/requester/basic.js +0 -1
  164. package/modules/requester/oauth-2.js +3 -14
  165. package/modules/test/mock-api/definition.js +15 -10
  166. package/modules/test/mock-api/mocks/hubspot.js +36 -36
  167. package/modules/tests/doubles/test-module-factory.js +10 -4
  168. package/modules/tests/doubles/test-module-repository.js +1 -1
  169. package/modules/use-cases/delete-module-entity.js +23 -0
  170. package/modules/use-cases/get-authorization-requirements.js +110 -0
  171. package/modules/use-cases/get-entities-for-user.js +1 -1
  172. package/modules/use-cases/get-entity-options-by-id.js +8 -6
  173. package/modules/use-cases/get-module-entity-by-id.js +16 -0
  174. package/modules/use-cases/get-module.js +12 -9
  175. package/modules/use-cases/process-authorization-callback.js +12 -6
  176. package/modules/use-cases/process-authorization-step.js +160 -0
  177. package/modules/use-cases/refresh-entity-options.js +8 -6
  178. package/modules/use-cases/start-authorization-session.js +86 -0
  179. package/modules/use-cases/test-module-auth.js +8 -6
  180. package/modules/use-cases/update-module-entity.js +27 -0
  181. package/modules/utils/map-module-dto.js +2 -2
  182. package/openapi/openapi-spec-generator.js +370 -0
  183. package/openapi/openapi-v1.yaml +1360 -0
  184. package/openapi/openapi-v2.yaml +1535 -0
  185. package/package.json +7 -9
  186. package/prisma-mongodb/schema.prisma +87 -0
  187. package/prisma-postgresql/migrations/20260222000000_add_admin_process_and_script_schedule/migration.sql +55 -0
  188. package/prisma-postgresql/schema.prisma +85 -0
  189. package/providers/index.js +13 -0
  190. package/providers/resolve-provider.js +93 -0
  191. package/queues/index.js +19 -0
  192. package/queues/providers/index.js +7 -0
  193. package/queues/providers/netlify-background-provider.js +76 -0
  194. package/queues/providers/qstash-queue-provider.js +122 -0
  195. package/queues/queue-client-interface.js +55 -0
  196. package/queues/queue-provider-factory.js +93 -0
  197. package/queues/queue-provider.js +46 -0
  198. package/queues/queuer-util.js +33 -25
  199. package/syncs/repositories/sync-repository-documentdb.js +39 -19
  200. package/syncs/repositories/sync-repository-factory.js +4 -4
  201. package/syncs/repositories/sync-repository-interface.js +6 -2
  202. package/syncs/repositories/sync-repository-postgres.js +19 -13
  203. package/syncs/sync.js +104 -100
  204. package/token/repositories/token-repository-factory.js +4 -6
  205. package/types/assertions/index.d.ts +72 -72
  206. package/types/core/index.d.ts +98 -53
  207. package/types/errors/index.d.ts +61 -54
  208. package/types/eslint-config/index.d.ts +40 -40
  209. package/types/lambda/index.d.ts +26 -26
  210. package/types/logs/index.d.ts +4 -4
  211. package/types/prettier-config/index.d.ts +5 -5
  212. package/types/test-environment/index.d.ts +14 -14
  213. package/types/tsconfig.json +95 -93
  214. package/user/repositories/user-repository-factory.js +4 -4
  215. package/user/tests/doubles/test-user-repository.js +5 -2
  216. package/user/use-cases/authenticate-user.js +2 -4
  217. package/user/use-cases/authenticate-with-shared-secret.js +1 -1
  218. package/user/use-cases/create-individual-user.js +8 -7
  219. package/user/use-cases/create-organization-user.js +1 -1
  220. package/user/use-cases/create-token-for-user-id.js +1 -1
  221. package/user/use-cases/delete-user.js +47 -0
  222. package/user/use-cases/get-user-from-adopter-jwt.js +0 -2
  223. package/user/use-cases/get-user-from-bearer-token.js +7 -3
  224. package/user/use-cases/get-user-from-x-frigg-headers.js +14 -11
  225. package/user/use-cases/login-user.js +8 -8
  226. package/user/user.js +18 -4
  227. package/utils/backend-path.js +2 -2
  228. package/utils/index.js +4 -1
  229. package/websocket/repositories/websocket-connection-repository-documentdb.js +46 -21
  230. package/websocket/repositories/websocket-connection-repository-factory.js +4 -8
  231. package/websocket/repositories/websocket-connection-repository-mongo.js +26 -16
  232. package/websocket/repositories/websocket-connection-repository-postgres.js +26 -15
  233. package/websocket/repositories/websocket-connection-repository.js +26 -20
  234. package/websocket/websocket-message-sender-interface.js +38 -0
  235. package/database/adapters/lambda-invoker.js +0 -97
  236. package/database/repositories/migration-status-repository-s3.js +0 -137
  237. package/infrastructure/scheduler/eventbridge-scheduler-adapter.js +0 -184
package/package.json CHANGED
@@ -1,12 +1,8 @@
1
1
  {
2
2
  "name": "@friggframework/core",
3
3
  "prettier": "@friggframework/prettier-config",
4
- "version": "2.0.0--canary.548.c8ae0ca.0",
4
+ "version": "2.0.0--canary.545.c40eca4.0",
5
5
  "dependencies": {
6
- "@aws-sdk/client-apigatewaymanagementapi": "^3.588.0",
7
- "@aws-sdk/client-kms": "^3.588.0",
8
- "@aws-sdk/client-lambda": "^3.714.0",
9
- "@aws-sdk/client-sqs": "^3.588.0",
10
6
  "@hapi/boom": "^10.0.1",
11
7
  "bcryptjs": "^2.4.3",
12
8
  "body-parser": "^1.20.2",
@@ -19,6 +15,7 @@
19
15
  "express-async-handler": "^1.2.0",
20
16
  "form-data": "^4.0.0",
21
17
  "fs-extra": "^11.2.0",
18
+ "js-yaml": "^4.1.0",
22
19
  "lodash": "4.17.21",
23
20
  "lodash.get": "^4.4.2",
24
21
  "node-fetch": "^2.6.7",
@@ -38,9 +35,9 @@
38
35
  }
39
36
  },
40
37
  "devDependencies": {
41
- "@friggframework/eslint-config": "2.0.0--canary.548.c8ae0ca.0",
42
- "@friggframework/prettier-config": "2.0.0--canary.548.c8ae0ca.0",
43
- "@friggframework/test": "2.0.0--canary.548.c8ae0ca.0",
38
+ "@friggframework/eslint-config": "2.0.0--canary.545.c40eca4.0",
39
+ "@friggframework/prettier-config": "2.0.0--canary.545.c40eca4.0",
40
+ "@friggframework/test": "2.0.0--canary.545.c40eca4.0",
44
41
  "@prisma/client": "^6.17.0",
45
42
  "@types/lodash": "4.17.15",
46
43
  "@typescript-eslint/eslint-plugin": "^8.0.0",
@@ -53,6 +50,7 @@
53
50
  "prettier": "^2.7.1",
54
51
  "prisma": "^6.17.0",
55
52
  "sinon": "^16.1.1",
53
+ "supertest": "^7.1.4",
56
54
  "typescript": "^5.0.2"
57
55
  },
58
56
  "scripts": {
@@ -80,5 +78,5 @@
80
78
  "publishConfig": {
81
79
  "access": "public"
82
80
  },
83
- "gitHead": "c8ae0ca7837ba8393e3617be15c941607627c16b"
81
+ "gitHead": "c40eca49e72472f0499b24ce709a655a7503727b"
84
82
  }
@@ -360,3 +360,90 @@ model WebsocketConnection {
360
360
  @@index([connectionId])
361
361
  @@map("WebsocketConnection")
362
362
  }
363
+
364
+ // ============================================================================
365
+ // ADMIN PROCESS MODELS
366
+ // ============================================================================
367
+
368
+ /// Admin process state machine
369
+ enum AdminProcessState {
370
+ PENDING
371
+ RUNNING
372
+ COMPLETED
373
+ FAILED
374
+ }
375
+
376
+ /// Admin trigger types
377
+ enum AdminTrigger {
378
+ MANUAL
379
+ SCHEDULED
380
+ QUEUE
381
+ WEBHOOK
382
+ }
383
+
384
+ /// Admin process tracking (like Process but without user/integration FK)
385
+ /// Used for: admin scripts, db migrations, system maintenance tasks
386
+ model AdminProcess {
387
+ id String @id @default(auto()) @map("_id") @db.ObjectId
388
+ name String // e.g., "oauth-token-refresh", "db-migration-xyz"
389
+ type String // e.g., "ADMIN_SCRIPT", "DB_MIGRATION"
390
+
391
+ // State machine
392
+ state AdminProcessState @default(PENDING)
393
+
394
+ // Flexible storage (mirrors Process model pattern)
395
+ context Json @default("{}") // input, trigger, audit info, script version
396
+ results Json @default("{}") // output, logs, metrics, errors
397
+
398
+ // Hierarchy support
399
+ childProcesses String[] @db.ObjectId
400
+ parentProcessId String? @db.ObjectId
401
+
402
+ // Timestamps
403
+ createdAt DateTime @default(now())
404
+ updatedAt DateTime @updatedAt
405
+
406
+ @@index([name, createdAt(sort: Desc)])
407
+ @@index([state])
408
+ @@index([type])
409
+ @@map("AdminProcess")
410
+ }
411
+
412
+ /// Script scheduling configuration for hybrid scheduling (SQS + EventBridge)
413
+ model ScriptSchedule {
414
+ id String @id @default(auto()) @map("_id") @db.ObjectId
415
+ scriptName String @unique
416
+ enabled Boolean @default(false)
417
+ cronExpression String?
418
+ timezone String @default("UTC")
419
+ lastTriggeredAt DateTime?
420
+ nextTriggerAt DateTime?
421
+
422
+ // External Scheduler (e.g., AWS EventBridge Scheduler)
423
+ externalScheduleId String?
424
+ externalScheduleName String?
425
+
426
+ createdAt DateTime @default(now())
427
+ updatedAt DateTime @updatedAt
428
+
429
+ @@index([enabled])
430
+ @@map("ScriptSchedule")
431
+ }
432
+
433
+ /// One-time scheduled jobs for poll-and-dispatch scheduling pattern.
434
+ /// Used by providers without native one-time scheduling APIs (e.g., Netlify).
435
+ /// A cron function queries for due jobs and dispatches them to a queue.
436
+ model ScheduledJob {
437
+ id String @id @default(auto()) @map("_id") @db.ObjectId
438
+ scheduleName String @unique
439
+ scheduledAt DateTime
440
+ queueResourceId String
441
+ payload Json?
442
+ state String @default("PENDING")
443
+
444
+ createdAt DateTime @default(now())
445
+ updatedAt DateTime @updatedAt
446
+
447
+ @@index([state, scheduledAt])
448
+ @@map("ScheduledJob")
449
+ }
@@ -0,0 +1,55 @@
1
+ -- CreateEnum
2
+ CREATE TYPE "AdminProcessState" AS ENUM ('PENDING', 'RUNNING', 'COMPLETED', 'FAILED');
3
+
4
+ -- CreateEnum
5
+ CREATE TYPE "AdminTrigger" AS ENUM ('MANUAL', 'SCHEDULED', 'QUEUE', 'WEBHOOK');
6
+
7
+ -- CreateTable
8
+ CREATE TABLE "AdminProcess" (
9
+ "id" SERIAL NOT NULL,
10
+ "name" TEXT NOT NULL,
11
+ "type" TEXT NOT NULL,
12
+ "state" "AdminProcessState" NOT NULL DEFAULT 'PENDING',
13
+ "context" JSONB NOT NULL DEFAULT '{}',
14
+ "results" JSONB NOT NULL DEFAULT '{}',
15
+ "parentProcessId" INTEGER,
16
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
17
+ "updatedAt" TIMESTAMP(3) NOT NULL,
18
+
19
+ CONSTRAINT "AdminProcess_pkey" PRIMARY KEY ("id")
20
+ );
21
+
22
+ -- CreateTable
23
+ CREATE TABLE "ScriptSchedule" (
24
+ "id" SERIAL NOT NULL,
25
+ "scriptName" TEXT NOT NULL,
26
+ "enabled" BOOLEAN NOT NULL DEFAULT false,
27
+ "cronExpression" TEXT,
28
+ "timezone" TEXT NOT NULL DEFAULT 'UTC',
29
+ "lastTriggeredAt" TIMESTAMP(3),
30
+ "nextTriggerAt" TIMESTAMP(3),
31
+ "externalScheduleId" TEXT,
32
+ "externalScheduleName" TEXT,
33
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
34
+ "updatedAt" TIMESTAMP(3) NOT NULL,
35
+
36
+ CONSTRAINT "ScriptSchedule_pkey" PRIMARY KEY ("id")
37
+ );
38
+
39
+ -- CreateIndex
40
+ CREATE INDEX "AdminProcess_name_createdAt_idx" ON "AdminProcess"("name", "createdAt" DESC);
41
+
42
+ -- CreateIndex
43
+ CREATE INDEX "AdminProcess_state_idx" ON "AdminProcess"("state");
44
+
45
+ -- CreateIndex
46
+ CREATE INDEX "AdminProcess_type_idx" ON "AdminProcess"("type");
47
+
48
+ -- CreateIndex
49
+ CREATE UNIQUE INDEX "ScriptSchedule_scriptName_key" ON "ScriptSchedule"("scriptName");
50
+
51
+ -- CreateIndex
52
+ CREATE INDEX "ScriptSchedule_enabled_idx" ON "ScriptSchedule"("enabled");
53
+
54
+ -- AddForeignKey
55
+ ALTER TABLE "AdminProcess" ADD CONSTRAINT "AdminProcess_parentProcessId_fkey" FOREIGN KEY ("parentProcessId") REFERENCES "AdminProcess"("id") ON DELETE SET NULL ON UPDATE CASCADE;
@@ -343,3 +343,88 @@ model WebsocketConnection {
343
343
 
344
344
  @@index([connectionId])
345
345
  }
346
+
347
+ // ============================================================================
348
+ // ADMIN PROCESS MODELS
349
+ // ============================================================================
350
+
351
+ /// Admin process state machine
352
+ enum AdminProcessState {
353
+ PENDING
354
+ RUNNING
355
+ COMPLETED
356
+ FAILED
357
+ }
358
+
359
+ /// Admin trigger types
360
+ enum AdminTrigger {
361
+ MANUAL
362
+ SCHEDULED
363
+ QUEUE
364
+ WEBHOOK
365
+ }
366
+
367
+ /// Admin process tracking (like Process but without user/integration FK)
368
+ /// Used for: admin scripts, db migrations, system maintenance tasks
369
+ model AdminProcess {
370
+ id Int @id @default(autoincrement())
371
+ name String // e.g., "oauth-token-refresh", "db-migration-xyz"
372
+ type String // e.g., "ADMIN_SCRIPT", "DB_MIGRATION"
373
+
374
+ // State machine
375
+ state AdminProcessState @default(PENDING)
376
+
377
+ // Flexible storage (mirrors Process model pattern)
378
+ context Json @default("{}") // input, trigger, audit info, script version
379
+ results Json @default("{}") // output, logs, metrics, errors
380
+
381
+ // Hierarchy support - self-referential relation
382
+ parentProcessId Int?
383
+ parentProcess AdminProcess? @relation("AdminProcessHierarchy", fields: [parentProcessId], references: [id], onDelete: SetNull)
384
+ childProcesses AdminProcess[] @relation("AdminProcessHierarchy")
385
+
386
+ // Timestamps
387
+ createdAt DateTime @default(now())
388
+ updatedAt DateTime @updatedAt
389
+
390
+ @@index([name, createdAt(sort: Desc)])
391
+ @@index([state])
392
+ @@index([type])
393
+ }
394
+
395
+ /// Script scheduling configuration for hybrid scheduling (SQS + EventBridge)
396
+ model ScriptSchedule {
397
+ id Int @id @default(autoincrement())
398
+ scriptName String @unique
399
+ enabled Boolean @default(false)
400
+ cronExpression String?
401
+ timezone String @default("UTC")
402
+ lastTriggeredAt DateTime?
403
+ nextTriggerAt DateTime?
404
+
405
+ // External Scheduler (e.g., AWS EventBridge Scheduler)
406
+ externalScheduleId String?
407
+ externalScheduleName String?
408
+
409
+ createdAt DateTime @default(now())
410
+ updatedAt DateTime @updatedAt
411
+
412
+ @@index([enabled])
413
+ }
414
+
415
+ /// One-time scheduled jobs for poll-and-dispatch scheduling pattern.
416
+ /// Used by providers without native one-time scheduling APIs (e.g., Netlify).
417
+ /// A cron function queries for due jobs and dispatches them to a queue.
418
+ model ScheduledJob {
419
+ id Int @id @default(autoincrement())
420
+ scheduleName String @unique
421
+ scheduledAt DateTime
422
+ queueResourceId String
423
+ payload Json?
424
+ state String @default("PENDING")
425
+
426
+ createdAt DateTime @default(now())
427
+ updatedAt DateTime @updatedAt
428
+
429
+ @@index([state, scheduledAt])
430
+ }
@@ -0,0 +1,13 @@
1
+ const {
2
+ resolveProvider,
3
+ determineProviderName,
4
+ providerPackageName,
5
+ KNOWN_PROVIDERS,
6
+ } = require('./resolve-provider');
7
+
8
+ module.exports = {
9
+ resolveProvider,
10
+ determineProviderName,
11
+ providerPackageName,
12
+ KNOWN_PROVIDERS,
13
+ };
@@ -0,0 +1,93 @@
1
+ /**
2
+ * Provider Resolver
3
+ *
4
+ * Resolves a provider name from the appDefinition into the corresponding
5
+ * @friggframework/provider-{name} package. This is the bridge between
6
+ * `appDefinition.provider` and the provider plugin interface.
7
+ *
8
+ * Resolution order:
9
+ * 1. Explicit `providerName` argument
10
+ * 2. `appDefinition.provider`
11
+ * 3. `FRIGG_PROVIDER` environment variable
12
+ * 4. Default: 'aws'
13
+ *
14
+ * Package naming convention:
15
+ * provider: 'netlify' → require('@friggframework/provider-netlify')
16
+ * provider: 'aws' → require('@friggframework/provider-aws')
17
+ *
18
+ * @example
19
+ * const provider = resolveProvider({ provider: 'netlify' });
20
+ * // provider.name === 'netlify'
21
+ * // provider.deploy(appDefinition, options)
22
+ * // provider.createHandler(options)
23
+ * // provider.QueueProvider (class)
24
+ * // etc.
25
+ */
26
+
27
+ const KNOWN_PROVIDERS = ['aws', 'netlify'];
28
+
29
+ /**
30
+ * Determine the provider name from available sources.
31
+ *
32
+ * @param {Object} [appDefinition] - Frigg app definition
33
+ * @param {string} [providerName] - Explicit override
34
+ * @returns {string} Provider name (e.g. 'netlify', 'aws')
35
+ */
36
+ function determineProviderName(appDefinition, providerName) {
37
+ if (providerName) {
38
+ return providerName;
39
+ }
40
+ if (appDefinition?.provider) {
41
+ return appDefinition.provider;
42
+ }
43
+ if (process.env.FRIGG_PROVIDER) {
44
+ return process.env.FRIGG_PROVIDER;
45
+ }
46
+ return 'aws';
47
+ }
48
+
49
+ /**
50
+ * Resolve a provider name to a package name.
51
+ *
52
+ * @param {string} name - Provider name (e.g. 'netlify')
53
+ * @returns {string} npm package name (e.g. '@friggframework/provider-netlify')
54
+ */
55
+ function providerPackageName(name) {
56
+ return `@friggframework/provider-${name}`;
57
+ }
58
+
59
+ /**
60
+ * Resolve and load the provider plugin for the given app definition.
61
+ *
62
+ * @param {Object} [appDefinition] - Frigg app definition (reads .provider)
63
+ * @param {Object} [options]
64
+ * @param {string} [options.provider] - Explicit provider name override
65
+ * @returns {Object} Provider plugin (conforms to the provider plugin interface)
66
+ * @throws {Error} If the provider package is not installed
67
+ */
68
+ function resolveProvider(appDefinition, options = {}) {
69
+ const name = determineProviderName(appDefinition, options.provider);
70
+ const packageName = providerPackageName(name);
71
+
72
+ try {
73
+ return require(packageName);
74
+ } catch (error) {
75
+ if (error.code === 'MODULE_NOT_FOUND') {
76
+ const hint = KNOWN_PROVIDERS.includes(name)
77
+ ? `Install it with: npm install ${packageName}`
78
+ : `Is '${name}' a valid Frigg provider? Known providers: ${KNOWN_PROVIDERS.join(', ')}`;
79
+
80
+ throw new Error(
81
+ `Provider '${name}' specified in appDefinition but package '${packageName}' is not installed.\n ${hint}`
82
+ );
83
+ }
84
+ throw error;
85
+ }
86
+ }
87
+
88
+ module.exports = {
89
+ resolveProvider,
90
+ determineProviderName,
91
+ providerPackageName,
92
+ KNOWN_PROVIDERS,
93
+ };
package/queues/index.js CHANGED
@@ -1,4 +1,23 @@
1
1
  const { QueuerUtil } = require('./queuer-util');
2
+ const { QueueProvider } = require('./queue-provider');
3
+ const { QueueClientInterface } = require('./queue-client-interface');
4
+ const {
5
+ createQueueProvider,
6
+ determineProvider,
7
+ QUEUE_PROVIDERS,
8
+ } = require('./queue-provider-factory');
9
+ const {
10
+ NetlifyBackgroundProvider,
11
+ QStashQueueProvider,
12
+ } = require('./providers');
13
+
2
14
  module.exports = {
3
15
  QueuerUtil,
16
+ QueueProvider,
17
+ QueueClientInterface,
18
+ createQueueProvider,
19
+ determineProvider,
20
+ QUEUE_PROVIDERS,
21
+ NetlifyBackgroundProvider,
22
+ QStashQueueProvider,
4
23
  };
@@ -0,0 +1,7 @@
1
+ const { NetlifyBackgroundProvider } = require('./netlify-background-provider');
2
+ const { QStashQueueProvider } = require('./qstash-queue-provider');
3
+
4
+ module.exports = {
5
+ NetlifyBackgroundProvider,
6
+ QStashQueueProvider,
7
+ };
@@ -0,0 +1,76 @@
1
+ /**
2
+ * Netlify Background Functions Queue Provider (Adapter)
3
+ *
4
+ * Uses Netlify Background Functions for async job processing.
5
+ * Background functions have a 15-minute execution limit and are triggered via HTTP POST.
6
+ * They return 202 immediately while processing continues.
7
+ *
8
+ * Queue ID format: The function name or URL path for the background function.
9
+ * Convention: function files named with '-background' suffix (e.g., 'worker-background.js')
10
+ *
11
+ * Limitations:
12
+ * - No built-in retry (must implement manually)
13
+ * - No dead-letter queue
14
+ * - No FIFO ordering guarantees
15
+ * - 15-minute max execution time
16
+ */
17
+ const { QueueProvider } = require('../queue-provider');
18
+
19
+ class NetlifyBackgroundProvider extends QueueProvider {
20
+ constructor(options = {}) {
21
+ super();
22
+ // Base URL for triggering background functions (e.g., https://mysite.netlify.app)
23
+ this.baseUrl = options.baseUrl || process.env.URL || '';
24
+ }
25
+
26
+ /**
27
+ * Send a message by invoking a Netlify Background Function via HTTP POST.
28
+ *
29
+ * @param {Object} message - Message payload
30
+ * @param {string} queueId - Background function path (e.g., '/.netlify/functions/worker-background')
31
+ */
32
+ async send(message, queueId) {
33
+ const url = `${this.baseUrl}${queueId}`;
34
+ const response = await fetch(url, {
35
+ method: 'POST',
36
+ headers: { 'Content-Type': 'application/json' },
37
+ body: JSON.stringify(message),
38
+ });
39
+
40
+ if (!response.ok && response.status !== 202) {
41
+ throw new Error(
42
+ `Failed to invoke background function at ${queueId}: ${response.status} ${response.statusText}`
43
+ );
44
+ }
45
+
46
+ return { status: response.status, queueId };
47
+ }
48
+
49
+ /**
50
+ * Send batch of messages sequentially (background functions are HTTP-triggered).
51
+ */
52
+ async batchSend(entries = [], queueId) {
53
+ const results = [];
54
+ for (const entry of entries) {
55
+ const result = await this.send(entry, queueId);
56
+ results.push(result);
57
+ }
58
+ return { results };
59
+ }
60
+
61
+ /**
62
+ * Parse Netlify Background Function event.
63
+ * Background functions receive the raw HTTP event body as the event.
64
+ */
65
+ parseEvent(event) {
66
+ const body =
67
+ typeof event.body === 'string'
68
+ ? JSON.parse(event.body)
69
+ : event.body;
70
+
71
+ // Background functions receive a single message per invocation
72
+ return [body];
73
+ }
74
+ }
75
+
76
+ module.exports = { NetlifyBackgroundProvider };
@@ -0,0 +1,122 @@
1
+ /**
2
+ * QStash Queue Provider (Adapter)
3
+ *
4
+ * Upstash QStash implementation - platform-agnostic HTTP-based message queue.
5
+ * Works on any platform (Netlify, Vercel, AWS, bare metal, etc.)
6
+ *
7
+ * Features:
8
+ * - Configurable retry with exponential backoff
9
+ * - Dead-letter queue via callbacks
10
+ * - At-least-once delivery
11
+ * - Scheduling support (delay, cron)
12
+ * - Message deduplication
13
+ *
14
+ * Requires:
15
+ * - QSTASH_TOKEN env var (from Upstash console)
16
+ * - QSTASH_CURRENT_SIGNING_KEY and QSTASH_NEXT_SIGNING_KEY for verification
17
+ *
18
+ * Queue ID format: The destination URL that QStash will POST to when delivering the message.
19
+ */
20
+ const { QueueProvider } = require('../queue-provider');
21
+
22
+ const QSTASH_API_BASE = 'https://qstash.upstash.io/v2';
23
+
24
+ class QStashQueueProvider extends QueueProvider {
25
+ constructor(options = {}) {
26
+ super();
27
+ this.token =
28
+ options.token || process.env.QSTASH_TOKEN;
29
+ this.retries =
30
+ options.retries !== undefined ? options.retries : 3;
31
+ }
32
+
33
+ /**
34
+ * Send a message via QStash.
35
+ *
36
+ * @param {Object} message - Message payload
37
+ * @param {string} queueId - Destination URL that QStash will POST to
38
+ */
39
+ async send(message, queueId) {
40
+ if (!this.token) {
41
+ throw new Error(
42
+ 'QSTASH_TOKEN is required. Get one at https://console.upstash.com'
43
+ );
44
+ }
45
+
46
+ const headers = {
47
+ Authorization: `Bearer ${this.token}`,
48
+ 'Content-Type': 'application/json',
49
+ 'Upstash-Retries': String(this.retries),
50
+ };
51
+
52
+ const response = await fetch(
53
+ `${QSTASH_API_BASE}/publish/${queueId}`,
54
+ {
55
+ method: 'POST',
56
+ headers,
57
+ body: JSON.stringify(message),
58
+ }
59
+ );
60
+
61
+ if (!response.ok) {
62
+ const errorBody = await response.text().catch(() => 'unknown');
63
+ throw new Error(
64
+ `QStash publish failed: ${response.status} ${response.statusText} - ${errorBody}`
65
+ );
66
+ }
67
+
68
+ return response.json();
69
+ }
70
+
71
+ /**
72
+ * Send batch of messages via QStash batch API.
73
+ */
74
+ async batchSend(entries = [], queueId) {
75
+ if (!this.token) {
76
+ throw new Error(
77
+ 'QSTASH_TOKEN is required. Get one at https://console.upstash.com'
78
+ );
79
+ }
80
+
81
+ const messages = entries.map((entry) => ({
82
+ destination: queueId,
83
+ body: JSON.stringify(entry),
84
+ headers: { 'Content-Type': 'application/json' },
85
+ retries: this.retries,
86
+ }));
87
+
88
+ const response = await fetch(`${QSTASH_API_BASE}/batch`, {
89
+ method: 'POST',
90
+ headers: {
91
+ Authorization: `Bearer ${this.token}`,
92
+ 'Content-Type': 'application/json',
93
+ },
94
+ body: JSON.stringify(messages),
95
+ });
96
+
97
+ if (!response.ok) {
98
+ const errorBody = await response.text().catch(() => 'unknown');
99
+ throw new Error(
100
+ `QStash batch publish failed: ${response.status} - ${errorBody}`
101
+ );
102
+ }
103
+
104
+ return response.json();
105
+ }
106
+
107
+ /**
108
+ * Parse QStash webhook delivery event.
109
+ * QStash POSTs the message body directly to the destination URL.
110
+ */
111
+ parseEvent(event) {
112
+ const body =
113
+ typeof event.body === 'string'
114
+ ? JSON.parse(event.body)
115
+ : event.body;
116
+
117
+ // QStash delivers a single message per invocation
118
+ return [body];
119
+ }
120
+ }
121
+
122
+ module.exports = { QStashQueueProvider };
@@ -0,0 +1,55 @@
1
+ /**
2
+ * Queue Client Interface (Port)
3
+ *
4
+ * Defines the contract for low-level queue message operations.
5
+ * Used by Worker class and QueuerUtil for sending/receiving queue messages.
6
+ *
7
+ * Following Frigg's hexagonal architecture pattern:
8
+ * - Port defines WHAT the service does (contract)
9
+ * - Adapters implement HOW (AWS SQS, Netlify, QStash, etc.)
10
+ *
11
+ * Distinct from QueueProvider which is the higher-level provider for
12
+ * integration-level queue operations (send/batchSend/parseEvent).
13
+ * QueueClientInterface is the lower-level "how do I talk to the queue service" port.
14
+ */
15
+ class QueueClientInterface {
16
+ /**
17
+ * Send a single message to a queue
18
+ *
19
+ * @param {Object} params
20
+ * @param {string} params.QueueUrl - Queue endpoint/identifier
21
+ * @param {string} params.MessageBody - Serialized message body
22
+ * @param {number} [params.DelaySeconds] - Delay before message becomes visible
23
+ * @returns {Promise<{MessageId: string}>} Result with message identifier
24
+ */
25
+ async sendMessage(params) {
26
+ throw new Error('Method sendMessage must be implemented by subclass');
27
+ }
28
+
29
+ /**
30
+ * Send a batch of messages to a queue
31
+ *
32
+ * @param {Object} params
33
+ * @param {string} params.QueueUrl - Queue endpoint/identifier
34
+ * @param {Array<{Id: string, MessageBody: string}>} params.Entries - Messages to send
35
+ * @returns {Promise<Object>} Batch send result
36
+ */
37
+ async sendMessageBatch(params) {
38
+ throw new Error(
39
+ 'Method sendMessageBatch must be implemented by subclass'
40
+ );
41
+ }
42
+
43
+ /**
44
+ * Resolve a queue name to a URL/endpoint
45
+ *
46
+ * @param {Object} params
47
+ * @param {string} params.QueueName - Queue name to resolve
48
+ * @returns {Promise<string>} Resolved queue URL/endpoint
49
+ */
50
+ async getQueueUrl(params) {
51
+ throw new Error('Method getQueueUrl must be implemented by subclass');
52
+ }
53
+ }
54
+
55
+ module.exports = { QueueClientInterface };