sharetribe-flex-build-sdk 1.15.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.
- package/README.md +222 -0
- package/build.js +49 -0
- package/package.json +54 -0
- package/src/api/client.ts +218 -0
- package/src/api/http-client.ts +135 -0
- package/src/api/multipart.ts +78 -0
- package/src/api/transit.ts +96 -0
- package/src/assets.ts +116 -0
- package/src/auth-storage.ts +77 -0
- package/src/deploy.ts +96 -0
- package/src/edn-process.ts +126 -0
- package/src/events.ts +203 -0
- package/src/index.ts +101 -0
- package/src/listing-approval.ts +59 -0
- package/src/notifications.ts +89 -0
- package/src/processes.ts +320 -0
- package/src/sdk-exports.md +25 -0
- package/src/search.ts +273 -0
- package/src/stripe.ts +39 -0
- package/src/types/jsedn.d.ts +9 -0
- package/src/types/transit-js.d.ts +10 -0
- package/src/types.ts +38 -0
- package/tsconfig.json +19 -0
- package/vitest.config.ts +8 -0
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Multipart form-data implementation using Node.js streams
|
|
3
|
+
*
|
|
4
|
+
* No external dependencies - implements multipart/form-data manually
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { randomBytes } from 'node:crypto';
|
|
8
|
+
import { createReadStream } from 'node:fs';
|
|
9
|
+
|
|
10
|
+
export interface MultipartField {
|
|
11
|
+
name: string;
|
|
12
|
+
value: string | Buffer;
|
|
13
|
+
filename?: string;
|
|
14
|
+
contentType?: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Generates a random boundary for multipart form-data
|
|
19
|
+
* Returns the boundary without the -- prefix (that gets added when building the body)
|
|
20
|
+
*/
|
|
21
|
+
function generateBoundary(): string {
|
|
22
|
+
return `WebKitFormBoundary${randomBytes(16).toString('hex')}`;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Creates multipart form-data body from fields
|
|
27
|
+
*/
|
|
28
|
+
export function createMultipartBody(fields: MultipartField[]): {
|
|
29
|
+
body: Buffer;
|
|
30
|
+
boundary: string;
|
|
31
|
+
contentType: string;
|
|
32
|
+
} {
|
|
33
|
+
const boundary = generateBoundary();
|
|
34
|
+
const parts: Buffer[] = [];
|
|
35
|
+
|
|
36
|
+
for (const field of fields) {
|
|
37
|
+
// Add boundary
|
|
38
|
+
parts.push(Buffer.from(`--${boundary}\r\n`));
|
|
39
|
+
|
|
40
|
+
// Add Content-Disposition header
|
|
41
|
+
if (field.filename) {
|
|
42
|
+
parts.push(
|
|
43
|
+
Buffer.from(
|
|
44
|
+
`Content-Disposition: form-data; name="${field.name}"; filename="${field.filename}"\r\n`
|
|
45
|
+
)
|
|
46
|
+
);
|
|
47
|
+
|
|
48
|
+
// Add Content-Type if specified
|
|
49
|
+
if (field.contentType) {
|
|
50
|
+
parts.push(Buffer.from(`Content-Type: ${field.contentType}\r\n`));
|
|
51
|
+
}
|
|
52
|
+
} else {
|
|
53
|
+
parts.push(Buffer.from(`Content-Disposition: form-data; name="${field.name}"\r\n`));
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
parts.push(Buffer.from('\r\n'));
|
|
57
|
+
|
|
58
|
+
// Add value
|
|
59
|
+
if (typeof field.value === 'string') {
|
|
60
|
+
parts.push(Buffer.from(field.value));
|
|
61
|
+
} else {
|
|
62
|
+
parts.push(field.value);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
parts.push(Buffer.from('\r\n'));
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Add final boundary
|
|
69
|
+
parts.push(Buffer.from(`--${boundary}--\r\n`));
|
|
70
|
+
|
|
71
|
+
const body = Buffer.concat(parts);
|
|
72
|
+
|
|
73
|
+
return {
|
|
74
|
+
body,
|
|
75
|
+
boundary,
|
|
76
|
+
contentType: `multipart/form-data; boundary=${boundary}`,
|
|
77
|
+
};
|
|
78
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Transit format encoding/decoding for Sharetribe API
|
|
3
|
+
*
|
|
4
|
+
* The Build API expects Transit format (application/transit+json) for certain endpoints.
|
|
5
|
+
* Transit is a data format that extends JSON with support for additional data types.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import transit from 'transit-js';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Encodes data to Transit JSON format
|
|
12
|
+
*
|
|
13
|
+
* @param data - The data to encode (can include keywords, dates, etc.)
|
|
14
|
+
* @returns Transit-encoded string
|
|
15
|
+
*/
|
|
16
|
+
export function encodeTransit(data: unknown): string {
|
|
17
|
+
const writer = transit.writer('json', {
|
|
18
|
+
handlers: transit.map([
|
|
19
|
+
// Add any custom handlers here if needed
|
|
20
|
+
])
|
|
21
|
+
});
|
|
22
|
+
return writer.write(data);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Converts Transit values (maps, keywords, etc.) to plain JavaScript objects
|
|
27
|
+
*
|
|
28
|
+
* @param val - Transit value
|
|
29
|
+
* @returns Plain JavaScript value
|
|
30
|
+
*/
|
|
31
|
+
function transitToJS(val: any): any {
|
|
32
|
+
// Handle Transit maps
|
|
33
|
+
if (val && typeof val.get === 'function' && val._entries) {
|
|
34
|
+
const obj: Record<string, any> = {};
|
|
35
|
+
for (let i = 0; i < val._entries.length; i += 2) {
|
|
36
|
+
const key = val._entries[i];
|
|
37
|
+
const value = val._entries[i + 1];
|
|
38
|
+
// Convert keyword keys to strings (e.g., :data -> "data")
|
|
39
|
+
const keyStr = key._name || String(key);
|
|
40
|
+
obj[keyStr] = transitToJS(value);
|
|
41
|
+
}
|
|
42
|
+
return obj;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Handle Transit keywords
|
|
46
|
+
if (val && val._name !== undefined) {
|
|
47
|
+
return val._name;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Handle arrays
|
|
51
|
+
if (Array.isArray(val)) {
|
|
52
|
+
return val.map(transitToJS);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Return primitives as-is
|
|
56
|
+
return val;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Decodes Transit JSON format to JavaScript objects
|
|
61
|
+
*
|
|
62
|
+
* @param transitString - Transit-encoded string
|
|
63
|
+
* @returns Decoded JavaScript object
|
|
64
|
+
*/
|
|
65
|
+
export function decodeTransit(transitString: string): unknown {
|
|
66
|
+
const reader = transit.reader('json');
|
|
67
|
+
const transitValue = reader.read(transitString);
|
|
68
|
+
return transitToJS(transitValue);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Creates a Transit keyword (used for Clojure-style keywords in the API)
|
|
73
|
+
*
|
|
74
|
+
* @param name - The keyword name (e.g., "default-booking")
|
|
75
|
+
* @returns Transit keyword object
|
|
76
|
+
*/
|
|
77
|
+
export function keyword(name: string): unknown {
|
|
78
|
+
return transit.keyword(name);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Creates a Transit map with keyword keys
|
|
83
|
+
*
|
|
84
|
+
* This is needed because the Sharetribe API expects Transit maps with keyword keys,
|
|
85
|
+
* not string keys. In Clojure/Transit: {:name :value} not {"name" :value}
|
|
86
|
+
*
|
|
87
|
+
* @param obj - Plain JavaScript object with string keys
|
|
88
|
+
* @returns Transit map with keyword keys
|
|
89
|
+
*/
|
|
90
|
+
export function keywordMap(obj: Record<string, unknown>): unknown {
|
|
91
|
+
const entries: unknown[] = [];
|
|
92
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
93
|
+
entries.push(transit.keyword(key), value);
|
|
94
|
+
}
|
|
95
|
+
return transit.map(entries);
|
|
96
|
+
}
|
package/src/assets.ts
ADDED
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Asset management functions
|
|
3
|
+
*
|
|
4
|
+
* Programmatic API for managing marketplace assets
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { apiGet, apiPostMultipart, type MultipartField } from './api/client.js';
|
|
8
|
+
|
|
9
|
+
export interface Asset {
|
|
10
|
+
path: string;
|
|
11
|
+
dataRaw: string; // base64 encoded
|
|
12
|
+
contentHash?: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface PullAssetsResult {
|
|
16
|
+
version: string;
|
|
17
|
+
assets: Asset[];
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface PushAssetsResult {
|
|
21
|
+
version: string;
|
|
22
|
+
assets: Array<{ path: string; contentHash: string }>;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Pulls assets from remote
|
|
27
|
+
*
|
|
28
|
+
* @param apiKey - Sharetribe API key (optional, reads from auth file if not provided)
|
|
29
|
+
* @param marketplace - Marketplace ID
|
|
30
|
+
* @param options - Pull options
|
|
31
|
+
* @returns Assets and version information
|
|
32
|
+
*/
|
|
33
|
+
export async function pullAssets(
|
|
34
|
+
apiKey: string | undefined,
|
|
35
|
+
marketplace: string,
|
|
36
|
+
options?: { version?: string }
|
|
37
|
+
): Promise<PullAssetsResult> {
|
|
38
|
+
const params: Record<string, string> = { marketplace };
|
|
39
|
+
|
|
40
|
+
if (options?.version) {
|
|
41
|
+
params.version = options.version;
|
|
42
|
+
} else {
|
|
43
|
+
params['version-alias'] = 'latest';
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const response = await apiGet<{
|
|
47
|
+
data: Array<{
|
|
48
|
+
path: string;
|
|
49
|
+
'data-raw': string;
|
|
50
|
+
'content-hash'?: string;
|
|
51
|
+
}>;
|
|
52
|
+
meta: { version?: string; 'aliased-version'?: string };
|
|
53
|
+
}>(apiKey, '/assets/pull', params);
|
|
54
|
+
|
|
55
|
+
const version = response.meta.version || response.meta['aliased-version'];
|
|
56
|
+
if (!version) {
|
|
57
|
+
throw new Error('No version information in response');
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return {
|
|
61
|
+
version,
|
|
62
|
+
assets: response.data.map(a => ({
|
|
63
|
+
path: a.path,
|
|
64
|
+
dataRaw: a['data-raw'],
|
|
65
|
+
contentHash: a['content-hash'],
|
|
66
|
+
})),
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Pushes assets to remote
|
|
72
|
+
*
|
|
73
|
+
* @param apiKey - Sharetribe API key (optional, reads from auth file if not provided)
|
|
74
|
+
* @param marketplace - Marketplace ID
|
|
75
|
+
* @param currentVersion - Current version (use 'nil' if first push)
|
|
76
|
+
* @param operations - Array of operations to perform
|
|
77
|
+
* @returns New version and asset metadata
|
|
78
|
+
*/
|
|
79
|
+
export async function pushAssets(
|
|
80
|
+
apiKey: string | undefined,
|
|
81
|
+
marketplace: string,
|
|
82
|
+
currentVersion: string,
|
|
83
|
+
operations: Array<{
|
|
84
|
+
path: string;
|
|
85
|
+
op: 'upsert' | 'delete';
|
|
86
|
+
data?: Buffer;
|
|
87
|
+
}>
|
|
88
|
+
): Promise<PushAssetsResult> {
|
|
89
|
+
const fields: MultipartField[] = [
|
|
90
|
+
{ name: 'current-version', value: currentVersion },
|
|
91
|
+
];
|
|
92
|
+
|
|
93
|
+
for (let i = 0; i < operations.length; i++) {
|
|
94
|
+
const op = operations[i];
|
|
95
|
+
fields.push({ name: `path-${i}`, value: op.path });
|
|
96
|
+
fields.push({ name: `op-${i}`, value: op.op });
|
|
97
|
+
if (op.op === 'upsert' && op.data) {
|
|
98
|
+
fields.push({ name: `data-raw-${i}`, value: op.data });
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const response = await apiPostMultipart<{
|
|
103
|
+
data: {
|
|
104
|
+
version: string;
|
|
105
|
+
'asset-meta': { assets: Array<{ path: string; 'content-hash': string }> };
|
|
106
|
+
};
|
|
107
|
+
}>(apiKey, '/assets/push', { marketplace }, fields);
|
|
108
|
+
|
|
109
|
+
return {
|
|
110
|
+
version: response.data.version,
|
|
111
|
+
assets: response.data['asset-meta'].assets.map(a => ({
|
|
112
|
+
path: a.path,
|
|
113
|
+
contentHash: a['content-hash'],
|
|
114
|
+
})),
|
|
115
|
+
};
|
|
116
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Authentication storage - manages ~/.config/flex-cli/auth.edn
|
|
3
|
+
*
|
|
4
|
+
* Must maintain 100% compatibility with flex-cli's auth.edn format
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { readFileSync, writeFileSync, mkdirSync, existsSync } from 'node:fs';
|
|
8
|
+
import { homedir } from 'node:os';
|
|
9
|
+
import { join } from 'node:path';
|
|
10
|
+
import edn from 'jsedn';
|
|
11
|
+
|
|
12
|
+
const CONFIG_DIR = join(homedir(), '.config', 'flex-cli');
|
|
13
|
+
const AUTH_FILE = join(CONFIG_DIR, 'auth.edn');
|
|
14
|
+
|
|
15
|
+
export interface AuthData {
|
|
16
|
+
apiKey: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Reads authentication data from ~/.config/flex-cli/auth.edn
|
|
21
|
+
*
|
|
22
|
+
* Returns null if file doesn't exist or is invalid
|
|
23
|
+
*/
|
|
24
|
+
export function readAuth(): AuthData | null {
|
|
25
|
+
try {
|
|
26
|
+
if (!existsSync(AUTH_FILE)) {
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const content = readFileSync(AUTH_FILE, 'utf-8');
|
|
31
|
+
const parsed = edn.parse(content);
|
|
32
|
+
|
|
33
|
+
// EDN keys are symbols, get :api-key
|
|
34
|
+
const apiKeySymbol = edn.kw(':api-key');
|
|
35
|
+
const apiKey = parsed.at(apiKeySymbol);
|
|
36
|
+
|
|
37
|
+
if (typeof apiKey !== 'string') {
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return { apiKey };
|
|
42
|
+
} catch (error) {
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Writes authentication data to ~/.config/flex-cli/auth.edn
|
|
49
|
+
*
|
|
50
|
+
* Format must match flex-cli exactly: {:api-key "..."}
|
|
51
|
+
*/
|
|
52
|
+
export function writeAuth(data: AuthData): void {
|
|
53
|
+
// Ensure config directory exists
|
|
54
|
+
if (!existsSync(CONFIG_DIR)) {
|
|
55
|
+
mkdirSync(CONFIG_DIR, { recursive: true });
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Create EDN map with :api-key
|
|
59
|
+
const authMap = new (edn as any).Map([edn.kw(':api-key'), data.apiKey]);
|
|
60
|
+
const ednString = edn.encode(authMap);
|
|
61
|
+
|
|
62
|
+
writeFileSync(AUTH_FILE, ednString, 'utf-8');
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Clears authentication data (deletes auth.edn file)
|
|
67
|
+
*/
|
|
68
|
+
export async function clearAuth(): Promise<void> {
|
|
69
|
+
try {
|
|
70
|
+
if (existsSync(AUTH_FILE)) {
|
|
71
|
+
const fs = await import('node:fs/promises');
|
|
72
|
+
await fs.unlink(AUTH_FILE);
|
|
73
|
+
}
|
|
74
|
+
} catch (error) {
|
|
75
|
+
// Ignore errors if file doesn't exist
|
|
76
|
+
}
|
|
77
|
+
}
|
package/src/deploy.ts
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Process deployment functions
|
|
3
|
+
*
|
|
4
|
+
* High-level functions for deploying processes to aliases
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { createProcess, pushProcess, createAlias, updateAlias } from './processes.js';
|
|
8
|
+
import { serializeProcess } from './edn-process.js';
|
|
9
|
+
import type { ProcessDefinition } from './types.js';
|
|
10
|
+
|
|
11
|
+
export interface DeployProcessOptions {
|
|
12
|
+
/** Process name */
|
|
13
|
+
process: string;
|
|
14
|
+
/** Target alias to deploy to */
|
|
15
|
+
alias: string;
|
|
16
|
+
/** Path to process.edn file (used for display only) */
|
|
17
|
+
path?: string;
|
|
18
|
+
/** Process definition to deploy */
|
|
19
|
+
processDefinition: ProcessDefinition;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface DeployProcessResult {
|
|
23
|
+
/** Whether a new process was created */
|
|
24
|
+
processCreated: boolean;
|
|
25
|
+
/** Process version number */
|
|
26
|
+
version: number;
|
|
27
|
+
/** Whether a new alias was created */
|
|
28
|
+
aliasCreated: boolean;
|
|
29
|
+
/** Alias name */
|
|
30
|
+
alias: string;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Deploys a process to an alias
|
|
35
|
+
*
|
|
36
|
+
* This high-level function handles the complete deployment workflow:
|
|
37
|
+
* 1. Creates the process if it doesn't exist
|
|
38
|
+
* 2. Pushes the process definition to create a new version
|
|
39
|
+
* 3. Creates or updates the alias to point to the new version
|
|
40
|
+
*
|
|
41
|
+
* @param apiKey - Sharetribe API key (optional, reads from auth file if not provided)
|
|
42
|
+
* @param marketplace - Marketplace ID
|
|
43
|
+
* @param options - Deployment options
|
|
44
|
+
* @returns Deployment result including version and alias information
|
|
45
|
+
*/
|
|
46
|
+
export async function deployProcess(
|
|
47
|
+
apiKey: string | undefined,
|
|
48
|
+
marketplace: string,
|
|
49
|
+
options: DeployProcessOptions
|
|
50
|
+
): Promise<DeployProcessResult> {
|
|
51
|
+
const { process, alias, processDefinition } = options;
|
|
52
|
+
|
|
53
|
+
// Serialize process definition to EDN format
|
|
54
|
+
const processEdn = serializeProcess(processDefinition);
|
|
55
|
+
|
|
56
|
+
// Step 1: Try to create the process (will fail if it already exists, which is fine)
|
|
57
|
+
let processCreated = false;
|
|
58
|
+
try {
|
|
59
|
+
await createProcess(apiKey, marketplace, process, processEdn);
|
|
60
|
+
processCreated = true;
|
|
61
|
+
} catch (error: any) {
|
|
62
|
+
// If process already exists, continue
|
|
63
|
+
if (error.code !== 'already-exists') {
|
|
64
|
+
throw error;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Step 2: Push the process to create a new version
|
|
69
|
+
const pushResult = await pushProcess(apiKey, marketplace, process, processEdn);
|
|
70
|
+
|
|
71
|
+
// Ensure we have a version number
|
|
72
|
+
if (pushResult.version === undefined) {
|
|
73
|
+
throw new Error('Failed to get version number from push result');
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Step 3: Create or update the alias
|
|
77
|
+
let aliasCreated = false;
|
|
78
|
+
try {
|
|
79
|
+
await createAlias(apiKey, marketplace, process, pushResult.version, alias);
|
|
80
|
+
aliasCreated = true;
|
|
81
|
+
} catch (error: any) {
|
|
82
|
+
// If alias already exists, update it
|
|
83
|
+
if (error.code === 'already-exists') {
|
|
84
|
+
await updateAlias(apiKey, marketplace, process, pushResult.version, alias);
|
|
85
|
+
} else {
|
|
86
|
+
throw error;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return {
|
|
91
|
+
processCreated,
|
|
92
|
+
version: pushResult.version,
|
|
93
|
+
aliasCreated,
|
|
94
|
+
alias,
|
|
95
|
+
};
|
|
96
|
+
}
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Process.edn file parser
|
|
3
|
+
*
|
|
4
|
+
* Parses transaction process definitions from EDN format
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import edn from 'jsedn';
|
|
8
|
+
import { readFileSync } from 'node:fs';
|
|
9
|
+
|
|
10
|
+
export interface ProcessState {
|
|
11
|
+
name: string;
|
|
12
|
+
in: string[];
|
|
13
|
+
out: string[];
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface ProcessTransition {
|
|
17
|
+
name: string;
|
|
18
|
+
from: string;
|
|
19
|
+
to: string;
|
|
20
|
+
actor: string;
|
|
21
|
+
privileged?: boolean;
|
|
22
|
+
actions?: Array<{ name: string; config?: unknown }>;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface ProcessNotification {
|
|
26
|
+
name: string;
|
|
27
|
+
on: string;
|
|
28
|
+
to: string;
|
|
29
|
+
template: string;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface ProcessDefinition {
|
|
33
|
+
name: string;
|
|
34
|
+
version?: number;
|
|
35
|
+
states: ProcessState[];
|
|
36
|
+
transitions: ProcessTransition[];
|
|
37
|
+
notifications: ProcessNotification[];
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Converts EDN keyword to string
|
|
42
|
+
*/
|
|
43
|
+
function ednKeywordToString(kw: unknown): string {
|
|
44
|
+
if (kw && typeof kw === 'object' && 'name' in kw) {
|
|
45
|
+
return (kw as { name: string }).name;
|
|
46
|
+
}
|
|
47
|
+
return String(kw);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Parses a process.edn file
|
|
52
|
+
*/
|
|
53
|
+
export function parseProcessFile(filePath: string): ProcessDefinition {
|
|
54
|
+
const content = readFileSync(filePath, 'utf-8');
|
|
55
|
+
const parsed = edn.parse(content);
|
|
56
|
+
|
|
57
|
+
// Extract process data from EDN map
|
|
58
|
+
const nameKw = edn.kw(':name');
|
|
59
|
+
const statesKw = edn.kw(':states');
|
|
60
|
+
const transitionsKw = edn.kw(':transitions');
|
|
61
|
+
const notificationsKw = edn.kw(':notifications');
|
|
62
|
+
|
|
63
|
+
const name = ednKeywordToString(parsed.at(nameKw));
|
|
64
|
+
const statesData = parsed.at(statesKw) || [];
|
|
65
|
+
const transitionsData = parsed.at(transitionsKw) || [];
|
|
66
|
+
const notificationsData = parsed.at(notificationsKw) || [];
|
|
67
|
+
|
|
68
|
+
// Parse states
|
|
69
|
+
const states: ProcessState[] = [];
|
|
70
|
+
if (Array.isArray(statesData)) {
|
|
71
|
+
for (const state of statesData) {
|
|
72
|
+
states.push({
|
|
73
|
+
name: ednKeywordToString(state.at(edn.kw(':name'))),
|
|
74
|
+
in: (state.at(edn.kw(':in')) || []).map(ednKeywordToString),
|
|
75
|
+
out: (state.at(edn.kw(':out')) || []).map(ednKeywordToString),
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Parse transitions
|
|
81
|
+
const transitions: ProcessTransition[] = [];
|
|
82
|
+
if (Array.isArray(transitionsData)) {
|
|
83
|
+
for (const transition of transitionsData) {
|
|
84
|
+
transitions.push({
|
|
85
|
+
name: ednKeywordToString(transition.at(edn.kw(':name'))),
|
|
86
|
+
from: ednKeywordToString(transition.at(edn.kw(':from'))),
|
|
87
|
+
to: ednKeywordToString(transition.at(edn.kw(':to'))),
|
|
88
|
+
actor: ednKeywordToString(transition.at(edn.kw(':actor'))),
|
|
89
|
+
privileged: transition.at(edn.kw(':privileged?')) || false,
|
|
90
|
+
actions: transition.at(edn.kw(':actions')) || [],
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Parse notifications
|
|
96
|
+
const notifications: ProcessNotification[] = [];
|
|
97
|
+
if (Array.isArray(notificationsData)) {
|
|
98
|
+
for (const notification of notificationsData) {
|
|
99
|
+
notifications.push({
|
|
100
|
+
name: ednKeywordToString(notification.at(edn.kw(':name'))),
|
|
101
|
+
on: ednKeywordToString(notification.at(edn.kw(':on'))),
|
|
102
|
+
to: ednKeywordToString(notification.at(edn.kw(':to'))),
|
|
103
|
+
template: ednKeywordToString(notification.at(edn.kw(':template'))),
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return {
|
|
109
|
+
name,
|
|
110
|
+
states,
|
|
111
|
+
transitions,
|
|
112
|
+
notifications,
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Serializes a process definition to EDN format
|
|
118
|
+
*/
|
|
119
|
+
export function serializeProcess(process: ProcessDefinition): string {
|
|
120
|
+
// For now, return a simplified EDN representation
|
|
121
|
+
// A full implementation would properly serialize to EDN format
|
|
122
|
+
return `{:name :${process.name}
|
|
123
|
+
:states [${process.states.map((s) => `{:name :${s.name} :in [${s.in.map((i) => `:${i}`).join(' ')}] :out [${s.out.map((o) => `:${o}`).join(' ')}]}`).join('\n ')}]
|
|
124
|
+
:transitions [${process.transitions.map((t) => `{:name :${t.name} :from :${t.from} :to :${t.to} :actor :${t.actor}}`).join('\n ')}]
|
|
125
|
+
:notifications [${process.notifications.map((n) => `{:name :${n.name} :on :${n.on} :to :${n.to} :template :${n.template}}`).join('\n ')}]}`;
|
|
126
|
+
}
|