@unchainedshop/platform 5.0.0-alpha.1 → 5.0.0-alpha.11
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 +79 -106
- package/bin/unchained.js +164 -0
- package/lib/migrations/createMigrationRunner.js +3 -9
- package/lib/migrations/runMigrations.js +3 -8
- package/lib/setup/setupWorkqueue.js +15 -5
- package/lib/startPlatform.d.ts +4 -1
- package/lib/startPlatform.js +34 -13
- package/lib/templates/order-parser/getOrderPositionsData.js +1 -1
- package/lib/templates/order-parser/getOrderSummaryData.js +2 -2
- package/lib/templates/resolveEnrollmentStatusTemplate.js +10 -2
- package/lib/templates/resolveErrorReportTemplate.js +2 -2
- package/package.json +19 -13
package/README.md
CHANGED
|
@@ -3,147 +3,120 @@
|
|
|
3
3
|
|
|
4
4
|
# @unchainedshop/platform
|
|
5
5
|
|
|
6
|
-
Umbrella package for the Unchained Engine.
|
|
6
|
+
Umbrella package for the Unchained Engine. Initializes the database, core modules and services, plugins, GraphQL server, message templates, and work queue.
|
|
7
7
|
|
|
8
8
|
## Installation
|
|
9
9
|
|
|
10
10
|
```bash
|
|
11
|
-
npm install @unchainedshop/platform
|
|
11
|
+
npm install @unchainedshop/platform express
|
|
12
12
|
```
|
|
13
13
|
|
|
14
|
-
##
|
|
14
|
+
## Quick start
|
|
15
|
+
|
|
16
|
+
Register plugins, initialize the platform, and connect it to your HTTP server:
|
|
15
17
|
|
|
16
18
|
```typescript
|
|
17
|
-
import { startPlatform } from '@unchainedshop/platform';
|
|
18
19
|
import express from 'express';
|
|
20
|
+
import { startPlatform } from '@unchainedshop/platform';
|
|
21
|
+
import { connect } from '@unchainedshop/api/express';
|
|
22
|
+
import { registerBasePlugins } from '@unchainedshop/plugins/presets/base';
|
|
19
23
|
|
|
20
|
-
|
|
24
|
+
registerBasePlugins();
|
|
21
25
|
|
|
22
|
-
const
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
// Platform options
|
|
26
|
-
},
|
|
27
|
-
});
|
|
26
|
+
const app = express();
|
|
27
|
+
const platform = await startPlatform({});
|
|
28
|
+
await connect(app, platform);
|
|
28
29
|
|
|
29
|
-
|
|
30
|
+
const products = await platform.unchainedAPI.modules.products.findProducts({});
|
|
30
31
|
app.listen(4010);
|
|
31
32
|
```
|
|
32
33
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
### Platform Setup
|
|
36
|
-
|
|
37
|
-
| Export | Description |
|
|
38
|
-
|--------|-------------|
|
|
39
|
-
| `startPlatform` | Initialize complete Unchained platform |
|
|
40
|
-
| `runMigrations` | Run database migrations |
|
|
41
|
-
| `printRuntimeConfiguration` | Log registered templates, events, and adapters |
|
|
42
|
-
|
|
43
|
-
### Context Helpers
|
|
44
|
-
|
|
45
|
-
| Export | Description |
|
|
46
|
-
|--------|-------------|
|
|
47
|
-
| `setAccessToken` | Set access token for user session |
|
|
48
|
-
| `getAccessToken` | Get current access token |
|
|
49
|
-
| `invalidateAccessToken` | Invalidate/logout access token |
|
|
50
|
-
|
|
51
|
-
### Templates
|
|
52
|
-
|
|
53
|
-
| Export | Description |
|
|
54
|
-
|--------|-------------|
|
|
55
|
-
| `MessageTypes` | Available message/notification types |
|
|
56
|
-
| `setupTemplates` | Register message templates |
|
|
57
|
-
|
|
58
|
-
### Message Types
|
|
59
|
-
|
|
60
|
-
| Type | Description |
|
|
61
|
-
|------|-------------|
|
|
62
|
-
| `ACCOUNT_ACTION` | Account verification, password reset |
|
|
63
|
-
| `DELIVERY` | Delivery notifications |
|
|
64
|
-
| `ORDER_CONFIRMATION` | Order confirmation emails |
|
|
65
|
-
| `ORDER_REJECTION` | Order rejection notifications |
|
|
66
|
-
| `QUOTATION_STATUS` | Quotation status updates |
|
|
67
|
-
| `ENROLLMENT_STATUS` | Subscription status updates |
|
|
68
|
-
| `FORWARD_DELIVERY` | Forward delivery notifications |
|
|
69
|
-
|
|
70
|
-
## Quick Start
|
|
71
|
-
|
|
72
|
-
```typescript
|
|
73
|
-
import express from 'express';
|
|
74
|
-
import { startPlatform, MessageTypes } from '@unchainedshop/platform';
|
|
75
|
-
|
|
76
|
-
const app = express();
|
|
77
|
-
|
|
78
|
-
const { unchainedAPI, graphqlHandler } = await startPlatform({
|
|
79
|
-
express: app,
|
|
80
|
-
options: {
|
|
81
|
-
modules: {
|
|
82
|
-
// Module-specific configuration
|
|
83
|
-
},
|
|
84
|
-
plugins: [
|
|
85
|
-
// Plugin imports
|
|
86
|
-
],
|
|
87
|
-
},
|
|
88
|
-
workQueueConfig: {
|
|
89
|
-
// Worker queue configuration
|
|
90
|
-
},
|
|
91
|
-
});
|
|
34
|
+
For Fastify, use `connect` from `@unchainedshop/api/fastify` with a Fastify instance. `connect()` mounts GraphQL, MCP, and registered plugin routes. Pass `{ adminUI: true }` as its third argument to serve an installed `@unchainedshop/admin-ui` package.
|
|
92
35
|
|
|
93
|
-
|
|
94
|
-
const products = await unchainedAPI.modules.products.findProducts({});
|
|
95
|
-
|
|
96
|
-
// Start server
|
|
97
|
-
app.use('/graphql', graphqlHandler);
|
|
98
|
-
app.listen(4010);
|
|
99
|
-
```
|
|
36
|
+
Before startup, set `ROOT_URL`, `EMAIL_WEBSITE_NAME`, `EMAIL_WEBSITE_URL`, `EMAIL_FROM`, and `UNCHAINED_TOKEN_SECRET` (at least 32 characters). Set `MONGO_URL` for an external MongoDB; otherwise the database package starts a local instance.
|
|
100
37
|
|
|
101
38
|
## Configuration
|
|
102
39
|
|
|
103
40
|
```typescript
|
|
104
41
|
const platform = await startPlatform({
|
|
105
|
-
express: app,
|
|
106
42
|
options: {
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
// Order module options
|
|
110
|
-
},
|
|
111
|
-
products: {
|
|
112
|
-
// Product module options
|
|
113
|
-
},
|
|
43
|
+
orders: {
|
|
44
|
+
// Order module settings
|
|
114
45
|
},
|
|
115
|
-
|
|
116
|
-
//
|
|
117
|
-
},
|
|
118
|
-
bulkImporter: {
|
|
119
|
-
handlers: {
|
|
120
|
-
// Custom import handlers
|
|
121
|
-
},
|
|
46
|
+
products: {
|
|
47
|
+
// Product module settings
|
|
122
48
|
},
|
|
123
49
|
},
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
pollInterval: 1000,
|
|
50
|
+
modules: {
|
|
51
|
+
// Custom modules with a configure({ db, migrationRepository, options }) function
|
|
127
52
|
},
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
return {
|
|
131
|
-
...context,
|
|
132
|
-
// Custom context properties
|
|
133
|
-
};
|
|
53
|
+
services: {
|
|
54
|
+
// Custom services
|
|
134
55
|
},
|
|
56
|
+
bulkImporter: {
|
|
57
|
+
handlers: {
|
|
58
|
+
// Custom import handlers
|
|
59
|
+
},
|
|
60
|
+
},
|
|
61
|
+
workQueueOptions: {
|
|
62
|
+
skipInvalidationOnStartup: true,
|
|
63
|
+
},
|
|
64
|
+
context: (defaultResolver) => async (props, req, res) => ({
|
|
65
|
+
...(await defaultResolver(props, req, res)),
|
|
66
|
+
// Additional request context
|
|
67
|
+
}),
|
|
135
68
|
});
|
|
136
69
|
```
|
|
137
70
|
|
|
138
|
-
|
|
71
|
+
`PlatformOptions` combines core configuration with GraphQL server options, `rolesOptions`, `workQueueOptions`, and `auditLog`. Built-in modules are included automatically. Module settings are direct properties of `options`; module/service overrides and import/export handlers are top-level options.
|
|
72
|
+
|
|
73
|
+
The work queue runs migrations and starts queue managers during startup unless workers are disabled. See [work queue options](src/setup/setupWorkqueue.ts) and [platform initialization](src/startPlatform.ts) for the current options.
|
|
139
74
|
|
|
140
|
-
|
|
75
|
+
## Download documentation for coding assistants
|
|
76
|
+
|
|
77
|
+
```bash
|
|
78
|
+
npx --package @unchainedshop/platform unchained download-llm-docs
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
Requires Git. Downloads docs into `.unchained-docs/` for the installed platform
|
|
82
|
+
version's tag, falling back to `master` when the tag is unavailable. Updates a
|
|
83
|
+
managed file index in existing `CLAUDE.md` and `AGENTS.md` files (legacy `agents.md`
|
|
84
|
+
is also supported), or creates `CLAUDE.md` if none exists. Re-running replaces the
|
|
85
|
+
downloaded docs and index while preserving surrounding instructions. Add
|
|
86
|
+
`.unchained-docs/` to your project's `.gitignore`.
|
|
87
|
+
|
|
88
|
+
## Exports
|
|
89
|
+
|
|
90
|
+
| Export | Description |
|
|
91
|
+
|--------|-------------|
|
|
92
|
+
| `startPlatform` | Initialize the platform |
|
|
93
|
+
| `PlatformOptions` | Platform configuration type |
|
|
94
|
+
| `runMigrations` | Run registered database migrations |
|
|
95
|
+
| `printRuntimeConfiguration` | Log registered templates, events, and adapters |
|
|
96
|
+
| `MessageTypes` | Built-in message type constants |
|
|
97
|
+
|
|
98
|
+
The package also exports the [built-in template resolvers](src/templates/index.ts). Platform startup registers them automatically; custom templates use `MessagingDirector.registerTemplate()` from `@unchainedshop/core`.
|
|
99
|
+
|
|
100
|
+
| Message type | Purpose |
|
|
101
|
+
|--------------|---------|
|
|
102
|
+
| `ACCOUNT_ACTION` | Email verification and password reset |
|
|
103
|
+
| `DELIVERY` | Forward delivery notifications |
|
|
104
|
+
| `ORDER_CONFIRMATION` | Order confirmations |
|
|
105
|
+
| `ORDER_REJECTION` | Order rejections |
|
|
106
|
+
| `QUOTATION_STATUS` | Quotation status updates |
|
|
107
|
+
| `ENROLLMENT_STATUS` | Enrollment status updates |
|
|
108
|
+
| `ERROR_REPORT` | Error reports |
|
|
109
|
+
|
|
110
|
+
## Return value
|
|
141
111
|
|
|
142
112
|
| Property | Description |
|
|
143
113
|
|----------|-------------|
|
|
144
|
-
| `unchainedAPI` |
|
|
145
|
-
| `graphqlHandler` | GraphQL Yoga server instance
|
|
114
|
+
| `unchainedAPI` | Core modules, services, bulk importer/exporter, and options |
|
|
115
|
+
| `graphqlHandler` | GraphQL Yoga server instance |
|
|
146
116
|
| `db` | MongoDB database instance |
|
|
117
|
+
| `shutdown` | Stops the work queue, plugins, event emitter, GraphQL server, audit log and database without exiting the process; every call returns the same promise |
|
|
118
|
+
|
|
119
|
+
On `SIGTERM` / `SIGINT` the platform calls `shutdown()` and exits. Once `shutdown()` has started, these signals no longer exit the process, so exit it yourself after awaiting `shutdown()`.
|
|
147
120
|
|
|
148
121
|
## License
|
|
149
122
|
|
package/bin/unchained.js
ADDED
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { execFileSync } from 'node:child_process';
|
|
4
|
+
import {
|
|
5
|
+
cpSync,
|
|
6
|
+
existsSync,
|
|
7
|
+
mkdtempSync,
|
|
8
|
+
readFileSync,
|
|
9
|
+
readdirSync,
|
|
10
|
+
rmSync,
|
|
11
|
+
writeFileSync,
|
|
12
|
+
} from 'node:fs';
|
|
13
|
+
import { join, relative } from 'node:path';
|
|
14
|
+
import { tmpdir } from 'node:os';
|
|
15
|
+
import { createRequire } from 'node:module';
|
|
16
|
+
|
|
17
|
+
const GITHUB_REPO = 'https://github.com/unchainedshop/unchained.git';
|
|
18
|
+
const DOCS_SUBPATH = 'docs/docs';
|
|
19
|
+
const DOCS_DIR = join(process.cwd(), '.unchained-docs');
|
|
20
|
+
const INDEX_START = '<!-- UNCHAINED_DOCS_INDEX:BEGIN -->';
|
|
21
|
+
const INDEX_END = '<!-- UNCHAINED_DOCS_INDEX:END -->';
|
|
22
|
+
|
|
23
|
+
const command = process.argv[2];
|
|
24
|
+
|
|
25
|
+
if (command !== 'download-llm-docs') {
|
|
26
|
+
console.log(`Usage: unchained <command>
|
|
27
|
+
|
|
28
|
+
Commands:
|
|
29
|
+
download-llm-docs Download Unchained Engine docs for LLM coding assistants
|
|
30
|
+
(Claude Code, Cursor, etc.) and inject a file index into
|
|
31
|
+
CLAUDE.md and/or AGENTS.md`);
|
|
32
|
+
process.exit(command ? 1 : 0);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Resolve version from own package.json
|
|
36
|
+
const require = createRequire(import.meta.url);
|
|
37
|
+
const pkg = require('../package.json');
|
|
38
|
+
const version = pkg.version;
|
|
39
|
+
|
|
40
|
+
// Determine git ref: try tag first, fallback to master
|
|
41
|
+
function tagExists(tag) {
|
|
42
|
+
try {
|
|
43
|
+
execFileSync('git', ['ls-remote', '--tags', '--exit-code', GITHUB_REPO, `refs/tags/${tag}`], {
|
|
44
|
+
stdio: 'pipe',
|
|
45
|
+
});
|
|
46
|
+
return true;
|
|
47
|
+
} catch {
|
|
48
|
+
return false;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const tag = `v${version}`;
|
|
53
|
+
const ref = tagExists(tag) ? tag : 'master';
|
|
54
|
+
if (ref === 'master') {
|
|
55
|
+
console.log(`Tag ${tag} not found on remote, falling back to master branch.`);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Clone docs using sparse-checkout into a temp directory
|
|
59
|
+
const tempDir = mkdtempSync(join(tmpdir(), 'unchained-docs-'));
|
|
60
|
+
console.log(`Cloning docs from ${ref} using sparse-checkout ...`);
|
|
61
|
+
try {
|
|
62
|
+
execFileSync(
|
|
63
|
+
'git',
|
|
64
|
+
[
|
|
65
|
+
'clone',
|
|
66
|
+
'--filter=blob:none',
|
|
67
|
+
'--no-checkout',
|
|
68
|
+
'--depth',
|
|
69
|
+
'1',
|
|
70
|
+
'--branch',
|
|
71
|
+
ref,
|
|
72
|
+
GITHUB_REPO,
|
|
73
|
+
tempDir,
|
|
74
|
+
],
|
|
75
|
+
{ stdio: 'inherit' },
|
|
76
|
+
);
|
|
77
|
+
execFileSync('git', ['-C', tempDir, 'sparse-checkout', 'set', DOCS_SUBPATH], {
|
|
78
|
+
stdio: 'inherit',
|
|
79
|
+
});
|
|
80
|
+
execFileSync('git', ['-C', tempDir, 'checkout'], { stdio: 'inherit' });
|
|
81
|
+
} catch {
|
|
82
|
+
rmSync(tempDir, { recursive: true, force: true });
|
|
83
|
+
console.error('Failed to clone docs repository.');
|
|
84
|
+
process.exit(1);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Copy docs/docs/ contents to .unchained-docs/
|
|
88
|
+
const sourceDir = join(tempDir, DOCS_SUBPATH);
|
|
89
|
+
if (!existsSync(sourceDir)) {
|
|
90
|
+
rmSync(tempDir, { recursive: true, force: true });
|
|
91
|
+
console.error(`Docs directory not found at ${DOCS_SUBPATH} in the repository.`);
|
|
92
|
+
process.exit(1);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
rmSync(DOCS_DIR, { recursive: true, force: true });
|
|
96
|
+
cpSync(sourceDir, DOCS_DIR, { recursive: true });
|
|
97
|
+
rmSync(tempDir, { recursive: true, force: true });
|
|
98
|
+
|
|
99
|
+
// Scan for markdown files
|
|
100
|
+
const markdownsByDir = Map.groupBy(
|
|
101
|
+
readdirSync(DOCS_DIR, { withFileTypes: true, recursive: true }).filter(
|
|
102
|
+
(e) => e.isFile() && (e.name.endsWith('.md') || e.name.endsWith('.mdx')),
|
|
103
|
+
),
|
|
104
|
+
(e) => relative(DOCS_DIR, e.parentPath) || '.',
|
|
105
|
+
);
|
|
106
|
+
|
|
107
|
+
const fileCount = [...markdownsByDir.values()].flat().length;
|
|
108
|
+
console.log(`Found ${fileCount} markdown files in .unchained-docs/`);
|
|
109
|
+
|
|
110
|
+
// Build compact pipe-delimited index
|
|
111
|
+
const indexSnippet = [
|
|
112
|
+
INDEX_START,
|
|
113
|
+
[
|
|
114
|
+
`[Unchained Engine v${version} Docs Index]`,
|
|
115
|
+
'root: ./.unchained-docs',
|
|
116
|
+
'Search these docs before answering Unchained questions.',
|
|
117
|
+
'If docs missing, run: npx --package @unchainedshop/platform unchained download-llm-docs',
|
|
118
|
+
...[...markdownsByDir]
|
|
119
|
+
.sort(([a], [b]) => a.localeCompare(b))
|
|
120
|
+
.map(
|
|
121
|
+
([dir, entries]) =>
|
|
122
|
+
`${dir}:{${entries
|
|
123
|
+
.map((e) => e.name)
|
|
124
|
+
.sort()
|
|
125
|
+
.join(',')}}`,
|
|
126
|
+
),
|
|
127
|
+
].join('|'),
|
|
128
|
+
INDEX_END,
|
|
129
|
+
].join('');
|
|
130
|
+
|
|
131
|
+
// Upsert index into target files
|
|
132
|
+
function upsertIndex(filePath) {
|
|
133
|
+
const content = readFileSync(filePath, 'utf-8');
|
|
134
|
+
const [before, ...afterParts] = content.split(INDEX_START);
|
|
135
|
+
const after =
|
|
136
|
+
afterParts.length > 0
|
|
137
|
+
? afterParts.join(INDEX_START).split(INDEX_END).slice(1).join(INDEX_END)
|
|
138
|
+
: undefined;
|
|
139
|
+
|
|
140
|
+
writeFileSync(
|
|
141
|
+
filePath,
|
|
142
|
+
after !== undefined ? before + indexSnippet + after : `${content.trimEnd()}\n${indexSnippet}\n`,
|
|
143
|
+
);
|
|
144
|
+
console.log(`Updated ${relative(process.cwd(), filePath)} with docs index.`);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const claudeMdPath = join(process.cwd(), 'CLAUDE.md');
|
|
148
|
+
const agentsMdPath = join(process.cwd(), 'AGENTS.md');
|
|
149
|
+
const legacyAgentsMdPath = join(process.cwd(), 'agents.md');
|
|
150
|
+
const claudeExists = existsSync(claudeMdPath);
|
|
151
|
+
const agentsExists = existsSync(agentsMdPath);
|
|
152
|
+
const legacyAgentsExists = !agentsExists && existsSync(legacyAgentsMdPath);
|
|
153
|
+
|
|
154
|
+
if (claudeExists) upsertIndex(claudeMdPath);
|
|
155
|
+
if (agentsExists) upsertIndex(agentsMdPath);
|
|
156
|
+
if (legacyAgentsExists) upsertIndex(legacyAgentsMdPath);
|
|
157
|
+
|
|
158
|
+
if (!claudeExists && !agentsExists && !legacyAgentsExists) {
|
|
159
|
+
writeFileSync(claudeMdPath, `${indexSnippet}\n`);
|
|
160
|
+
console.log('Created CLAUDE.md with docs index.');
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// Remind about .gitignore
|
|
164
|
+
console.log('\nDone! Remember to add .unchained-docs to your .gitignore.');
|
|
@@ -35,14 +35,8 @@ export const createMigrationRunner = ({ currentId, logger = console, migrationRe
|
|
|
35
35
|
.map(this.operationFactory('up'));
|
|
36
36
|
},
|
|
37
37
|
async run() {
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
return [mostRecentId, migrations.length];
|
|
42
|
-
}
|
|
43
|
-
catch (e) {
|
|
44
|
-
logger.error(`Migration failed: ${e.name} - ${e.message}`);
|
|
45
|
-
return [e.migrationId, null];
|
|
46
|
-
}
|
|
38
|
+
const migrations = this.migrateToLatest();
|
|
39
|
+
const mostRecentId = await migrations.reduce((idPromise, migration) => Promise.resolve(idPromise).then(migration), currentId);
|
|
40
|
+
return [mostRecentId, migrations.length];
|
|
47
41
|
},
|
|
48
42
|
});
|
|
@@ -32,15 +32,10 @@ export const runMigrations = async ({ migrationRepository, logger = createLogger
|
|
|
32
32
|
unchainedAPI,
|
|
33
33
|
});
|
|
34
34
|
const [lastMigrationId, operationCount] = await runner.run();
|
|
35
|
-
if (operationCount
|
|
36
|
-
|
|
37
|
-
logger.info(`All ${operationCount} migrations completed with most recent id: ${lastMigrationId}`);
|
|
38
|
-
}
|
|
39
|
-
else {
|
|
40
|
-
logger.debug(`No migrations run, already at latest id: ${currentId}`);
|
|
41
|
-
}
|
|
35
|
+
if (operationCount > 0) {
|
|
36
|
+
logger.info(`All ${operationCount} migrations completed with most recent id: ${lastMigrationId}`);
|
|
42
37
|
}
|
|
43
38
|
else {
|
|
44
|
-
logger.
|
|
39
|
+
logger.debug(`No migrations run, already at latest id: ${currentId}`);
|
|
45
40
|
}
|
|
46
41
|
};
|
|
@@ -12,17 +12,27 @@ export const queueWorkers = [];
|
|
|
12
12
|
export async function setupWorkqueue({ unchainedAPI, migrationRepository, ...workQueueOptions }) {
|
|
13
13
|
if (workQueueOptions.disableWorker || UNCHAINED_DISABLE_WORKER)
|
|
14
14
|
return;
|
|
15
|
-
|
|
15
|
+
try {
|
|
16
|
+
await runMigrations({ migrationRepository, unchainedAPI });
|
|
17
|
+
}
|
|
18
|
+
catch (error) {
|
|
19
|
+
logger.error('Migration failed; continuing startup', {
|
|
20
|
+
error: error instanceof Error ? error.message : String(error),
|
|
21
|
+
});
|
|
22
|
+
}
|
|
16
23
|
(workQueueOptions?.enabledQueueManagers || defaultQueueManagers).forEach((f) => {
|
|
17
24
|
const handler = f.actions(workQueueOptions, unchainedAPI);
|
|
18
25
|
queueWorkers.push(handler);
|
|
19
26
|
handler.start();
|
|
20
27
|
});
|
|
21
28
|
if (workQueueOptions.invalidateProviders ?? !UNCHAINED_DISABLE_PROVIDER_INVALIDATION) {
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
29
|
+
await unchainedAPI.modules.worker.addWorkIfNotExists({
|
|
30
|
+
type: 'INVALIDATE_CARTS',
|
|
31
|
+
input: workQueueOptions.providerInvalidationMaxAgeDays !== undefined
|
|
32
|
+
? { maxAgeDays: workQueueOptions.providerInvalidationMaxAgeDays }
|
|
33
|
+
: {},
|
|
34
|
+
retries: 0,
|
|
35
|
+
}, (work) => !work.autoscheduled);
|
|
26
36
|
}
|
|
27
37
|
if (workQueueOptions.assignCartForUsers ?? Boolean(UNCHAINED_ASSIGN_CART_FOR_USERS)) {
|
|
28
38
|
const users = await unchainedAPI.modules.users.findUsers({});
|
package/lib/startPlatform.d.ts
CHANGED
|
@@ -2,6 +2,7 @@ import { type UnchainedServerOptions } from '@unchainedshop/api';
|
|
|
2
2
|
import { type UnchainedCoreOptions } from '@unchainedshop/core';
|
|
3
3
|
import { mongodb } from '@unchainedshop/mongodb';
|
|
4
4
|
import type { UnchainedCore } from '@unchainedshop/core';
|
|
5
|
+
import { type AuditLogOptions } from '@unchainedshop/events';
|
|
5
6
|
import { MessageTypes } from './setup/setupTemplates.ts';
|
|
6
7
|
import { type SetupWorkqueueOptions } from './setup/setupWorkqueue.ts';
|
|
7
8
|
import type { IRoleOptionConfig } from '@unchainedshop/roles';
|
|
@@ -9,9 +10,11 @@ export { MessageTypes };
|
|
|
9
10
|
export type PlatformOptions = {
|
|
10
11
|
rolesOptions?: IRoleOptionConfig;
|
|
11
12
|
workQueueOptions?: SetupWorkqueueOptions;
|
|
13
|
+
auditLog?: AuditLogOptions | false;
|
|
12
14
|
} & Omit<UnchainedCoreOptions, 'migrationRepository' | 'db'> & Omit<UnchainedServerOptions, 'roles' | 'unchainedAPI'>;
|
|
13
|
-
export declare const startPlatform: ({ modules, services, options, rolesOptions, bulkImporter, bulkExporter, workQueueOptions, ...arbitraryAPIServerConfiguration }: PlatformOptions) => Promise<{
|
|
15
|
+
export declare const startPlatform: ({ modules, services, options, rolesOptions, bulkImporter, bulkExporter, workQueueOptions, auditLog: auditLogConfig, ...arbitraryAPIServerConfiguration }: PlatformOptions) => Promise<{
|
|
14
16
|
unchainedAPI: UnchainedCore;
|
|
15
17
|
graphqlHandler: any;
|
|
16
18
|
db: mongodb.Db;
|
|
19
|
+
shutdown: () => Promise<void>;
|
|
17
20
|
}>;
|
package/lib/startPlatform.js
CHANGED
|
@@ -2,6 +2,8 @@ import { startAPIServer, roles } from '@unchainedshop/api';
|
|
|
2
2
|
import { initCore, pluginRegistry } from '@unchainedshop/core';
|
|
3
3
|
import { initDb, mongodb, stopDb } from '@unchainedshop/mongodb';
|
|
4
4
|
import { defaultLogger } from '@unchainedshop/logger';
|
|
5
|
+
import { getEmitAdapter } from '@unchainedshop/events';
|
|
6
|
+
import { createAuditLog, configureAuditIntegration, setAuditLogInstance, } from '@unchainedshop/events';
|
|
5
7
|
import { setupAccounts } from "./setup/setupAccounts.js";
|
|
6
8
|
import { setupUploadHandlers } from "./setup/setupUploadHandlers.js";
|
|
7
9
|
import { setupTemplates, MessageTypes } from "./setup/setupTemplates.js";
|
|
@@ -30,7 +32,7 @@ const existOnInvalidEnvironmentVariables = () => {
|
|
|
30
32
|
process.exit(1);
|
|
31
33
|
}
|
|
32
34
|
};
|
|
33
|
-
export const startPlatform = async ({ modules, services, options, rolesOptions, bulkImporter, bulkExporter, workQueueOptions, ...arbitraryAPIServerConfiguration }) => {
|
|
35
|
+
export const startPlatform = async ({ modules, services, options, rolesOptions, bulkImporter, bulkExporter, workQueueOptions, auditLog: auditLogConfig, ...arbitraryAPIServerConfiguration }) => {
|
|
34
36
|
const start = performance.now();
|
|
35
37
|
exitOnMissingEnvironmentVariables();
|
|
36
38
|
existOnInvalidEnvironmentVariables();
|
|
@@ -58,6 +60,11 @@ export const startPlatform = async ({ modules, services, options, rolesOptions,
|
|
|
58
60
|
bulkExporter,
|
|
59
61
|
});
|
|
60
62
|
await pluginRegistry.initialize(unchainedAPI);
|
|
63
|
+
let auditLog;
|
|
64
|
+
if (auditLogConfig !== false) {
|
|
65
|
+
auditLog = createAuditLog(auditLogConfig || {});
|
|
66
|
+
setAuditLogInstance(auditLog);
|
|
67
|
+
}
|
|
61
68
|
setupAccounts(unchainedAPI);
|
|
62
69
|
setupTemplates(unchainedAPI);
|
|
63
70
|
setupUploadHandlers(unchainedAPI);
|
|
@@ -66,6 +73,9 @@ export const startPlatform = async ({ modules, services, options, rolesOptions,
|
|
|
66
73
|
roles: configuredRoles,
|
|
67
74
|
...arbitraryAPIServerConfiguration,
|
|
68
75
|
});
|
|
76
|
+
if (auditLog) {
|
|
77
|
+
configureAuditIntegration(auditLog);
|
|
78
|
+
}
|
|
69
79
|
await setupWorkqueue({
|
|
70
80
|
unchainedAPI,
|
|
71
81
|
migrationRepository,
|
|
@@ -73,13 +83,31 @@ export const startPlatform = async ({ modules, services, options, rolesOptions,
|
|
|
73
83
|
});
|
|
74
84
|
const version = UNCHAINED_API_VERSION || npm_package_version || 'n/a';
|
|
75
85
|
defaultLogger.info(`Unchained Engine running`, { version });
|
|
76
|
-
let
|
|
86
|
+
let shutdownPromise;
|
|
87
|
+
const shutdown = () => {
|
|
88
|
+
shutdownPromise ??= (async () => {
|
|
89
|
+
defaultLogger.debug('Stopping Workqueue');
|
|
90
|
+
stopWorkqueue();
|
|
91
|
+
defaultLogger.debug('Shutting down plugins');
|
|
92
|
+
await pluginRegistry.shutdown(unchainedAPI);
|
|
93
|
+
defaultLogger.debug('Shutting down event emitter');
|
|
94
|
+
await getEmitAdapter()?.shutdown?.();
|
|
95
|
+
defaultLogger.debug('Stopping GraphQL server');
|
|
96
|
+
await graphqlHandler.dispose();
|
|
97
|
+
if (auditLog) {
|
|
98
|
+
defaultLogger.debug('Closing audit log');
|
|
99
|
+
await auditLog.close();
|
|
100
|
+
}
|
|
101
|
+
defaultLogger.debug('Stopping DB Connection');
|
|
102
|
+
await stopDb();
|
|
103
|
+
})();
|
|
104
|
+
return shutdownPromise;
|
|
105
|
+
};
|
|
77
106
|
const cleanup = (signal) => async () => {
|
|
78
|
-
if (
|
|
107
|
+
if (shutdownPromise) {
|
|
79
108
|
defaultLogger.debug('Cleanup already in progress, ignoring signal', { signal });
|
|
80
109
|
return;
|
|
81
110
|
}
|
|
82
|
-
isCleaningUp = true;
|
|
83
111
|
defaultLogger.debug('Starting cleanup', { signal });
|
|
84
112
|
const forceExitTimeout = setTimeout(() => {
|
|
85
113
|
defaultLogger.error('Cleanup timeout exceeded, forcing exit');
|
|
@@ -87,14 +115,7 @@ export const startPlatform = async ({ modules, services, options, rolesOptions,
|
|
|
87
115
|
}, CLEANUP_FORCE_EXIT_TIMEOUT_MS);
|
|
88
116
|
forceExitTimeout.unref();
|
|
89
117
|
try {
|
|
90
|
-
|
|
91
|
-
stopWorkqueue();
|
|
92
|
-
defaultLogger.debug('Shutting down plugins', { signal });
|
|
93
|
-
await pluginRegistry.shutdown(unchainedAPI);
|
|
94
|
-
defaultLogger.debug('Stopping GraphQL server', { signal });
|
|
95
|
-
await graphqlHandler.dispose();
|
|
96
|
-
defaultLogger.debug('Stopping DB Connection', { signal });
|
|
97
|
-
await stopDb();
|
|
118
|
+
await shutdown();
|
|
98
119
|
defaultLogger.debug(`Unchained Engine exiting gracefully`, { signal, version });
|
|
99
120
|
clearTimeout(forceExitTimeout);
|
|
100
121
|
process.exit(0);
|
|
@@ -122,5 +143,5 @@ export const startPlatform = async ({ modules, services, options, rolesOptions,
|
|
|
122
143
|
});
|
|
123
144
|
const end = performance.now();
|
|
124
145
|
defaultLogger.debug(`Unchained Engine started in ${(end - start).toFixed(0)} ms`);
|
|
125
|
-
return { unchainedAPI, graphqlHandler, db };
|
|
146
|
+
return { unchainedAPI, graphqlHandler, db, shutdown };
|
|
126
147
|
};
|
|
@@ -2,7 +2,7 @@ import { ProductPricingSheet } from '@unchainedshop/core';
|
|
|
2
2
|
import { ch } from '@unchainedshop/utils';
|
|
3
3
|
export const getOrderPositionsData = async (order, params, context) => {
|
|
4
4
|
const { modules } = context;
|
|
5
|
-
const { useNetPrice, format = ch.priceToString } = params || {};
|
|
5
|
+
const { useNetPrice, format = (price) => ch.priceToString({ ...price, locale: params?.locale }) } = params || {};
|
|
6
6
|
const orderPositions = await modules.orders.positions.findOrderPositions({
|
|
7
7
|
orderId: order._id,
|
|
8
8
|
});
|
|
@@ -2,7 +2,7 @@ import { OrderPricingSheet, OrderPricingRowCategory } from '@unchainedshop/core'
|
|
|
2
2
|
import { ch } from '@unchainedshop/utils';
|
|
3
3
|
export const getOrderSummaryData = async (order, params, context) => {
|
|
4
4
|
const { modules } = context;
|
|
5
|
-
const { useNetPrice, format = ch.priceToString } = params || {};
|
|
5
|
+
const { useNetPrice, format = (price) => ch.priceToString({ ...price, locale: params?.locale }) } = params || {};
|
|
6
6
|
const orderDelivery = order.deliveryId &&
|
|
7
7
|
(await modules.orders.deliveries.findDelivery({
|
|
8
8
|
orderDeliveryId: order.deliveryId,
|
|
@@ -21,7 +21,7 @@ export const getOrderSummaryData = async (order, params, context) => {
|
|
|
21
21
|
const deliveryProvider = await modules.delivery.findProvider({
|
|
22
22
|
deliveryProviderId: orderDelivery.deliveryProviderId,
|
|
23
23
|
});
|
|
24
|
-
const deliveryAddress = ch.addressToString(orderDelivery?.context?.deliveryAddress || order.billingAddress);
|
|
24
|
+
const deliveryAddress = ch.addressToString(orderDelivery?.context?.address || orderDelivery?.context?.deliveryAddress || order.billingAddress);
|
|
25
25
|
const billingAddress = ch.addressToString(order.billingAddress);
|
|
26
26
|
const orderPricing = OrderPricingSheet({
|
|
27
27
|
calculation: order.calculation,
|
|
@@ -1,14 +1,22 @@
|
|
|
1
1
|
const { EMAIL_FROM, EMAIL_WEBSITE_NAME, EMAIL_WEBSITE_URL } = process.env;
|
|
2
|
-
export const resolveEnrollmentStatusTemplate = async ({ enrollmentId }, context) => {
|
|
2
|
+
export const resolveEnrollmentStatusTemplate = async ({ enrollmentId, locale }, context) => {
|
|
3
3
|
const { modules } = context;
|
|
4
4
|
const enrollment = await modules.enrollments.findEnrollment({ enrollmentId });
|
|
5
5
|
const user = await modules.users.findUserById(enrollment.userId);
|
|
6
|
-
const subject = `${EMAIL_WEBSITE_NAME}: Updated Enrollment / ${enrollment.enrollmentNumber}`;
|
|
6
|
+
const subject = `${EMAIL_WEBSITE_NAME}: Updated Enrollment / ${enrollment.enrollmentNumber || enrollment._id}`;
|
|
7
7
|
const url = `${EMAIL_WEBSITE_URL}/enrollment?_id=${enrollment._id}`;
|
|
8
|
+
const formatDate = (date) => new Date(date).toLocaleDateString(locale || 'en', { dateStyle: 'long' });
|
|
9
|
+
const scheduledDates = [
|
|
10
|
+
enrollment.expires && `Ends: ${formatDate(enrollment.expires)}`,
|
|
11
|
+
enrollment.resumeAt && `Resumes: ${formatDate(enrollment.resumeAt)}`,
|
|
12
|
+
enrollment.minimumCommitmentEnd &&
|
|
13
|
+
`Minimum commitment until: ${formatDate(enrollment.minimumCommitmentEnd)}`,
|
|
14
|
+
].filter(Boolean);
|
|
8
15
|
const text = `
|
|
9
16
|
${subject}\n
|
|
10
17
|
\n
|
|
11
18
|
Status: ${enrollment.status}
|
|
19
|
+
${scheduledDates.join('\n ')}
|
|
12
20
|
\n
|
|
13
21
|
-----------------\n
|
|
14
22
|
Show: ${url}\n
|
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { safeStringify } from '@unchainedshop/logger';
|
|
2
2
|
const { EMAIL_FROM = 'noreply@unchained.local', EMAIL_ERROR_REPORT_RECIPIENT = 'support@unchained.local', EMAIL_WEBSITE_NAME = 'Unchained', ROOT_URL, } = process.env;
|
|
3
3
|
const formatWorkItems = (workItems) => {
|
|
4
4
|
return workItems
|
|
5
5
|
.map(({ _id, type, started, error }) => {
|
|
6
|
-
const stringifiedErrors =
|
|
6
|
+
const stringifiedErrors = safeStringify(error, null, 2);
|
|
7
7
|
return `${new Date(started).toLocaleString()} ${type} (${_id}): ${stringifiedErrors}`;
|
|
8
8
|
})
|
|
9
9
|
.join('\n');
|
package/package.json
CHANGED
|
@@ -1,10 +1,17 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@unchainedshop/platform",
|
|
3
3
|
"description": "Complete e-commerce platform bundle for the Unchained Engine",
|
|
4
|
-
"version": "5.0.0-alpha.
|
|
4
|
+
"version": "5.0.0-alpha.11",
|
|
5
|
+
"files": [
|
|
6
|
+
"lib",
|
|
7
|
+
"bin"
|
|
8
|
+
],
|
|
5
9
|
"main": "lib/platform-index.js",
|
|
6
10
|
"types": "lib/platform-index.d.ts",
|
|
7
11
|
"type": "module",
|
|
12
|
+
"bin": {
|
|
13
|
+
"unchained": "./bin/unchained.js"
|
|
14
|
+
},
|
|
8
15
|
"scripts": {
|
|
9
16
|
"clean": "tsc -b --clean",
|
|
10
17
|
"build": "tsc -b",
|
|
@@ -34,22 +41,21 @@
|
|
|
34
41
|
},
|
|
35
42
|
"homepage": "https://github.com/unchainedshop/unchained#readme",
|
|
36
43
|
"engines": {
|
|
37
|
-
"node": ">=
|
|
44
|
+
"node": ">=26.0.0",
|
|
38
45
|
"npm": ">=10.0.0"
|
|
39
46
|
},
|
|
40
47
|
"dependencies": {
|
|
41
|
-
"@unchainedshop/api": "^5.0.0-alpha.
|
|
42
|
-
"@unchainedshop/core": "^5.0.0-alpha.
|
|
43
|
-
"@unchainedshop/events": "^5.0.0-alpha.
|
|
44
|
-
"@unchainedshop/logger": "^5.0.0-alpha.
|
|
45
|
-
"@unchainedshop/mongodb": "^5.0.0-alpha.
|
|
46
|
-
"@unchainedshop/plugins": "^5.0.0-alpha.
|
|
47
|
-
"@unchainedshop/roles": "^5.0.0-alpha.
|
|
48
|
-
"@unchainedshop/utils": "^5.0.0-alpha.
|
|
49
|
-
"safe-stable-stringify": "^2.5.0"
|
|
48
|
+
"@unchainedshop/api": "^5.0.0-alpha.11",
|
|
49
|
+
"@unchainedshop/core": "^5.0.0-alpha.11",
|
|
50
|
+
"@unchainedshop/events": "^5.0.0-alpha.11",
|
|
51
|
+
"@unchainedshop/logger": "^5.0.0-alpha.11",
|
|
52
|
+
"@unchainedshop/mongodb": "^5.0.0-alpha.11",
|
|
53
|
+
"@unchainedshop/plugins": "^5.0.0-alpha.11",
|
|
54
|
+
"@unchainedshop/roles": "^5.0.0-alpha.11",
|
|
55
|
+
"@unchainedshop/utils": "^5.0.0-alpha.11"
|
|
50
56
|
},
|
|
51
57
|
"devDependencies": {
|
|
52
|
-
"@types/node": "^
|
|
53
|
-
"typescript": "^
|
|
58
|
+
"@types/node": "^26.2.0",
|
|
59
|
+
"typescript": "^6.0.3"
|
|
54
60
|
}
|
|
55
61
|
}
|