Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .changeset/12055-upload-path-not-scope.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

The adapter used to send `scope: opts.scope ?? options.path` in its presigned upload request, so an adapter built without a `scope` turned the `path` a caller passed to `upload(file, { path })` into the scope. A path is a key prefix for the S3 and Azure presign callbacks, not a scope: the ObjectStack storage service accepts only the values of the `scope` select on its `sys_file` object and refuses any other, so such an upload failed at the presign step. The adapter now sends only the `scope` it was built with, or none, and the server applies its default. `UploadResult.meta.scope` reports the same value.

The `scope` option's documentation no longer teaches free key prefixes ("avatars", "logos", "attachments/case"). It names the server's `sys_file` scope select as the vocabulary. The option's type is unchanged (`string`).
The `scope` option's documentation no longer teaches free key prefixes ("avatars", "logos", "attachments/case"). It names the server's `sys_file` scope select as the vocabulary.

BREAKING (`@object-ui/providers`), for a caller that relied on `path` to choose the scope of an adapter built without one: the upload is now filed under the server's default scope. Remedy: pass one of the server's scopes as the adapter's `scope`, or none to take the server's default. (The bump is `minor` by this repo's release model: objectui's major follows the `@objectstack` family major, and its own breaking changes ship as `minor` with the breaking semantics stated here.)

Expand Down
11 changes: 11 additions & 0 deletions .changeset/12055-upload-scope-type.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
---
'@object-ui/providers': minor
---

`createObjectStackUploadAdapter`'s `scope` option is typed from the spec's upload-scope vocabulary (objectui#12055).

`ObjectStackUploadAdapterOptions.scope` was an open `string`, so a caller could build an adapter with a scope the ObjectStack storage service refuses with `400`. It is now `UploadScope` from `@objectstack/spec/api`, the one list the spec's upload request, the `scope` select on `sys_file` and the upload doors share. `@object-ui/providers` reaches it through the `API` namespace `@object-ui/types` already re-exports, so no dependency and no export is added. Omitting `scope` still sends none, and the server applies the default the spec's upload request declares.

BREAKING (`@object-ui/providers`), TypeScript only, for a caller that builds the adapter with a scope outside the vocabulary (for example `'avatars'` or `'attachments/case'`): it no longer compiles. The server already refused such a request at runtime. Remedy: pass one of the vocabulary's values, or omit `scope` to take the server's default. (The bump is `minor` by this repo's release model: objectui's major follows the `@objectstack` family major, and its own breaking changes ship as `minor` with the breaking semantics stated here.)

**Clause-②: no (narrowing)**: the option's accepted values narrow from any string to the spec's upload scopes. No export, option or type member is added; the declaration file gains a type-only import of `API` from `@object-ui/types`, a package `@object-ui/providers` already depends on.
18 changes: 11 additions & 7 deletions packages/providers/src/UploadProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import React, {
useRef,
type ReactNode,
} from 'react';
import type { API } from '@object-ui/types';

export interface UploadResult {
/** Canonical, shareable URL (e.g. https://my-bucket.s3.amazonaws.com/avatars/abc.png). */
Expand Down Expand Up @@ -204,14 +205,17 @@ export interface ObjectStackUploadAdapterOptions {
/**
* The storage scope the server files every upload from this adapter
* under, sent as the presigned upload request's `scope`. It is not a key
* prefix or folder. The server accepts only the values of the `scope`
* select on its `sys_file` object (`@objectstack/service-storage`) and
* refuses any other value; `attachments`, for example, is the scope of
* files joined to records through `sys_attachment`. Omit it and the
* request carries no scope, so the server applies its default
* (objectui#12055).
* prefix or folder. Its values are the spec's upload-scope vocabulary,
* `UploadScope` from `@objectstack/spec/api` (reached here as
* `API.UploadScope` from `@object-ui/types`): the one list the upload
* request, the `scope` select on `sys_file` and the upload doors share.
* A value outside it does not compile, and the server refuses it with
* `400`. `attachments`, for example, is the scope of files joined to
* records through `sys_attachment`. Omit it and the request carries no
* scope, so the server applies the default the spec's upload request
* declares (objectui#12055).
*/
scope?: string;
scope?: API.UploadScope;
/** Override `fetch` (tests, ServiceWorker queues). */
fetchImpl?: typeof fetch;
/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,17 @@
*
* These pins read the presign request body the adapter actually sends,
* through an injected `fetchImpl` (no global is replaced).
*
* The option's TYPE is the spec's upload-scope vocabulary, `UploadScope` from
* `@objectstack/spec/api`, reached through `@object-ui/types`'s `API`
* namespace. The compile-time pins at the end of this file are checked by
* `tsconfig.test.json`, which `type-check` chains after the source program.
*/

import { describe, it, expect } from 'vitest';
import type { API } from '@object-ui/types';

import { createObjectStackUploadAdapter } from '../UploadProvider';
import { createObjectStackUploadAdapter, type ObjectStackUploadAdapterOptions } from '../UploadProvider';

const FILE_ID = 'f-12055';

Expand Down Expand Up @@ -105,3 +111,39 @@ describe('the in-repo upload shapes send the same request as before (objectui#12
});
});
});

/* -------------------------------------------------------------------------- */
/* Compile-time pins — compiled by tsconfig.test.json, chained off type-check. */
/* -------------------------------------------------------------------------- */

type Assert<T extends true> = T;
type Equal<A, B> = (<T>() => T extends A ? 1 : 2) extends <T>() => T extends B ? 1 : 2 ? true : false;
type IsAny<T> = 0 extends 1 & T ? true : false;

describe("the scope option is typed from the spec's upload-scope vocabulary (objectui#12055)", () => {
it('is the spec type itself: not a free string, not a local copy', () => {
type Scope = NonNullable<ObjectStackUploadAdapterOptions['scope']>;
// The spec type resolved (an unresolved import would read as `any` and
// make every pin below vacuous).
type _SpecTypeIsReal = Assert<Equal<IsAny<API.UploadScope>, false>>;
// Exactly the spec's type: a member the spec adds or retires moves the
// option with it, and no consumer-side list can drift from it.
type _OptionIsTheSpecType = Assert<Equal<Scope, API.UploadScope>>;
// Not widened back to an open string.
type _NotAnOpenString = Assert<Equal<string extends Scope ? true : false, false>>;

expect(true).toBe(true);
});

it("refuses a scope outside the vocabulary at the authoring site: 'avatars' does not compile", () => {
// The pin is this directive: with `scope` widened to `string` the line
// compiles, the directive goes unused, and `type-check` fails (TS2578).
// @ts-expect-error -- 'avatars' is a key prefix, not an upload scope; the server refuses it with 400.
const refused: ObjectStackUploadAdapterOptions = { scope: 'avatars' };
void refused;
// The in-repo producer's value compiles (RecordAttachmentsPanel builds its adapter with it).
const accepted: ObjectStackUploadAdapterOptions = { scope: 'attachments' };

expect(accepted.scope).toBe('attachments');
});
});
Loading