@colixsystems/widget-sdk 0.220.0 → 0.221.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 CHANGED
@@ -84,7 +84,22 @@ See the design reference for the full architecture: [`docs/architecture/widget-m
84
84
 
85
85
  ## Status
86
86
 
87
- `v0.220.0` — pre-publish. The package surface (types, function names, export paths) is the v1 contract; runtime behaviour for some hooks is stubbed (each hook documents what's wired and what isn't). It is **not yet published to npm**.
87
+ `v0.221.0` — pre-publish. The package surface (types, function names, export paths) is the v1 contract; runtime behaviour for some hooks is stubbed (each hook documents what's wired and what isn't). It is **not yet published to npm**.
88
+
89
+ ### What's new in 0.221.0 (contract 1.177.0)
90
+
91
+ **A row's file goes in the row's record space, and `upload()` names the record per call.** A file a widget keeps in a `FILE` column belongs in that row's `record` space: exactly the people who may read the row may read the file, so a private table's attachments stay private, and a table that lets everyone read still serves them to logged-out visitors. A `public` file, by contrast, is readable by anyone holding its link whatever the table's permissions say. A row must exist before a file can be attached to it, and a create form only learns the new row's id inside its submit handler, after the hook rendered. So `upload(file, { recordId })` now takes the record per call, the way it already took `folderId`:
92
+
93
+ ```js
94
+ const { create, update } = useDatastoreMutation(tableId);
95
+ const { upload } = useFilestoreUpload({ spaceType: "record" });
96
+
97
+ const row = await create({ Title: title }); // without the file
98
+ const stored = await upload(file, { recordId: row.id }); // into that row's space
99
+ await update(row.id, { Receipt: stored.id });
100
+ ```
101
+
102
+ A per-call `recordId` wins over the hook's; outside a `record` space it is ignored. A record upload that names no record now rejects with `useFilestoreUpload: a record space needs a recordId` instead of the misleading `Sign in to upload`. A logged-out visitor cannot upload into a record space, so ask them to sign in to attach a file. `useCamera()` and `useImageEditor()` now point at `useFilestoreUpload` for the upload: their descriptions named `ctx.assets.upload`, which widget code cannot reach. Additive.
88
103
 
89
104
  ### What's new in 0.220.0 (contract 1.176.0)
90
105
 
@@ -1496,9 +1511,7 @@ const { edit } = useImageEditor();
1496
1511
  const shot = await capture();
1497
1512
  const small = await edit(shot.uri, [{ resize: { width: 800 } }], { format: "jpeg", compress: 0.8 });
1498
1513
 
1499
- const fd = new FormData();
1500
- fd.append("file", small.file);
1501
- await ctx.assets.upload(fd);
1514
+ await upload(small.file); // upload from useFilestoreUpload()
1502
1515
  ```
1503
1516
 
1504
1517
  `edit(uri, actions, options?)` applies `actions` in order and resolves the SAME normalised asset shape `useCamera()` yields, so capture → edit → upload is one code path on both hosts. Actions are `{ resize: { width?, height? } }`, `{ crop: { originX, originY, width, height } }`, `{ rotate: degrees }` and `{ flip: "horizontal" | "vertical" }`; output is `{ format: "jpeg" | "png" | "webp", compress, base64? }`.
@@ -1712,12 +1725,10 @@ When you take the `useCanWrite`-only route, omit the USER column for a guest (`i
1712
1725
 
1713
1726
  **Dismissing the picker resolves `null`, not an error.** Backing out is the most common outcome, so it is deliberately not a rejection — your happy path needs no `try/catch`. A genuine failure (permission refused, no host broker) rejects with a structured `CameraError` (new named export) carrying a stable `.code` (`PERMISSION_DENIED` / `UNSUPPORTED` / `INTERNAL`).
1714
1727
 
1715
- **One upload path on both hosts.** The resolved asset is normalised to `{ uri, name, mimeType, width, height, size, file }`, where `uri` is directly displayable (`<Image source={{ uri }} />`) and `file` is already the right upload part for the platform — a browser `File` on web, expo-file-system's `File` on native. (Never React Native's legacy `{ uri, name, type }` part: the native host streams the file off disk, and Expo's fetch rejects that part outright anyway. Do not branch on the part's type — an expo-file-system `File` is NOT `instanceof Blob` at runtime.) So the same three lines work everywhere:
1728
+ **One upload path on both hosts.** The resolved asset is normalised to `{ uri, name, mimeType, width, height, size, file }`, where `uri` is directly displayable (`<Image source={{ uri }} />`) and `file` is already the right upload part for the platform — a browser `File` on web, expo-file-system's `File` on native. (Never React Native's legacy `{ uri, name, type }` part: the native host streams the file off disk, and Expo's fetch rejects that part outright anyway. Do not branch on the part's type — an expo-file-system `File` is NOT `instanceof Blob` at runtime.) So the same line works everywhere:
1716
1729
 
1717
1730
  ```js
1718
- const fd = new FormData();
1719
- fd.append("file", asset.file);
1720
- await ctx.assets.upload(fd);
1731
+ await upload(asset.file); // upload from useFilestoreUpload()
1721
1732
  ```
1722
1733
 
1723
1734
  `options` (`{ allowsEditing, quality }`) are **hints**: the Expo export applies both, the web camera applies `quality` only — so never depend on a cropped result. `reset()` clears the asset and releases it (on web that revokes the blob URL, which otherwise leaks for the life of the document). **Gate your camera button on `supported`** — a host that brokers no camera reports `false` rather than throwing. The web Player brokers it via `getUserMedia` — a live preview with a shutter — on every web host: it is the only camera a DESKTOP browser will open, and a phone reaches the same preview rather than its own camera app. It falls back to a file input (`capture="environment"`) only where getUserMedia is absent up front: an insecure origin or an older browser. Once the permission prompt has been shown the user gesture is spent, so a camera that then fails to open rejects (`PERMISSION_DENIED` if refused, `UNSUPPORTED` if there is no usable device) rather than falling back to a file dialog the browser would refuse to open. The Expo export uses `expo-image-picker`, whose config plugin declares the camera and photo-library permissions the runtime needs.
package/dist/contract.cjs CHANGED
@@ -1369,7 +1369,7 @@ const HOOKS = [
1369
1369
  "from the host context, builds a multipart FormData with the " +
1370
1370
  "snake_case fields the backend reads verbatim (`space_type`, " +
1371
1371
  "`owner_id`, `folder_id`, plus the binary `file`), and POSTs through " +
1372
- "ctx.filestore.files.upload. `upload(file, { folderId?, compress?, fileName? })` " +
1372
+ "ctx.filestore.files.upload. `upload(file, { folderId?, recordId?, compress?, fileName? })` " +
1373
1373
  "resolves to the created file row or throws the wire error. `file` must be a BLOB — a browser File on web, " +
1374
1374
  "expo-file-system's File on native (what FilePicker / useCamera / useImageEditor hand back); Expo's fetch rejects " +
1375
1375
  "React Native's legacy { uri, name, type } part with \"Unsupported FormDataPart implementation\". The stored name " +
@@ -1387,13 +1387,20 @@ const HOOKS = [
1387
1387
  "ORIGINAL matters (a document archive, a photo the user re-downloads, " +
1388
1388
  "anything with an exact-bytes requirement). " +
1389
1389
  "ALWAYS pass spaceType explicitly — omitting it falls back to " +
1390
- "'project', which is unreadable by a logged-out visitor. Choose it by " +
1391
- "who must SEE the file: 'public' for content the app displays to " +
1392
- "anyone browsing (product, catalogue, gallery, cover images), " +
1393
- "'project' for content restricted to signed-in workspace users, " +
1394
- "'personal' for a file private to the uploading app user.",
1390
+ "'project', which is unreadable by a logged-out visitor. A file that " +
1391
+ "belongs to a datastore row — anything stored in a FILE column — goes " +
1392
+ "in that row's 'record' space, so exactly the people who may read the " +
1393
+ "row may read the file (logged-out visitors too, when the table lets " +
1394
+ "everyone read). A file can only be attached to a row that exists: " +
1395
+ "create the row, call upload(file, { recordId: row.id }) — the per-call " +
1396
+ "recordId names the row the hook could not know at render — then write " +
1397
+ "the returned id into the FILE column with update(). A logged-out " +
1398
+ "visitor cannot upload into a record space. For a file that belongs to " +
1399
+ "no row: 'public' for content the app shows to anyone browsing, " +
1400
+ "'project' for signed-in workspace users only, 'personal' for a file " +
1401
+ "private to the uploading app user.",
1395
1402
  returnShape: {
1396
- upload: "(file, { folderId?, compress?, fileName? }) => Promise<FilestoreFile>",
1403
+ upload: "(file, { folderId?, recordId?, compress?, fileName? }) => Promise<FilestoreFile>",
1397
1404
  uploading: "boolean",
1398
1405
  error: "Error | null",
1399
1406
  lastUploaded: "FilestoreFile | null",
@@ -2818,8 +2825,8 @@ const HOOKS = [
2818
2825
  "common case and is deliberately NOT an error, so no try/catch is needed on the happy path. They reject with a " +
2819
2826
  "CameraError whose .code is one of PERMISSION_DENIED | UNSUPPORTED | INTERNAL. `asset.file` is already the right " +
2820
2827
  "upload part for the host — a BLOB on both (a File on web, expo-file-system's File on native), because Expo's fetch " +
2821
- "implements the web FormData spec and rejects React Native's legacy { uri, name, type } part outright: append it to a FormData as `file` and pass " +
2822
- "that to ctx.assets.upload(fd) — one code path on both platforms. reset() clears the asset and releases it. Check " +
2828
+ "implements the web FormData spec and rejects React Native's legacy { uri, name, type } part outright: pass it " +
2829
+ "straight to upload(asset.file) from useFilestoreUpload — one code path on both platforms. reset() clears the asset and releases it. Check " +
2823
2830
  "`supported` before rendering a camera button. options: { allowsEditing, quality } are HINTS the host honours where it " +
2824
2831
  "can — the Expo export applies both, the web camera applies quality only, so never depend on a cropped result. " +
2825
2832
  "Behaviour is otherwise identical on web (a getUserMedia preview on EVERY host, phones included; a file input " +
@@ -2885,7 +2892,7 @@ const HOOKS = [
2885
2892
  "Editing is IMPERATIVE — call edit() from an event handler, never during render. " +
2886
2893
  "edit(uri, actions, options?) applies `actions` IN ORDER and resolves the SAME normalised asset shape useCamera() " +
2887
2894
  "yields — { uri, name, mimeType, width, height, size, base64?, file } — so capture -> edit -> upload is ONE code path: " +
2888
- "append result.file to a FormData as `file` and pass it to ctx.assets.upload(fd). " +
2895
+ "pass result.file to upload() from useFilestoreUpload. " +
2889
2896
  "Each action entry carries exactly one of { resize: { width?, height? } } (aspect preserved when only one is given), " +
2890
2897
  "{ crop: { originX, originY, width, height } }, { rotate: degrees } (positive is clockwise), or " +
2891
2898
  "{ flip: \"horizontal\" | \"vertical\" }. A crop rect must lie INSIDE the surface the earlier actions produced, and " +
@@ -3022,7 +3029,7 @@ const PRIMITIVES = [
3022
3029
  },
3023
3030
  // sc-1378 — `<FilePicker accept onPick>` lets a widget surface a native
3024
3031
  // file-picker on the web Player so end users can upload files to the
3025
- // backend (paired with useFilestoreUpload or useAssets.upload). Web wraps
3032
+ // backend (paired with useFilestoreUpload). Web wraps
3026
3033
  // a hidden DOM `<input type="file">`; native renders a disabled trigger
3027
3034
  // and exposes `FilePicker.isSupported = false` until a vetted Expo
3028
3035
  // picker pin lands.
@@ -5856,7 +5863,12 @@ const CONTRACT = deepFreeze({
5856
5863
  // a widget never receives it. `account`'s photo actions are now
5857
5864
  // optional members, and `user.email` is documented as nullable: an
5858
5865
  // account without an email has none.
5859
- version: "1.176.0",
5866
+ // 1.177.0: additive (sc-10937) — `useFilestoreUpload().upload(file,
5867
+ // { recordId })` names the record per call, so a widget can create a row
5868
+ // and attach to it in one handler. A FILE column's file goes in its
5869
+ // row's record space; useCamera / useImageEditor point at
5870
+ // useFilestoreUpload, since widget code cannot reach ctx.assets.
5871
+ version: "1.177.0",
5860
5872
  sharedTranslationKeys: SHARED_TRANSLATION_KEYS,
5861
5873
  hooks: HOOKS,
5862
5874
  primitives: PRIMITIVES,
package/dist/contract.js CHANGED
@@ -1369,7 +1369,7 @@ const HOOKS = [
1369
1369
  "from the host context, builds a multipart FormData with the " +
1370
1370
  "snake_case fields the backend reads verbatim (`space_type`, " +
1371
1371
  "`owner_id`, `folder_id`, plus the binary `file`), and POSTs through " +
1372
- "ctx.filestore.files.upload. `upload(file, { folderId?, compress?, fileName? })` " +
1372
+ "ctx.filestore.files.upload. `upload(file, { folderId?, recordId?, compress?, fileName? })` " +
1373
1373
  "resolves to the created file row or throws the wire error. `file` must be a BLOB — a browser File on web, " +
1374
1374
  "expo-file-system's File on native (what FilePicker / useCamera / useImageEditor hand back); Expo's fetch rejects " +
1375
1375
  "React Native's legacy { uri, name, type } part with \"Unsupported FormDataPart implementation\". The stored name " +
@@ -1387,13 +1387,20 @@ const HOOKS = [
1387
1387
  "ORIGINAL matters (a document archive, a photo the user re-downloads, " +
1388
1388
  "anything with an exact-bytes requirement). " +
1389
1389
  "ALWAYS pass spaceType explicitly — omitting it falls back to " +
1390
- "'project', which is unreadable by a logged-out visitor. Choose it by " +
1391
- "who must SEE the file: 'public' for content the app displays to " +
1392
- "anyone browsing (product, catalogue, gallery, cover images), " +
1393
- "'project' for content restricted to signed-in workspace users, " +
1394
- "'personal' for a file private to the uploading app user.",
1390
+ "'project', which is unreadable by a logged-out visitor. A file that " +
1391
+ "belongs to a datastore row — anything stored in a FILE column — goes " +
1392
+ "in that row's 'record' space, so exactly the people who may read the " +
1393
+ "row may read the file (logged-out visitors too, when the table lets " +
1394
+ "everyone read). A file can only be attached to a row that exists: " +
1395
+ "create the row, call upload(file, { recordId: row.id }) — the per-call " +
1396
+ "recordId names the row the hook could not know at render — then write " +
1397
+ "the returned id into the FILE column with update(). A logged-out " +
1398
+ "visitor cannot upload into a record space. For a file that belongs to " +
1399
+ "no row: 'public' for content the app shows to anyone browsing, " +
1400
+ "'project' for signed-in workspace users only, 'personal' for a file " +
1401
+ "private to the uploading app user.",
1395
1402
  returnShape: {
1396
- upload: "(file, { folderId?, compress?, fileName? }) => Promise<FilestoreFile>",
1403
+ upload: "(file, { folderId?, recordId?, compress?, fileName? }) => Promise<FilestoreFile>",
1397
1404
  uploading: "boolean",
1398
1405
  error: "Error | null",
1399
1406
  lastUploaded: "FilestoreFile | null",
@@ -2818,8 +2825,8 @@ const HOOKS = [
2818
2825
  "common case and is deliberately NOT an error, so no try/catch is needed on the happy path. They reject with a " +
2819
2826
  "CameraError whose .code is one of PERMISSION_DENIED | UNSUPPORTED | INTERNAL. `asset.file` is already the right " +
2820
2827
  "upload part for the host — a BLOB on both (a File on web, expo-file-system's File on native), because Expo's fetch " +
2821
- "implements the web FormData spec and rejects React Native's legacy { uri, name, type } part outright: append it to a FormData as `file` and pass " +
2822
- "that to ctx.assets.upload(fd) — one code path on both platforms. reset() clears the asset and releases it. Check " +
2828
+ "implements the web FormData spec and rejects React Native's legacy { uri, name, type } part outright: pass it " +
2829
+ "straight to upload(asset.file) from useFilestoreUpload — one code path on both platforms. reset() clears the asset and releases it. Check " +
2823
2830
  "`supported` before rendering a camera button. options: { allowsEditing, quality } are HINTS the host honours where it " +
2824
2831
  "can — the Expo export applies both, the web camera applies quality only, so never depend on a cropped result. " +
2825
2832
  "Behaviour is otherwise identical on web (a getUserMedia preview on EVERY host, phones included; a file input " +
@@ -2885,7 +2892,7 @@ const HOOKS = [
2885
2892
  "Editing is IMPERATIVE — call edit() from an event handler, never during render. " +
2886
2893
  "edit(uri, actions, options?) applies `actions` IN ORDER and resolves the SAME normalised asset shape useCamera() " +
2887
2894
  "yields — { uri, name, mimeType, width, height, size, base64?, file } — so capture -> edit -> upload is ONE code path: " +
2888
- "append result.file to a FormData as `file` and pass it to ctx.assets.upload(fd). " +
2895
+ "pass result.file to upload() from useFilestoreUpload. " +
2889
2896
  "Each action entry carries exactly one of { resize: { width?, height? } } (aspect preserved when only one is given), " +
2890
2897
  "{ crop: { originX, originY, width, height } }, { rotate: degrees } (positive is clockwise), or " +
2891
2898
  "{ flip: \"horizontal\" | \"vertical\" }. A crop rect must lie INSIDE the surface the earlier actions produced, and " +
@@ -3022,7 +3029,7 @@ const PRIMITIVES = [
3022
3029
  },
3023
3030
  // sc-1378 — `<FilePicker accept onPick>` lets a widget surface a native
3024
3031
  // file-picker on the web Player so end users can upload files to the
3025
- // backend (paired with useFilestoreUpload or useAssets.upload). Web wraps
3032
+ // backend (paired with useFilestoreUpload). Web wraps
3026
3033
  // a hidden DOM `<input type="file">`; native renders a disabled trigger
3027
3034
  // and exposes `FilePicker.isSupported = false` until a vetted Expo
3028
3035
  // picker pin lands.
@@ -5852,7 +5859,12 @@ const CONTRACT = deepFreeze({
5852
5859
  // a widget never receives it. `account`'s photo actions are now
5853
5860
  // optional members, and `user.email` is documented as nullable: an
5854
5861
  // account without an email has none.
5855
- version: "1.176.0",
5862
+ // 1.177.0: additive (sc-10937) — `useFilestoreUpload().upload(file,
5863
+ // { recordId })` names the record per call, so a widget can create a row
5864
+ // and attach to it in one handler. A FILE column's file goes in its
5865
+ // row's record space; useCamera / useImageEditor point at
5866
+ // useFilestoreUpload, since widget code cannot reach ctx.assets.
5867
+ version: "1.177.0",
5856
5868
  sharedTranslationKeys: SHARED_TRANSLATION_KEYS,
5857
5869
  hooks: HOOKS,
5858
5870
  primitives: PRIMITIVES,
package/dist/hooks.js CHANGED
@@ -4700,7 +4700,10 @@ export function useFilestoreFile(fileId) {
4700
4700
  * same way the read hooks do (tenant for PROJECT, app user for PERSONAL, the
4701
4701
  * record for RECORD) and posts a multipart FormData to `ctx.filestore.files.upload`.
4702
4702
  *
4703
- * `upload(file, { folderId?, fileName? })` takes a browser `File` on the web
4703
+ * sc-10937: `upload(file, { recordId })` names the record per call, so a
4704
+ * widget can create a row and attach to it in the same handler.
4705
+ *
4706
+ * `upload(file, { folderId?, recordId?, fileName? })` takes a browser `File` on the web
4704
4707
  * Player and expo-file-system's `File` on the Expo export, as `<FilePicker>`,
4705
4708
  * `useCamera()` and `useImageEditor()` all hand back. Never React Native's
4706
4709
  * legacy `{ uri, name, type }` part: the native host streams the file off
@@ -4734,7 +4737,8 @@ export function useFilestoreUpload(options) {
4734
4737
  folderId: defaultFolderId = null,
4735
4738
  compress: defaultCompress = true,
4736
4739
  } = options || {};
4737
- const ownerId = _filestoreOwnerId(ctx, spaceType, recordId);
4740
+ const isRecordSpace = String(spaceType || "project").toUpperCase() === "RECORD";
4741
+ const defaultOwnerId = _filestoreOwnerId(ctx, spaceType, recordId);
4738
4742
 
4739
4743
  const [uploading, setUploading] = useState(false);
4740
4744
  const [error, setError] = useState(null);
@@ -4746,19 +4750,25 @@ export function useFilestoreUpload(options) {
4746
4750
  const upload = useCallback(
4747
4751
  async (file, overrides) => {
4748
4752
  if (!file) throw new Error("useFilestoreUpload: file is required");
4753
+ const hasOverride = (key) =>
4754
+ Boolean(overrides) && Object.prototype.hasOwnProperty.call(overrides, key);
4755
+ const ownerId =
4756
+ isRecordSpace && hasOverride("recordId")
4757
+ ? overrides.recordId || null
4758
+ : defaultOwnerId;
4759
+ if (isRecordSpace && !ownerId) {
4760
+ // Not "Sign in": the widget forgot which row to attach to.
4761
+ const err = new Error("useFilestoreUpload: a record space needs a recordId");
4762
+ setError(err);
4763
+ throw err;
4764
+ }
4749
4765
  if (!ownerId) {
4750
4766
  const err = new Error("Sign in to upload");
4751
4767
  setError(err);
4752
4768
  throw err;
4753
4769
  }
4754
- const folderId =
4755
- overrides && Object.prototype.hasOwnProperty.call(overrides, "folderId")
4756
- ? overrides.folderId
4757
- : defaultFolderId;
4758
- const compress =
4759
- overrides && Object.prototype.hasOwnProperty.call(overrides, "compress")
4760
- ? overrides.compress
4761
- : defaultCompress;
4770
+ const folderId = hasOverride("folderId") ? overrides.folderId : defaultFolderId;
4771
+ const compress = hasOverride("compress") ? overrides.compress : defaultCompress;
4762
4772
  const form = new FormData();
4763
4773
  form.append("space_type", String(spaceType || "project").toUpperCase());
4764
4774
  form.append("owner_id", ownerId);
@@ -4800,7 +4810,7 @@ export function useFilestoreUpload(options) {
4800
4810
  throw err;
4801
4811
  }
4802
4812
  },
4803
- [ownerId, defaultFolderId, defaultCompress, spaceType],
4813
+ [defaultOwnerId, isRecordSpace, defaultFolderId, defaultCompress, spaceType],
4804
4814
  );
4805
4815
 
4806
4816
  return { upload, uploading, error, lastUploaded };
package/dist/index.d.ts CHANGED
@@ -2452,7 +2452,7 @@ export interface CameraAsset {
2452
2452
  /**
2453
2453
  * Ready-to-upload part — a Blob on both hosts: a `File` on web,
2454
2454
  * expo-file-system's `File` on native. Expo's fetch rejects any other shape.
2455
- * Append it to a FormData and hand that to `ctx.assets.upload(...)`.
2455
+ * Pass it to `upload(file)` from `useFilestoreUpload`.
2456
2456
  */
2457
2457
  file: unknown;
2458
2458
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@colixsystems/widget-sdk",
3
- "version": "0.220.0",
3
+ "version": "0.221.0",
4
4
  "description": "Common widget interface for AppStudio. Implements WidgetManifest, WidgetContext, property schema, and helper hooks.",
5
5
  "homepage": "https://github.com/Colix-AB/AppStudio",
6
6
  "type": "module",
package/src/contract.js CHANGED
@@ -1369,7 +1369,7 @@ const HOOKS = [
1369
1369
  "from the host context, builds a multipart FormData with the " +
1370
1370
  "snake_case fields the backend reads verbatim (`space_type`, " +
1371
1371
  "`owner_id`, `folder_id`, plus the binary `file`), and POSTs through " +
1372
- "ctx.filestore.files.upload. `upload(file, { folderId?, compress?, fileName? })` " +
1372
+ "ctx.filestore.files.upload. `upload(file, { folderId?, recordId?, compress?, fileName? })` " +
1373
1373
  "resolves to the created file row or throws the wire error. `file` must be a BLOB — a browser File on web, " +
1374
1374
  "expo-file-system's File on native (what FilePicker / useCamera / useImageEditor hand back); Expo's fetch rejects " +
1375
1375
  "React Native's legacy { uri, name, type } part with \"Unsupported FormDataPart implementation\". The stored name " +
@@ -1387,13 +1387,20 @@ const HOOKS = [
1387
1387
  "ORIGINAL matters (a document archive, a photo the user re-downloads, " +
1388
1388
  "anything with an exact-bytes requirement). " +
1389
1389
  "ALWAYS pass spaceType explicitly — omitting it falls back to " +
1390
- "'project', which is unreadable by a logged-out visitor. Choose it by " +
1391
- "who must SEE the file: 'public' for content the app displays to " +
1392
- "anyone browsing (product, catalogue, gallery, cover images), " +
1393
- "'project' for content restricted to signed-in workspace users, " +
1394
- "'personal' for a file private to the uploading app user.",
1390
+ "'project', which is unreadable by a logged-out visitor. A file that " +
1391
+ "belongs to a datastore row — anything stored in a FILE column — goes " +
1392
+ "in that row's 'record' space, so exactly the people who may read the " +
1393
+ "row may read the file (logged-out visitors too, when the table lets " +
1394
+ "everyone read). A file can only be attached to a row that exists: " +
1395
+ "create the row, call upload(file, { recordId: row.id }) — the per-call " +
1396
+ "recordId names the row the hook could not know at render — then write " +
1397
+ "the returned id into the FILE column with update(). A logged-out " +
1398
+ "visitor cannot upload into a record space. For a file that belongs to " +
1399
+ "no row: 'public' for content the app shows to anyone browsing, " +
1400
+ "'project' for signed-in workspace users only, 'personal' for a file " +
1401
+ "private to the uploading app user.",
1395
1402
  returnShape: {
1396
- upload: "(file, { folderId?, compress?, fileName? }) => Promise<FilestoreFile>",
1403
+ upload: "(file, { folderId?, recordId?, compress?, fileName? }) => Promise<FilestoreFile>",
1397
1404
  uploading: "boolean",
1398
1405
  error: "Error | null",
1399
1406
  lastUploaded: "FilestoreFile | null",
@@ -2818,8 +2825,8 @@ const HOOKS = [
2818
2825
  "common case and is deliberately NOT an error, so no try/catch is needed on the happy path. They reject with a " +
2819
2826
  "CameraError whose .code is one of PERMISSION_DENIED | UNSUPPORTED | INTERNAL. `asset.file` is already the right " +
2820
2827
  "upload part for the host — a BLOB on both (a File on web, expo-file-system's File on native), because Expo's fetch " +
2821
- "implements the web FormData spec and rejects React Native's legacy { uri, name, type } part outright: append it to a FormData as `file` and pass " +
2822
- "that to ctx.assets.upload(fd) — one code path on both platforms. reset() clears the asset and releases it. Check " +
2828
+ "implements the web FormData spec and rejects React Native's legacy { uri, name, type } part outright: pass it " +
2829
+ "straight to upload(asset.file) from useFilestoreUpload — one code path on both platforms. reset() clears the asset and releases it. Check " +
2823
2830
  "`supported` before rendering a camera button. options: { allowsEditing, quality } are HINTS the host honours where it " +
2824
2831
  "can — the Expo export applies both, the web camera applies quality only, so never depend on a cropped result. " +
2825
2832
  "Behaviour is otherwise identical on web (a getUserMedia preview on EVERY host, phones included; a file input " +
@@ -2885,7 +2892,7 @@ const HOOKS = [
2885
2892
  "Editing is IMPERATIVE — call edit() from an event handler, never during render. " +
2886
2893
  "edit(uri, actions, options?) applies `actions` IN ORDER and resolves the SAME normalised asset shape useCamera() " +
2887
2894
  "yields — { uri, name, mimeType, width, height, size, base64?, file } — so capture -> edit -> upload is ONE code path: " +
2888
- "append result.file to a FormData as `file` and pass it to ctx.assets.upload(fd). " +
2895
+ "pass result.file to upload() from useFilestoreUpload. " +
2889
2896
  "Each action entry carries exactly one of { resize: { width?, height? } } (aspect preserved when only one is given), " +
2890
2897
  "{ crop: { originX, originY, width, height } }, { rotate: degrees } (positive is clockwise), or " +
2891
2898
  "{ flip: \"horizontal\" | \"vertical\" }. A crop rect must lie INSIDE the surface the earlier actions produced, and " +
@@ -3022,7 +3029,7 @@ const PRIMITIVES = [
3022
3029
  },
3023
3030
  // sc-1378 — `<FilePicker accept onPick>` lets a widget surface a native
3024
3031
  // file-picker on the web Player so end users can upload files to the
3025
- // backend (paired with useFilestoreUpload or useAssets.upload). Web wraps
3032
+ // backend (paired with useFilestoreUpload). Web wraps
3026
3033
  // a hidden DOM `<input type="file">`; native renders a disabled trigger
3027
3034
  // and exposes `FilePicker.isSupported = false` until a vetted Expo
3028
3035
  // picker pin lands.
@@ -5852,7 +5859,12 @@ const CONTRACT = deepFreeze({
5852
5859
  // a widget never receives it. `account`'s photo actions are now
5853
5860
  // optional members, and `user.email` is documented as nullable: an
5854
5861
  // account without an email has none.
5855
- version: "1.176.0",
5862
+ // 1.177.0: additive (sc-10937) — `useFilestoreUpload().upload(file,
5863
+ // { recordId })` names the record per call, so a widget can create a row
5864
+ // and attach to it in one handler. A FILE column's file goes in its
5865
+ // row's record space; useCamera / useImageEditor point at
5866
+ // useFilestoreUpload, since widget code cannot reach ctx.assets.
5867
+ version: "1.177.0",
5856
5868
  sharedTranslationKeys: SHARED_TRANSLATION_KEYS,
5857
5869
  hooks: HOOKS,
5858
5870
  primitives: PRIMITIVES,