@jay-framework/stack-client-runtime 0.16.0 → 0.16.2

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/dist/index.cjs CHANGED
@@ -332,19 +332,60 @@ let globalOptions = {};
332
332
  function setActionCallerOptions(options) {
333
333
  globalOptions = { ...globalOptions, ...options };
334
334
  }
335
- function createActionCaller(actionName, method = "POST") {
335
+ function buildFormData(input) {
336
+ const formData = new FormData();
337
+ const jsonFields = {};
338
+ for (const [key, value] of Object.entries(input)) {
339
+ if (value instanceof Blob) {
340
+ const name = value instanceof File ? value.name : `${key}.bin`;
341
+ formData.append(key, value, name);
342
+ } else if (Array.isArray(value) && value.some((v) => v instanceof Blob)) {
343
+ const nonFiles = [];
344
+ for (const item of value) {
345
+ if (item instanceof Blob) {
346
+ const name = item instanceof File ? item.name : `${key}.bin`;
347
+ formData.append(key, item, name);
348
+ } else {
349
+ nonFiles.push(item);
350
+ }
351
+ }
352
+ if (nonFiles.length > 0) {
353
+ jsonFields[key] = nonFiles;
354
+ }
355
+ } else {
356
+ jsonFields[key] = value;
357
+ }
358
+ }
359
+ if (Object.keys(jsonFields).length > 0) {
360
+ formData.append("_json", JSON.stringify(jsonFields));
361
+ }
362
+ return formData;
363
+ }
364
+ function hasFiles(input) {
365
+ if (typeof input !== "object" || input === null)
366
+ return false;
367
+ for (const value of Object.values(input)) {
368
+ if (value instanceof Blob)
369
+ return true;
370
+ if (Array.isArray(value) && value.some((v) => v instanceof Blob))
371
+ return true;
372
+ }
373
+ return false;
374
+ }
375
+ function createActionCaller(actionName, method = "POST", options) {
336
376
  return async (input) => {
337
377
  const baseUrl = globalOptions.baseUrl ?? "";
338
- const url = buildActionUrl(baseUrl, actionName, method, input);
378
+ const useFormData = options?.acceptsFiles && hasFiles(input);
379
+ const url = useFormData ? `${baseUrl}${ACTION_ENDPOINT_BASE}/${actionName}` : buildActionUrl(baseUrl, actionName, method, input);
339
380
  const fetchOptions = {
340
381
  method,
341
382
  headers: {
342
- "Content-Type": "application/json",
383
+ ...useFormData ? {} : { "Content-Type": "application/json" },
343
384
  ...globalOptions.headers
344
385
  }
345
386
  };
346
387
  if (method !== "GET") {
347
- fetchOptions.body = JSON.stringify(input);
388
+ fetchOptions.body = useFormData ? buildFormData(input) : JSON.stringify(input);
348
389
  }
349
390
  const timeout = globalOptions.timeout ?? 3e4;
350
391
  const controller = new AbortController();
@@ -402,12 +443,13 @@ function buildActionUrl(baseUrl, actionName, method, input) {
402
443
  }
403
444
  return fullUrl;
404
445
  }
405
- function createStreamCaller(actionName) {
446
+ function createStreamCaller(actionName, options) {
406
447
  return (input) => {
407
448
  return {
408
449
  [Symbol.asyncIterator]() {
409
450
  const baseUrl = globalOptions.baseUrl ?? "";
410
451
  const url = `${baseUrl}${ACTION_ENDPOINT_BASE}/${actionName}`;
452
+ const useFormData = options?.acceptsFiles && hasFiles(input);
411
453
  let reader = null;
412
454
  let buffer = "";
413
455
  let done = false;
@@ -417,10 +459,10 @@ function createStreamCaller(actionName) {
417
459
  const fetchPromise = fetch(url, {
418
460
  method: "POST",
419
461
  headers: {
420
- "Content-Type": "application/json",
462
+ ...useFormData ? {} : { "Content-Type": "application/json" },
421
463
  ...globalOptions.headers
422
464
  },
423
- body: JSON.stringify(input)
465
+ body: useFormData ? buildFormData(input) : JSON.stringify(input)
424
466
  }).then((response) => {
425
467
  if (!response.ok) {
426
468
  throw new ActionError(
package/dist/index.d.ts CHANGED
@@ -145,7 +145,14 @@ declare function setActionCallerOptions(options: ActionCallerOptions): void;
145
145
  * const addToCart = createActionCaller<{productId: string}, {cartCount: number}>('cart.addToCart', 'POST');
146
146
  * ```
147
147
  */
148
- declare function createActionCaller<Input, Output>(actionName: string, method?: HttpMethod): (input: Input) => Promise<Output>;
148
+ /**
149
+ * Options for action callers.
150
+ */
151
+ interface CreateActionCallerOptions {
152
+ /** Whether this action accepts file uploads (DL#131) */
153
+ acceptsFiles?: boolean;
154
+ }
155
+ declare function createActionCaller<Input, Output>(actionName: string, method?: HttpMethod, options?: CreateActionCallerOptions): (input: Input) => Promise<Output>;
149
156
  /**
150
157
  * Creates a client-side stream caller that makes an HTTP request and returns
151
158
  * an async iterable of chunks via NDJSON streaming.
@@ -165,6 +172,6 @@ declare function createActionCaller<Input, Output>(actionName: string, method?:
165
172
  * const checkInventory = createStreamCaller<void, { name: string }>('inventory.check');
166
173
  * ```
167
174
  */
168
- declare function createStreamCaller<Input, Chunk>(actionName: string): (input: Input) => AsyncIterable<Chunk>;
175
+ declare function createStreamCaller<Input, Chunk>(actionName: string, options?: CreateActionCallerOptions): (input: Input) => AsyncIterable<Chunk>;
169
176
 
170
- export { type ActionCallerOptions, ActionError, type CompositePart, HEADLESS_INSTANCES, type HeadlessComponentDef, type HeadlessInstancesData, type HttpMethod, createActionCaller, createStreamCaller, hydrateCompositeJayComponent, makeCompositeJayComponent, makeHeadlessInstanceComponent, setActionCallerOptions };
177
+ export { type ActionCallerOptions, ActionError, type CompositePart, type CreateActionCallerOptions, HEADLESS_INSTANCES, type HeadlessComponentDef, type HeadlessInstancesData, type HttpMethod, createActionCaller, createStreamCaller, hydrateCompositeJayComponent, makeCompositeJayComponent, makeHeadlessInstanceComponent, setActionCallerOptions };
package/dist/index.js CHANGED
@@ -330,19 +330,60 @@ let globalOptions = {};
330
330
  function setActionCallerOptions(options) {
331
331
  globalOptions = { ...globalOptions, ...options };
332
332
  }
333
- function createActionCaller(actionName, method = "POST") {
333
+ function buildFormData(input) {
334
+ const formData = new FormData();
335
+ const jsonFields = {};
336
+ for (const [key, value] of Object.entries(input)) {
337
+ if (value instanceof Blob) {
338
+ const name = value instanceof File ? value.name : `${key}.bin`;
339
+ formData.append(key, value, name);
340
+ } else if (Array.isArray(value) && value.some((v) => v instanceof Blob)) {
341
+ const nonFiles = [];
342
+ for (const item of value) {
343
+ if (item instanceof Blob) {
344
+ const name = item instanceof File ? item.name : `${key}.bin`;
345
+ formData.append(key, item, name);
346
+ } else {
347
+ nonFiles.push(item);
348
+ }
349
+ }
350
+ if (nonFiles.length > 0) {
351
+ jsonFields[key] = nonFiles;
352
+ }
353
+ } else {
354
+ jsonFields[key] = value;
355
+ }
356
+ }
357
+ if (Object.keys(jsonFields).length > 0) {
358
+ formData.append("_json", JSON.stringify(jsonFields));
359
+ }
360
+ return formData;
361
+ }
362
+ function hasFiles(input) {
363
+ if (typeof input !== "object" || input === null)
364
+ return false;
365
+ for (const value of Object.values(input)) {
366
+ if (value instanceof Blob)
367
+ return true;
368
+ if (Array.isArray(value) && value.some((v) => v instanceof Blob))
369
+ return true;
370
+ }
371
+ return false;
372
+ }
373
+ function createActionCaller(actionName, method = "POST", options) {
334
374
  return async (input) => {
335
375
  const baseUrl = globalOptions.baseUrl ?? "";
336
- const url = buildActionUrl(baseUrl, actionName, method, input);
376
+ const useFormData = options?.acceptsFiles && hasFiles(input);
377
+ const url = useFormData ? `${baseUrl}${ACTION_ENDPOINT_BASE}/${actionName}` : buildActionUrl(baseUrl, actionName, method, input);
337
378
  const fetchOptions = {
338
379
  method,
339
380
  headers: {
340
- "Content-Type": "application/json",
381
+ ...useFormData ? {} : { "Content-Type": "application/json" },
341
382
  ...globalOptions.headers
342
383
  }
343
384
  };
344
385
  if (method !== "GET") {
345
- fetchOptions.body = JSON.stringify(input);
386
+ fetchOptions.body = useFormData ? buildFormData(input) : JSON.stringify(input);
346
387
  }
347
388
  const timeout = globalOptions.timeout ?? 3e4;
348
389
  const controller = new AbortController();
@@ -400,12 +441,13 @@ function buildActionUrl(baseUrl, actionName, method, input) {
400
441
  }
401
442
  return fullUrl;
402
443
  }
403
- function createStreamCaller(actionName) {
444
+ function createStreamCaller(actionName, options) {
404
445
  return (input) => {
405
446
  return {
406
447
  [Symbol.asyncIterator]() {
407
448
  const baseUrl = globalOptions.baseUrl ?? "";
408
449
  const url = `${baseUrl}${ACTION_ENDPOINT_BASE}/${actionName}`;
450
+ const useFormData = options?.acceptsFiles && hasFiles(input);
409
451
  let reader = null;
410
452
  let buffer = "";
411
453
  let done = false;
@@ -415,10 +457,10 @@ function createStreamCaller(actionName) {
415
457
  const fetchPromise = fetch(url, {
416
458
  method: "POST",
417
459
  headers: {
418
- "Content-Type": "application/json",
460
+ ...useFormData ? {} : { "Content-Type": "application/json" },
419
461
  ...globalOptions.headers
420
462
  },
421
- body: JSON.stringify(input)
463
+ body: useFormData ? buildFormData(input) : JSON.stringify(input)
422
464
  }).then((response) => {
423
465
  if (!response.ok) {
424
466
  throw new ActionError(
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jay-framework/stack-client-runtime",
3
- "version": "0.16.0",
3
+ "version": "0.16.2",
4
4
  "type": "module",
5
5
  "license": "Apache-2.0",
6
6
  "main": "dist/index.js",
@@ -27,15 +27,15 @@
27
27
  "test:watch": "vitest"
28
28
  },
29
29
  "dependencies": {
30
- "@jay-framework/component": "^0.16.0",
31
- "@jay-framework/fullstack-component": "^0.16.0",
32
- "@jay-framework/runtime": "^0.16.0",
33
- "@jay-framework/runtime-automation": "^0.16.0",
34
- "@jay-framework/view-state-merge": "^0.16.0"
30
+ "@jay-framework/component": "^0.16.2",
31
+ "@jay-framework/fullstack-component": "^0.16.2",
32
+ "@jay-framework/runtime": "^0.16.2",
33
+ "@jay-framework/runtime-automation": "^0.16.2",
34
+ "@jay-framework/view-state-merge": "^0.16.2"
35
35
  },
36
36
  "devDependencies": {
37
- "@jay-framework/dev-environment": "^0.16.0",
38
- "@jay-framework/jay-cli": "^0.16.0",
37
+ "@jay-framework/dev-environment": "^0.16.2",
38
+ "@jay-framework/jay-cli": "^0.16.2",
39
39
  "@types/express": "^5.0.2",
40
40
  "@types/node": "^22.15.21",
41
41
  "nodemon": "^3.0.3",