@unchainedshop/platform 4.8.29 → 5.0.0-alpha.10

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
package/bin/unchained.js CHANGED
@@ -28,7 +28,7 @@ if (command !== 'download-llm-docs') {
28
28
  Commands:
29
29
  download-llm-docs Download Unchained Engine docs for LLM coding assistants
30
30
  (Claude Code, Cursor, etc.) and inject a file index into
31
- CLAUDE.md and/or agents.md`);
31
+ CLAUDE.md and/or AGENTS.md`);
32
32
  process.exit(command ? 1 : 0);
33
33
  }
34
34
 
@@ -114,7 +114,7 @@ const indexSnippet = [
114
114
  `[Unchained Engine v${version} Docs Index]`,
115
115
  'root: ./.unchained-docs',
116
116
  'Search these docs before answering Unchained questions.',
117
- 'If docs missing, run: npx unchained download-llm-docs',
117
+ 'If docs missing, run: npx --package @unchainedshop/platform unchained download-llm-docs',
118
118
  ...[...markdownsByDir]
119
119
  .sort(([a], [b]) => a.localeCompare(b))
120
120
  .map(
@@ -145,14 +145,17 @@ function upsertIndex(filePath) {
145
145
  }
146
146
 
147
147
  const claudeMdPath = join(process.cwd(), 'CLAUDE.md');
148
- const agentsMdPath = join(process.cwd(), 'agents.md');
148
+ const agentsMdPath = join(process.cwd(), 'AGENTS.md');
149
+ const legacyAgentsMdPath = join(process.cwd(), 'agents.md');
149
150
  const claudeExists = existsSync(claudeMdPath);
150
151
  const agentsExists = existsSync(agentsMdPath);
152
+ const legacyAgentsExists = !agentsExists && existsSync(legacyAgentsMdPath);
151
153
 
152
154
  if (claudeExists) upsertIndex(claudeMdPath);
153
155
  if (agentsExists) upsertIndex(agentsMdPath);
156
+ if (legacyAgentsExists) upsertIndex(legacyAgentsMdPath);
154
157
 
155
- if (!claudeExists && !agentsExists) {
158
+ if (!claudeExists && !agentsExists && !legacyAgentsExists) {
156
159
  writeFileSync(claudeMdPath, `${indexSnippet}\n`);
157
160
  console.log('Created CLAUDE.md with docs index.');
158
161
  }
@@ -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
  };
@@ -1,4 +1,4 @@
1
- import { FileDirector } from '@unchainedshop/file-upload';
1
+ import { FileDirector } from '@unchainedshop/core';
2
2
  export const setupUploadHandlers = ({ services, modules }) => {
3
3
  FileDirector.registerFileUploadCallback('user-avatars', async (file) => {
4
4
  return services.users.updateUserAvatarAfterUpload({ file });
@@ -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);
@@ -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,8 +10,9 @@ 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;
@@ -1,7 +1,9 @@
1
1
  import { startAPIServer, roles } from '@unchainedshop/api';
2
- import { initCore } from '@unchainedshop/core';
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,22 +32,39 @@ 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();
37
39
  const configuredRoles = roles.configureRoles(rolesOptions || {});
38
40
  const db = await initDb();
39
41
  const migrationRepository = createMigrationRepository(db);
42
+ const pluginModuleFactories = pluginRegistry.getModuleFactories();
43
+ const pluginModules = pluginModuleFactories.reduce((acc, factory) => {
44
+ const factoryModules = factory({ db });
45
+ Object.entries(factoryModules).forEach(([key, moduleInstance]) => {
46
+ acc[key] = {
47
+ configure: () => moduleInstance,
48
+ };
49
+ });
50
+ return acc;
51
+ }, {});
52
+ const allModules = { ...pluginModules, ...(modules || {}) };
40
53
  const unchainedAPI = await initCore({
41
54
  db,
42
55
  migrationRepository,
43
56
  bulkImporter,
44
- modules,
57
+ modules: allModules,
45
58
  services,
46
59
  options,
47
60
  bulkExporter,
48
61
  });
62
+ await pluginRegistry.initialize(unchainedAPI);
63
+ let auditLog;
64
+ if (auditLogConfig !== false) {
65
+ auditLog = createAuditLog(auditLogConfig || {});
66
+ setAuditLogInstance(auditLog);
67
+ }
49
68
  setupAccounts(unchainedAPI);
50
69
  setupTemplates(unchainedAPI);
51
70
  setupUploadHandlers(unchainedAPI);
@@ -54,6 +73,9 @@ export const startPlatform = async ({ modules, services, options, rolesOptions,
54
73
  roles: configuredRoles,
55
74
  ...arbitraryAPIServerConfiguration,
56
75
  });
76
+ if (auditLog) {
77
+ configureAuditIntegration(auditLog);
78
+ }
57
79
  await setupWorkqueue({
58
80
  unchainedAPI,
59
81
  migrationRepository,
@@ -77,8 +99,16 @@ export const startPlatform = async ({ modules, services, options, rolesOptions,
77
99
  try {
78
100
  defaultLogger.debug('Stopping Workqueue', { signal });
79
101
  stopWorkqueue();
102
+ defaultLogger.debug('Shutting down plugins', { signal });
103
+ await pluginRegistry.shutdown(unchainedAPI);
104
+ defaultLogger.debug('Shutting down event emitter', { signal });
105
+ await getEmitAdapter()?.shutdown?.();
80
106
  defaultLogger.debug('Stopping GraphQL server', { signal });
81
107
  await graphqlHandler.dispose();
108
+ if (auditLog) {
109
+ defaultLogger.debug('Closing audit log', { signal });
110
+ await auditLog.close();
111
+ }
82
112
  defaultLogger.debug('Stopping DB Connection', { signal });
83
113
  await stopDb();
84
114
  defaultLogger.debug(`Unchained Engine exiting gracefully`, { signal, version });
@@ -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,
package/package.json CHANGED
@@ -1,7 +1,11 @@
1
1
  {
2
2
  "name": "@unchainedshop/platform",
3
3
  "description": "Complete e-commerce platform bundle for the Unchained Engine",
4
- "version": "4.8.29",
4
+ "version": "5.0.0-alpha.10",
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",
@@ -37,23 +41,21 @@
37
41
  },
38
42
  "homepage": "https://github.com/unchainedshop/unchained#readme",
39
43
  "engines": {
40
- "node": ">=22.0.0",
44
+ "node": ">=26.0.0",
41
45
  "npm": ">=10.0.0"
42
46
  },
43
47
  "dependencies": {
44
- "@unchainedshop/api": "^4.8.27",
45
- "@unchainedshop/core": "^4.8.27",
46
- "@unchainedshop/core-orders": "^4.8.27",
47
- "@unchainedshop/events": "^4.8.27",
48
- "@unchainedshop/file-upload": "^4.8.27",
49
- "@unchainedshop/logger": "^4.8.27",
50
- "@unchainedshop/mongodb": "^4.8.27",
51
- "@unchainedshop/plugins": "^4.8.27",
52
- "@unchainedshop/roles": "^4.8.27",
53
- "@unchainedshop/utils": "^4.8.27"
48
+ "@unchainedshop/api": "^5.0.0-alpha.10",
49
+ "@unchainedshop/core": "^5.0.0-alpha.10",
50
+ "@unchainedshop/events": "^5.0.0-alpha.10",
51
+ "@unchainedshop/logger": "^5.0.0-alpha.10",
52
+ "@unchainedshop/mongodb": "^5.0.0-alpha.10",
53
+ "@unchainedshop/plugins": "^5.0.0-alpha.10",
54
+ "@unchainedshop/roles": "^5.0.0-alpha.10",
55
+ "@unchainedshop/utils": "^5.0.0-alpha.10"
54
56
  },
55
57
  "devDependencies": {
56
58
  "@types/node": "^26.2.0",
57
- "typescript": "^5.8.3"
59
+ "typescript": "^6.0.3"
58
60
  }
59
61
  }