sharetribe-flex-build-sdk 1.15.1 → 1.16.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sharetribe-flex-build-sdk",
3
- "version": "1.15.1",
3
+ "version": "1.16.0",
4
4
  "description": "SDK for building and managing Sharetribe Flex transaction processes",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
package/src/assets.ts CHANGED
@@ -22,6 +22,10 @@ export interface PushAssetsResult {
22
22
  assets: Array<{ path: string; contentHash: string }>;
23
23
  }
24
24
 
25
+ export interface StageAssetResult {
26
+ stagingId: string;
27
+ }
28
+
25
29
  /**
26
30
  * Pulls assets from remote
27
31
  *
@@ -67,6 +71,34 @@ export async function pullAssets(
67
71
  };
68
72
  }
69
73
 
74
+ /**
75
+ * Stages an asset for upload
76
+ *
77
+ * @param apiKey - Sharetribe API key (optional, reads from auth file if not provided)
78
+ * @param marketplace - Marketplace ID
79
+ * @param fileData - File data as Buffer
80
+ * @param filename - Filename
81
+ * @returns Staging ID
82
+ */
83
+ export async function stageAsset(
84
+ apiKey: string | undefined,
85
+ marketplace: string,
86
+ fileData: Buffer,
87
+ filename: string
88
+ ): Promise<StageAssetResult> {
89
+ const fields: MultipartField[] = [
90
+ { name: 'file', value: fileData, filename },
91
+ ];
92
+
93
+ const response = await apiPostMultipart<{
94
+ data: { 'staging-id': string };
95
+ }>(apiKey, '/assets/stage', { marketplace }, fields);
96
+
97
+ return {
98
+ stagingId: response.data['staging-id'],
99
+ };
100
+ }
101
+
70
102
  /**
71
103
  * Pushes assets to remote
72
104
  *
@@ -84,6 +116,8 @@ export async function pushAssets(
84
116
  path: string;
85
117
  op: 'upsert' | 'delete';
86
118
  data?: Buffer;
119
+ stagingId?: string;
120
+ filename?: string;
87
121
  }>
88
122
  ): Promise<PushAssetsResult> {
89
123
  const fields: MultipartField[] = [
@@ -94,8 +128,13 @@ export async function pushAssets(
94
128
  const op = operations[i];
95
129
  fields.push({ name: `path-${i}`, value: op.path });
96
130
  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 });
131
+ if (op.op === 'upsert') {
132
+ if (op.stagingId) {
133
+ fields.push({ name: `staging-id-${i}`, value: String(op.stagingId) });
134
+ } else if (op.data) {
135
+ const fname = op.filename || op.path;
136
+ fields.push({ name: `data-raw-${i}`, value: op.data, filename: fname });
137
+ }
99
138
  }
100
139
  }
101
140
 
package/src/index.ts CHANGED
@@ -74,9 +74,11 @@ export {
74
74
  export {
75
75
  pullAssets,
76
76
  pushAssets,
77
+ stageAsset,
77
78
  type Asset,
78
79
  type PullAssetsResult,
79
80
  type PushAssetsResult,
81
+ type StageAssetResult,
80
82
  } from './assets.js';
81
83
 
82
84
  // Export notification management functions
@@ -0,0 +1,308 @@
1
+ /**
2
+ * Tests for asset management SDK functions
3
+ */
4
+
5
+ import { describe, it, expect, vi, beforeEach } from 'vitest';
6
+ import { stageAsset, pushAssets } from '../src/assets.js';
7
+ import * as client from '../src/api/client.js';
8
+
9
+ // Mock the API client
10
+ vi.mock('../src/api/client.js', () => ({
11
+ apiPostMultipart: vi.fn(),
12
+ }));
13
+
14
+ describe('stageAsset', () => {
15
+ beforeEach(() => {
16
+ vi.clearAllMocks();
17
+ });
18
+
19
+ it('should stage an asset and return staging ID', async () => {
20
+ const mockApiPostMultipart = vi.mocked(client.apiPostMultipart);
21
+ mockApiPostMultipart.mockResolvedValue({
22
+ data: {
23
+ 'staging-id': 'staging-12345',
24
+ },
25
+ });
26
+
27
+ const result = await stageAsset(
28
+ 'test-api-key',
29
+ 'test-marketplace',
30
+ Buffer.from('image data'),
31
+ 'test.png'
32
+ );
33
+
34
+ expect(mockApiPostMultipart).toHaveBeenCalledWith(
35
+ 'test-api-key',
36
+ '/assets/stage',
37
+ { marketplace: 'test-marketplace' },
38
+ [
39
+ {
40
+ name: 'file',
41
+ value: Buffer.from('image data'),
42
+ filename: 'test.png',
43
+ },
44
+ ]
45
+ );
46
+
47
+ expect(result.stagingId).toBe('staging-12345');
48
+ });
49
+
50
+ it('should work without API key (reads from auth file)', async () => {
51
+ const mockApiPostMultipart = vi.mocked(client.apiPostMultipart);
52
+ mockApiPostMultipart.mockResolvedValue({
53
+ data: {
54
+ 'staging-id': 'staging-67890',
55
+ },
56
+ });
57
+
58
+ const result = await stageAsset(
59
+ undefined,
60
+ 'test-marketplace',
61
+ Buffer.from('file data'),
62
+ 'document.pdf'
63
+ );
64
+
65
+ expect(mockApiPostMultipart).toHaveBeenCalledWith(
66
+ undefined,
67
+ '/assets/stage',
68
+ { marketplace: 'test-marketplace' },
69
+ expect.arrayContaining([
70
+ expect.objectContaining({
71
+ name: 'file',
72
+ filename: 'document.pdf',
73
+ }),
74
+ ])
75
+ );
76
+
77
+ expect(result.stagingId).toBe('staging-67890');
78
+ });
79
+ });
80
+
81
+ describe('pushAssets', () => {
82
+ beforeEach(() => {
83
+ vi.clearAllMocks();
84
+ });
85
+
86
+ it('should push assets with staging-id when provided', async () => {
87
+ const mockApiPostMultipart = vi.mocked(client.apiPostMultipart);
88
+ mockApiPostMultipart.mockResolvedValue({
89
+ data: {
90
+ version: 'v2',
91
+ 'asset-meta': {
92
+ assets: [
93
+ { path: 'test.png', 'content-hash': 'hash123' },
94
+ ],
95
+ },
96
+ },
97
+ });
98
+
99
+ const result = await pushAssets(
100
+ 'test-api-key',
101
+ 'test-marketplace',
102
+ 'v1',
103
+ [
104
+ {
105
+ path: 'test.png',
106
+ op: 'upsert',
107
+ stagingId: 'staging-123',
108
+ },
109
+ ]
110
+ );
111
+
112
+ expect(mockApiPostMultipart).toHaveBeenCalledWith(
113
+ 'test-api-key',
114
+ '/assets/push',
115
+ { marketplace: 'test-marketplace' },
116
+ expect.arrayContaining([
117
+ { name: 'current-version', value: 'v1' },
118
+ { name: 'path-0', value: 'test.png' },
119
+ { name: 'op-0', value: 'upsert' },
120
+ { name: 'staging-id-0', value: 'staging-123' },
121
+ ])
122
+ );
123
+
124
+ expect(result.version).toBe('v2');
125
+ expect(result.assets).toHaveLength(1);
126
+ expect(result.assets[0].path).toBe('test.png');
127
+ expect(result.assets[0].contentHash).toBe('hash123');
128
+ });
129
+
130
+ it('should push assets with data-raw when staging-id is not provided', async () => {
131
+ const mockApiPostMultipart = vi.mocked(client.apiPostMultipart);
132
+ mockApiPostMultipart.mockResolvedValue({
133
+ data: {
134
+ version: 'v2',
135
+ 'asset-meta': {
136
+ assets: [
137
+ { path: 'config.json', 'content-hash': 'hash456' },
138
+ ],
139
+ },
140
+ },
141
+ });
142
+
143
+ const fileData = Buffer.from('{"test": "data"}');
144
+
145
+ const result = await pushAssets(
146
+ 'test-api-key',
147
+ 'test-marketplace',
148
+ 'v1',
149
+ [
150
+ {
151
+ path: 'config.json',
152
+ op: 'upsert',
153
+ data: fileData,
154
+ filename: 'config.json',
155
+ },
156
+ ]
157
+ );
158
+
159
+ expect(mockApiPostMultipart).toHaveBeenCalledWith(
160
+ 'test-api-key',
161
+ '/assets/push',
162
+ { marketplace: 'test-marketplace' },
163
+ expect.arrayContaining([
164
+ { name: 'current-version', value: 'v1' },
165
+ { name: 'path-0', value: 'config.json' },
166
+ { name: 'op-0', value: 'upsert' },
167
+ expect.objectContaining({
168
+ name: 'data-raw-0',
169
+ value: fileData,
170
+ filename: 'config.json',
171
+ }),
172
+ ])
173
+ );
174
+
175
+ expect(result.version).toBe('v2');
176
+ });
177
+
178
+ it('should handle delete operations', async () => {
179
+ const mockApiPostMultipart = vi.mocked(client.apiPostMultipart);
180
+ mockApiPostMultipart.mockResolvedValue({
181
+ data: {
182
+ version: 'v2',
183
+ 'asset-meta': {
184
+ assets: [],
185
+ },
186
+ },
187
+ });
188
+
189
+ const result = await pushAssets(
190
+ 'test-api-key',
191
+ 'test-marketplace',
192
+ 'v1',
193
+ [
194
+ {
195
+ path: 'old-file.txt',
196
+ op: 'delete',
197
+ },
198
+ ]
199
+ );
200
+
201
+ expect(mockApiPostMultipart).toHaveBeenCalledWith(
202
+ 'test-api-key',
203
+ '/assets/push',
204
+ { marketplace: 'test-marketplace' },
205
+ expect.arrayContaining([
206
+ { name: 'current-version', value: 'v1' },
207
+ { name: 'path-0', value: 'old-file.txt' },
208
+ { name: 'op-0', value: 'delete' },
209
+ ])
210
+ );
211
+
212
+ // Should not include data-raw or staging-id for delete operations
213
+ const callArgs = mockApiPostMultipart.mock.calls[0];
214
+ const fields = callArgs[3] as Array<{ name: string }>;
215
+ const fieldNames = fields.map(f => f.name);
216
+ expect(fieldNames).not.toContain('data-raw-0');
217
+ expect(fieldNames).not.toContain('staging-id-0');
218
+
219
+ expect(result.version).toBe('v2');
220
+ });
221
+
222
+ it('should handle multiple operations', async () => {
223
+ const mockApiPostMultipart = vi.mocked(client.apiPostMultipart);
224
+ mockApiPostMultipart.mockResolvedValue({
225
+ data: {
226
+ version: 'v3',
227
+ 'asset-meta': {
228
+ assets: [
229
+ { path: 'new.png', 'content-hash': 'hash1' },
230
+ { path: 'config.json', 'content-hash': 'hash2' },
231
+ ],
232
+ },
233
+ },
234
+ });
235
+
236
+ const result = await pushAssets(
237
+ 'test-api-key',
238
+ 'test-marketplace',
239
+ 'v2',
240
+ [
241
+ {
242
+ path: 'new.png',
243
+ op: 'upsert',
244
+ stagingId: 'staging-1',
245
+ },
246
+ {
247
+ path: 'config.json',
248
+ op: 'upsert',
249
+ data: Buffer.from('{}'),
250
+ filename: 'config.json',
251
+ },
252
+ {
253
+ path: 'old.txt',
254
+ op: 'delete',
255
+ },
256
+ ]
257
+ );
258
+
259
+ expect(mockApiPostMultipart).toHaveBeenCalledWith(
260
+ 'test-api-key',
261
+ '/assets/push',
262
+ { marketplace: 'test-marketplace' },
263
+ expect.arrayContaining([
264
+ { name: 'current-version', value: 'v2' },
265
+ { name: 'path-0', value: 'new.png' },
266
+ { name: 'op-0', value: 'upsert' },
267
+ { name: 'staging-id-0', value: 'staging-1' },
268
+ { name: 'path-1', value: 'config.json' },
269
+ { name: 'op-1', value: 'upsert' },
270
+ { name: 'path-2', value: 'old.txt' },
271
+ { name: 'op-2', value: 'delete' },
272
+ ])
273
+ );
274
+
275
+ expect(result.version).toBe('v3');
276
+ expect(result.assets).toHaveLength(2);
277
+ });
278
+
279
+ it('should convert staging-id to string', async () => {
280
+ const mockApiPostMultipart = vi.mocked(client.apiPostMultipart);
281
+ mockApiPostMultipart.mockResolvedValue({
282
+ data: {
283
+ version: 'v2',
284
+ 'asset-meta': {
285
+ assets: [],
286
+ },
287
+ },
288
+ });
289
+
290
+ await pushAssets(
291
+ 'test-api-key',
292
+ 'test-marketplace',
293
+ 'v1',
294
+ [
295
+ {
296
+ path: 'test.png',
297
+ op: 'upsert',
298
+ stagingId: 12345 as any, // Test that it converts to string
299
+ },
300
+ ]
301
+ );
302
+
303
+ const callArgs = mockApiPostMultipart.mock.calls[0];
304
+ const fields = callArgs[3] as Array<{ name: string; value: string }>;
305
+ const stagingIdField = fields.find(f => f.name === 'staging-id-0');
306
+ expect(stagingIdField?.value).toBe('12345');
307
+ });
308
+ });