@unchainedshop/platform 5.0.0-alpha.5 → 5.0.0-alpha.8

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 CHANGED
@@ -3,146 +3,116 @@
3
3
 
4
4
  # @unchainedshop/platform
5
5
 
6
- Umbrella package for the Unchained Engine. Provides complete platform setup including database initialization, API server, migrations, templates, and runtime configuration.
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
- ## Usage
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
- const app = express();
24
+ registerBasePlugins();
21
25
 
22
- const { unchainedAPI, graphqlHandler } = await startPlatform({
23
- express: app,
24
- options: {
25
- // Platform options
26
- },
27
- });
26
+ const app = express();
27
+ const platform = await startPlatform({});
28
+ await connect(app, platform);
28
29
 
29
- // Platform is ready
30
+ const products = await platform.unchainedAPI.modules.products.findProducts({});
30
31
  app.listen(4010);
31
32
  ```
32
33
 
33
- ## API Overview
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.
34
35
 
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
- });
92
-
93
- // Access unchained API
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
- modules: {
108
- orders: {
109
- // Order module options
110
- },
111
- products: {
112
- // Product module options
113
- },
114
- },
115
- services: {
116
- // Custom services
43
+ orders: {
44
+ // Order module settings
117
45
  },
118
- bulkImporter: {
119
- handlers: {
120
- // Custom import handlers
121
- },
46
+ products: {
47
+ // Product module settings
122
48
  },
123
49
  },
124
- workQueueConfig: {
125
- batchSize: 10,
126
- pollInterval: 1000,
50
+ modules: {
51
+ // Custom modules with a configure({ db, migrationRepository, options }) function
52
+ },
53
+ services: {
54
+ // Custom services
55
+ },
56
+ bulkImporter: {
57
+ handlers: {
58
+ // Custom import handlers
59
+ },
127
60
  },
128
- context: (defaultResolver) => async (props, req, res) => {
129
- const context = await defaultResolver(props, req, res);
130
- return {
131
- ...context,
132
- // Custom context properties
133
- };
61
+ workQueueOptions: {
62
+ skipInvalidationOnStartup: true,
134
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
- ## Returns
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.
74
+
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 |
139
109
 
140
- The `startPlatform` function returns:
110
+ ## Return value
141
111
 
142
112
  | Property | Description |
143
113
  |----------|-------------|
144
- | `unchainedAPI` | Complete Unchained core API instance |
145
- | `graphqlHandler` | GraphQL Yoga server instance for Express/Fastify |
114
+ | `unchainedAPI` | Core modules, services, bulk importer/exporter, and options |
115
+ | `graphqlHandler` | GraphQL Yoga server instance |
146
116
  | `db` | MongoDB database instance |
147
117
 
148
118
  ## License
@@ -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
- try {
39
- const migrations = this.migrateToLatest();
40
- const mostRecentId = await migrations.reduce((idPromise, migration) => Promise.resolve(idPromise).then(migration), currentId);
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 !== null) {
36
- if (operationCount > 0) {
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.warn(`Some migrations failed, last successful id: ${lastMigrationId}`);
39
+ logger.debug(`No migrations run, already at latest id: ${currentId}`);
45
40
  }
46
41
  };
@@ -12,7 +12,14 @@ export const queueWorkers = [];
12
12
  export async function setupWorkqueue({ unchainedAPI, migrationRepository, ...workQueueOptions }) {
13
13
  if (workQueueOptions.disableWorker || UNCHAINED_DISABLE_WORKER)
14
14
  return;
15
- await runMigrations({ migrationRepository, unchainedAPI });
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);
@@ -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,
package/package.json CHANGED
@@ -1,10 +1,13 @@
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.5",
4
+ "version": "5.0.0-alpha.8",
5
5
  "main": "lib/platform-index.js",
6
6
  "types": "lib/platform-index.d.ts",
7
7
  "type": "module",
8
+ "bin": {
9
+ "unchained": "./bin/unchained.js"
10
+ },
8
11
  "scripts": {
9
12
  "clean": "tsc -b --clean",
10
13
  "build": "tsc -b",
@@ -34,18 +37,18 @@
34
37
  },
35
38
  "homepage": "https://github.com/unchainedshop/unchained#readme",
36
39
  "engines": {
37
- "node": ">=24.0.0",
40
+ "node": ">=26.0.0",
38
41
  "npm": ">=10.0.0"
39
42
  },
40
43
  "dependencies": {
41
- "@unchainedshop/api": "^5.0.0-alpha.5",
42
- "@unchainedshop/core": "^5.0.0-alpha.5",
43
- "@unchainedshop/events": "^5.0.0-alpha.5",
44
- "@unchainedshop/logger": "^5.0.0-alpha.5",
45
- "@unchainedshop/mongodb": "^5.0.0-alpha.5",
46
- "@unchainedshop/plugins": "^5.0.0-alpha.5",
47
- "@unchainedshop/roles": "^5.0.0-alpha.5",
48
- "@unchainedshop/utils": "^5.0.0-alpha.5"
44
+ "@unchainedshop/api": "^5.0.0-alpha.8",
45
+ "@unchainedshop/core": "^5.0.0-alpha.8",
46
+ "@unchainedshop/events": "^5.0.0-alpha.8",
47
+ "@unchainedshop/logger": "^5.0.0-alpha.8",
48
+ "@unchainedshop/mongodb": "^5.0.0-alpha.8",
49
+ "@unchainedshop/plugins": "^5.0.0-alpha.8",
50
+ "@unchainedshop/roles": "^5.0.0-alpha.8",
51
+ "@unchainedshop/utils": "^5.0.0-alpha.8"
49
52
  },
50
53
  "devDependencies": {
51
54
  "@types/node": "^26.2.0",