@ossy/platform 1.31.1 → 1.31.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +95 -0
- package/package.json +8 -5
- package/src/email.integration.js +26 -0
- package/src/index.js +1 -0
- package/src/integration.service.js +60 -0
- package/src/server.js +35 -11
- package/src/tasks/task-service.js +3 -2
package/README.md
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
# @ossy/platform
|
|
2
|
+
|
|
3
|
+
Ossy application server runtime — Express-based server that loads the build manifest produced by `@ossy/app` and serves pages, APIs, tasks, and integrations.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## `*.integration.js` — third-party integration primitive
|
|
8
|
+
|
|
9
|
+
An integration file declares a named third-party client (e.g. a database, email provider, or payment SDK) that the platform connects to once at startup and makes available to every task.
|
|
10
|
+
|
|
11
|
+
### File naming
|
|
12
|
+
|
|
13
|
+
Name the file `<slug>.integration.js` (or `.mjs` / `.cjs`) and place it anywhere inside `src/`. The build pipeline discovers it automatically using the `*.integration.js` pattern, just like `*.task.js` and `*.page.jsx`.
|
|
14
|
+
|
|
15
|
+
### Required exports
|
|
16
|
+
|
|
17
|
+
| Export | Type | Description |
|
|
18
|
+
|---|---|---|
|
|
19
|
+
| `id` | `string` | Unique slug used to retrieve the client — e.g. `'email'`, `'stripe'`. |
|
|
20
|
+
| `credentials` | `string[]` | Names of environment variables that must be present. If any are missing the integration is skipped at startup with a warning. |
|
|
21
|
+
| `connect` | `async ({ env }) => client` | Called once at startup with the full `process.env`. Returns the client object stored by `id`. If it throws, the integration is skipped (non-fatal). |
|
|
22
|
+
|
|
23
|
+
### Minimal example
|
|
24
|
+
|
|
25
|
+
```js
|
|
26
|
+
// src/stripe.integration.js
|
|
27
|
+
import Stripe from 'stripe'
|
|
28
|
+
|
|
29
|
+
export const id = 'stripe'
|
|
30
|
+
|
|
31
|
+
export const credentials = ['STRIPE_SECRET_KEY']
|
|
32
|
+
|
|
33
|
+
export async function connect ({ env }) {
|
|
34
|
+
return new Stripe(env.STRIPE_SECRET_KEY)
|
|
35
|
+
}
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
### Consuming integrations inside a task
|
|
39
|
+
|
|
40
|
+
The platform injects `integrations` into every task's `run()` call alongside `sdk`:
|
|
41
|
+
|
|
42
|
+
```js
|
|
43
|
+
// src/send-invoice.task.js
|
|
44
|
+
export const metadata = {
|
|
45
|
+
id: 'send-invoice',
|
|
46
|
+
triggers: [{ aggregateType: 'Invoice', event: 'InvoiceCreated' }],
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export async function run ({ event, sdk, integrations }) {
|
|
50
|
+
const stripe = integrations.get('stripe') // null if credentials are missing
|
|
51
|
+
if (!stripe) return
|
|
52
|
+
|
|
53
|
+
await stripe.invoices.send(event.payload.stripeInvoiceId)
|
|
54
|
+
}
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
`integrations.get(id)` returns `null` when the integration was not loaded (missing credentials, failed `connect()`, or not declared anywhere).
|
|
58
|
+
|
|
59
|
+
### Manifest output
|
|
60
|
+
|
|
61
|
+
After `app build` the manifest contains an `integrations` array:
|
|
62
|
+
|
|
63
|
+
```json
|
|
64
|
+
{
|
|
65
|
+
"integrations": [
|
|
66
|
+
{
|
|
67
|
+
"id": "stripe",
|
|
68
|
+
"entry": "/static/stripe.integration-7f2a.js",
|
|
69
|
+
"credentials": ["STRIPE_SECRET_KEY"]
|
|
70
|
+
}
|
|
71
|
+
]
|
|
72
|
+
}
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
### Built-in integrations
|
|
76
|
+
|
|
77
|
+
`@ossy/platform` ships with the following integration that is auto-discovered for every app using the package:
|
|
78
|
+
|
|
79
|
+
| id | Credentials | Client |
|
|
80
|
+
|---|---|---|
|
|
81
|
+
| `email` | `SES_REGION`, `SES_ACCESS_KEY_ID`, `SES_SECRET_ACCESS_KEY` | `SESClient` from `@aws-sdk/client-ses` |
|
|
82
|
+
|
|
83
|
+
If the SES env vars are not set the `email` integration is silently skipped and `integrations.get('email')` returns `null`.
|
|
84
|
+
|
|
85
|
+
### `IntegrationService` API
|
|
86
|
+
|
|
87
|
+
```js
|
|
88
|
+
import { IntegrationService } from '@ossy/platform/integrations'
|
|
89
|
+
|
|
90
|
+
// Load a list of already-imported integration modules (done automatically by the server).
|
|
91
|
+
await IntegrationService.load(modules, process.env)
|
|
92
|
+
|
|
93
|
+
// Retrieve a connected client by id.
|
|
94
|
+
const client = IntegrationService.get('email') // SESClient | null
|
|
95
|
+
```
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ossy/platform",
|
|
3
|
-
"version": "1.31.
|
|
3
|
+
"version": "1.31.3",
|
|
4
4
|
"description": "Ossy application server runtime",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -19,7 +19,8 @@
|
|
|
19
19
|
"./site-loader": "./src/site-loader.js",
|
|
20
20
|
"./tasks": "./src/index.js",
|
|
21
21
|
"./resources": "./src/resources/index.js",
|
|
22
|
-
"./definition": "./src/Definition.js"
|
|
22
|
+
"./definition": "./src/Definition.js",
|
|
23
|
+
"./integrations": "./src/integration.service.js"
|
|
23
24
|
},
|
|
24
25
|
"scripts": {
|
|
25
26
|
"start": "PORT=3003 node -e \"import('./src/server.js').then(m => m.startServer())\""
|
|
@@ -28,8 +29,10 @@
|
|
|
28
29
|
"author": "Ossy <yourfriends@ossy.se> (https://ossy.se)",
|
|
29
30
|
"license": "MIT",
|
|
30
31
|
"dependencies": {
|
|
31
|
-
"@
|
|
32
|
-
"@ossy/
|
|
32
|
+
"@aws-sdk/client-ses": "^3.0.0",
|
|
33
|
+
"@ossy/event-store": "^1.0.1",
|
|
34
|
+
"@ossy/router": "^1.32.3",
|
|
35
|
+
"@ossy/sdk": "^1.32.3",
|
|
33
36
|
"cookie-parser": "^1.4.7",
|
|
34
37
|
"dotenv": ">=16.0.0 <18.0.0",
|
|
35
38
|
"express": ">=5.0.0 <6.0.0",
|
|
@@ -40,5 +43,5 @@
|
|
|
40
43
|
"src",
|
|
41
44
|
"Dockerfile"
|
|
42
45
|
],
|
|
43
|
-
"gitHead": "
|
|
46
|
+
"gitHead": "b8cbdc545a659e96af8948aa08a89316a2c2d193"
|
|
44
47
|
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { SESClient } from '@aws-sdk/client-ses'
|
|
2
|
+
|
|
3
|
+
export const id = 'email'
|
|
4
|
+
|
|
5
|
+
export const credentials = [
|
|
6
|
+
'SES_REGION',
|
|
7
|
+
'SES_ACCESS_KEY_ID',
|
|
8
|
+
'SES_SECRET_ACCESS_KEY',
|
|
9
|
+
]
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Returns an AWS SES client configured from env vars.
|
|
13
|
+
* Tasks receive this via `integrations.get('email')`.
|
|
14
|
+
*
|
|
15
|
+
* @param {{ env: NodeJS.ProcessEnv }} opts
|
|
16
|
+
* @returns {SESClient}
|
|
17
|
+
*/
|
|
18
|
+
export async function connect ({ env }) {
|
|
19
|
+
return new SESClient({
|
|
20
|
+
region: env.SES_REGION,
|
|
21
|
+
credentials: {
|
|
22
|
+
accessKeyId: env.SES_ACCESS_KEY_ID,
|
|
23
|
+
secretAccessKey: env.SES_SECRET_ACCESS_KEY,
|
|
24
|
+
},
|
|
25
|
+
})
|
|
26
|
+
}
|
package/src/index.js
CHANGED
|
@@ -4,3 +4,4 @@ export { ChangeStream } from './tasks/change-stream.js'
|
|
|
4
4
|
export { registerResourceTemplate, getSystemResourceTemplates } from './resources/resource-template.registry.js'
|
|
5
5
|
export { normalizeAndValidateDocumentContent, validateResourceTemplatesForImport, ALLOWED_FIELD_TYPES } from './resources/resource-template.validation.js'
|
|
6
6
|
export { Definition } from './Definition.js'
|
|
7
|
+
export { IntegrationService } from './integration.service.js'
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/** @type {Record<string, unknown>} */
|
|
2
|
+
const _clients = {}
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Manages third-party integration clients declared via `*.integration.js` files.
|
|
6
|
+
*
|
|
7
|
+
* Integrations are loaded once at server startup. If a required env var is
|
|
8
|
+
* missing, or if `connect()` throws, the integration is skipped with a warning
|
|
9
|
+
* so the server can still boot without every optional credential being present.
|
|
10
|
+
*/
|
|
11
|
+
export const IntegrationService = {
|
|
12
|
+
/**
|
|
13
|
+
* Connect all integration modules, storing each resulting client by id.
|
|
14
|
+
* Pass the array of already-imported integration modules (each exporting
|
|
15
|
+
* `id`, `credentials`, and `connect`).
|
|
16
|
+
*
|
|
17
|
+
* @param {Array<{ id: string, credentials: string[], connect: (opts: { env: NodeJS.ProcessEnv }) => Promise<unknown> | unknown }>} modules
|
|
18
|
+
* @param {NodeJS.ProcessEnv} env Usually `process.env`.
|
|
19
|
+
*/
|
|
20
|
+
async load (modules, env) {
|
|
21
|
+
for (const mod of modules) {
|
|
22
|
+
const { id, credentials = [], connect } = mod
|
|
23
|
+
|
|
24
|
+
if (typeof id !== 'string' || id.trim() === '') {
|
|
25
|
+
console.warn('[IntegrationService] Skipping integration with missing or invalid "id"')
|
|
26
|
+
continue
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const missing = credentials.filter((key) => !env[key])
|
|
30
|
+
if (missing.length > 0) {
|
|
31
|
+
console.warn(
|
|
32
|
+
`[IntegrationService] Skipping integration "${id}" — missing env var(s): ${missing.join(', ')}`,
|
|
33
|
+
)
|
|
34
|
+
continue
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
try {
|
|
38
|
+
_clients[id] = await connect({ env })
|
|
39
|
+
console.log(`[IntegrationService] Connected integration "${id}"`)
|
|
40
|
+
} catch (err) {
|
|
41
|
+
console.warn(
|
|
42
|
+
`[IntegrationService] Integration "${id}" connect() failed — skipping:`,
|
|
43
|
+
err && err.message ? err.message : err,
|
|
44
|
+
)
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
},
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Retrieve a connected client by integration id.
|
|
51
|
+
* Returns `null` when the integration was not loaded (missing credentials,
|
|
52
|
+
* connect failure, or not declared in the manifest).
|
|
53
|
+
*
|
|
54
|
+
* @param {string} id
|
|
55
|
+
* @returns {unknown | null}
|
|
56
|
+
*/
|
|
57
|
+
get (id) {
|
|
58
|
+
return _clients[id] ?? null
|
|
59
|
+
},
|
|
60
|
+
}
|
package/src/server.js
CHANGED
|
@@ -7,9 +7,11 @@ import { Router as OssyRouter } from '@ossy/router'
|
|
|
7
7
|
import cookieParser from 'cookie-parser'
|
|
8
8
|
import { ProxyInternal } from './proxy-internal.js'
|
|
9
9
|
import { SDK } from '@ossy/sdk'
|
|
10
|
+
import { AggregateRebuild } from '@ossy/event-store'
|
|
10
11
|
import { TaskService } from './tasks/task-service.js'
|
|
11
12
|
import { ChangeStream } from './tasks/change-stream.js'
|
|
12
13
|
import { registerResourceTemplate } from './resources/resource-template.registry.js'
|
|
14
|
+
import { IntegrationService } from './integration.service.js'
|
|
13
15
|
|
|
14
16
|
const DEFAULT_PORT = 3000
|
|
15
17
|
const MANIFEST_FILE = 'manifest.json'
|
|
@@ -49,6 +51,8 @@ export function loadManifest (buildDir) {
|
|
|
49
51
|
const components = Array.isArray(manifest.components) ? manifest.components : []
|
|
50
52
|
const resourceTemplates = Array.isArray(manifest.resourceTemplates) ? manifest.resourceTemplates : []
|
|
51
53
|
const aggregates = Array.isArray(manifest.aggregates) ? manifest.aggregates : []
|
|
54
|
+
const integrations = Array.isArray(manifest.integrations) ? manifest.integrations : []
|
|
55
|
+
const startups = Array.isArray(manifest.startups) ? manifest.startups : []
|
|
52
56
|
for (const e of entries) {
|
|
53
57
|
if ((e.type === 'page' || e.type === 'api') && !validRoutable(e)) {
|
|
54
58
|
console.warn(`[@ossy/platform][server] Skipping ${e.type} "${e.id}" — manifest entry has no path.`)
|
|
@@ -62,6 +66,8 @@ export function loadManifest (buildDir) {
|
|
|
62
66
|
components,
|
|
63
67
|
resourceTemplates,
|
|
64
68
|
aggregates,
|
|
69
|
+
integrations,
|
|
70
|
+
startups,
|
|
65
71
|
config: manifest.config || {},
|
|
66
72
|
}
|
|
67
73
|
}
|
|
@@ -108,19 +114,22 @@ export async function startServer (options = {}) {
|
|
|
108
114
|
registerResourceTemplate(template)
|
|
109
115
|
}
|
|
110
116
|
|
|
111
|
-
//
|
|
112
|
-
//
|
|
113
|
-
//
|
|
114
|
-
//
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
117
|
+
// Integration loading — import each bundled integration module, check its
|
|
118
|
+
// declared credentials, and call connect({ env }) to obtain a client.
|
|
119
|
+
// Missing credentials or a failing connect() are non-fatal: the integration
|
|
120
|
+
// is skipped with a warning so the server still starts.
|
|
121
|
+
const integrationModules = []
|
|
122
|
+
for (const intEntry of manifest.integrations ?? []) {
|
|
123
|
+
try {
|
|
124
|
+
integrationModules.push(await import(resolveEntryUrl(intEntry.entry, buildDir)))
|
|
125
|
+
} catch (err) {
|
|
126
|
+
console.warn(`[@ossy/platform][server] Failed to import integration "${intEntry.id}":`, err.message)
|
|
127
|
+
}
|
|
122
128
|
}
|
|
129
|
+
await IntegrationService.load(integrationModules, process.env)
|
|
123
130
|
|
|
131
|
+
// Aggregate registration — mirrors task and resource-template registration above.
|
|
132
|
+
// AggregateRebuild is provided by @ossy/event-store.
|
|
124
133
|
if (AggregateRebuild) {
|
|
125
134
|
for (const agg of manifest.aggregates ?? []) {
|
|
126
135
|
try {
|
|
@@ -130,6 +139,21 @@ export async function startServer (options = {}) {
|
|
|
130
139
|
console.error(`[@ossy/platform][server] Failed to load aggregate "${agg.id}":`, err.message)
|
|
131
140
|
}
|
|
132
141
|
}
|
|
142
|
+
|
|
143
|
+
AggregateRebuild.BuildAndSaveAll().catch((error) => {
|
|
144
|
+
console.error('[@ossy/platform][server] BuildAndSaveAll failed — is MongoDB reachable?', error)
|
|
145
|
+
})
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
for (const startup of manifest.startups ?? []) {
|
|
149
|
+
try {
|
|
150
|
+
const mod = await import(resolveEntryUrl(startup.entry, buildDir))
|
|
151
|
+
if (typeof mod.run === 'function') {
|
|
152
|
+
await mod.run({ env: process.env })
|
|
153
|
+
}
|
|
154
|
+
} catch (err) {
|
|
155
|
+
console.error(`[@ossy/platform][server] Failed to run startup "${startup.id}":`, err.message)
|
|
156
|
+
}
|
|
133
157
|
}
|
|
134
158
|
|
|
135
159
|
// Register the SDK so all tasks receive it as `sdk`.
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { matchesGlob } from './glob.js'
|
|
2
2
|
import { matchesCron } from './cron.js'
|
|
3
|
+
import { IntegrationService } from '../integration.service.js'
|
|
3
4
|
|
|
4
5
|
const SCHEDULER_INTERVAL_MS = 60_000
|
|
5
6
|
|
|
@@ -74,7 +75,7 @@ export class TaskService {
|
|
|
74
75
|
console.log(`[TaskService] Dispatching task "${metadata.id}"`)
|
|
75
76
|
|
|
76
77
|
Promise.resolve()
|
|
77
|
-
.then(() => handler({ event, sdk: effectiveSdk }))
|
|
78
|
+
.then(() => handler({ event, sdk: effectiveSdk, integrations: IntegrationService }))
|
|
78
79
|
.catch(error =>
|
|
79
80
|
console.error(`[TaskService] Task "${metadata.id}" failed`, error),
|
|
80
81
|
)
|
|
@@ -104,7 +105,7 @@ export class TaskService {
|
|
|
104
105
|
console.log(`[TaskService] Schedule fired for task "${metadata.id}"`)
|
|
105
106
|
|
|
106
107
|
Promise.resolve()
|
|
107
|
-
.then(() => handler({ event: { type: 'scheduled', taskId: metadata.id }, sdk: TaskService._sdk }))
|
|
108
|
+
.then(() => handler({ event: { type: 'scheduled', taskId: metadata.id }, sdk: TaskService._sdk, integrations: IntegrationService }))
|
|
108
109
|
.catch(error =>
|
|
109
110
|
console.error(`[TaskService] Scheduled task "${metadata.id}" failed`, error),
|
|
110
111
|
)
|