@rebasepro/types 0.23.0 → 0.24.1-canary.g56af4a4

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.
@@ -21,10 +21,25 @@ export interface FetchOneProps<M extends Record<string, unknown> = Record<string
21
21
  */
22
22
  withDeleted?: boolean | "only";
23
23
  }
24
+ /**
25
+ * What a row listener subscribes to: the row, and the relations each frame
26
+ * loads into it.
27
+ *
28
+ * @internal
29
+ */
30
+ export type ListenOneTarget<M extends Record<string, unknown> = Record<string, unknown>> = FetchOneProps<M> & {
31
+ /**
32
+ * Relations to load into every frame, as {@link FetchCollectionProps.include}
33
+ * loads them into each row of a list. Without it a to-many relation
34
+ * is absent from each frame, and the admin's record form, opened by
35
+ * URL, showed it empty — so a save from it unlinked every related row.
36
+ */
37
+ include?: IncludeSpec;
38
+ };
24
39
  /**
25
40
  * @internal
26
41
  */
27
- export type ListenOneProps<M extends Record<string, unknown> = Record<string, unknown>> = FetchOneProps<M> & {
42
+ export type ListenOneProps<M extends Record<string, unknown> = Record<string, unknown>> = ListenOneTarget<M> & {
28
43
  onUpdate: (row: Record<string, unknown> | null) => void;
29
44
  onError?: (error: Error) => void;
30
45
  };
@@ -172,6 +187,13 @@ export type ListenCollectionProps<M extends Record<string, unknown> = Record<str
172
187
  * the two spellings then disagreed about what a page was.
173
188
  */
174
189
  page?: number;
190
+ /**
191
+ * Subscribe to the count alone: the server reads no rows, and each
192
+ * frame carries `rows: []` and `meta.total`. A server that predates it
193
+ * ignores the key and answers with the rows asked for (so ask for
194
+ * `limit: 1`) and the same `meta`. See `listenCount`.
195
+ */
196
+ countOnly?: boolean;
175
197
  onUpdate: (rows: Record<string, unknown>[], meta?: CollectionUpdateMeta) => void;
176
198
  onError?: (error: Error) => void;
177
199
  };
@@ -211,6 +233,39 @@ export interface SaveProps<M extends Record<string, unknown> = Record<string, un
211
233
  * targets rather than a 500 naming a constraint the caller never wrote.
212
234
  */
213
235
  onConflict?: readonly string[];
236
+ /**
237
+ * A check on the stored row an update addresses, run by the driver inside
238
+ * the write's own transaction, before any hook runs and before anything is
239
+ * written. See {@link RowPrecondition}. Honoured only by a driver that
240
+ * declares {@link DataDriver.checksRowPreconditions}; ignored on a create.
241
+ */
242
+ precondition?: RowPrecondition;
243
+ }
244
+ /**
245
+ * A check a caller makes on the row a write addresses, inside that write.
246
+ *
247
+ * The REST routes used to read the row in a transaction of their own before
248
+ * writing it, to answer a missing row with their 404 and to compare
249
+ * `If-Match`. Two transactions: the read loaded every declared relation of the
250
+ * row, and two writes carrying the same tag could both pass the comparison,
251
+ * because neither saw the other's write before making its own.
252
+ *
253
+ * @internal
254
+ */
255
+ export interface RowPrecondition {
256
+ /**
257
+ * Lock the row before it is read, so no other write to it can land between
258
+ * this check and this write. A conditional write (`If-Match`) sets it; a
259
+ * plain existence check does not need it and does not pay for it.
260
+ */
261
+ lock?: boolean;
262
+ /**
263
+ * Judge the row as this caller reads it at the address written (their
264
+ * policies, their `beforeQuery`, the trash hidden unless the write is a
265
+ * restore or a purge) — `undefined` when there is none. Throw to refuse the
266
+ * write; nothing has been written when this runs.
267
+ */
268
+ check(stored: Record<string, unknown> | undefined): void | Promise<void>;
214
269
  }
215
270
  /**
216
271
  * @internal
@@ -277,6 +332,12 @@ export interface DeleteProps<M extends Record<string, unknown> = Record<string,
277
332
  * already this one.
278
333
  */
279
334
  hard?: boolean;
335
+ /**
336
+ * A check on the stored row, inside the delete's own transaction, before
337
+ * any hook runs. See {@link RowPrecondition}. Honoured only by a driver
338
+ * that declares {@link DataDriver.checksRowPreconditions}.
339
+ */
340
+ precondition?: RowPrecondition;
280
341
  }
281
342
  /**
282
343
  * @internal
@@ -517,6 +578,21 @@ export interface DataDriver {
517
578
  * Count the number of entities in a collection
518
579
  */
519
580
  count?<M extends Record<string, unknown> = Record<string, unknown>>(props: FetchCollectionProps<M>): Promise<number>;
581
+ /**
582
+ * Run several reads as one: `work` is handed a driver whose reads all go
583
+ * through a single read-only transaction that reads one snapshot, under
584
+ * this driver's identity and row-level security, and its result is
585
+ * returned.
586
+ *
587
+ * A list page is a read and a count, and each was a transaction of its
588
+ * own — the second checkout, `BEGIN`, identity setup and `COMMIT` cost as
589
+ * many round trips as the count itself. On one snapshot the two also
590
+ * agree: `meta.total` describes the rows the page was taken from, and an
591
+ * `ETag` the row the body was rendered from.
592
+ *
593
+ * Optional: a driver without it is called directly, read by read.
594
+ */
595
+ readTogether?<T>(work: (driver: DataDriver) => Promise<T>): Promise<T>;
520
596
  /**
521
597
  * Check if the given filter combination is valid
522
598
  * @param props
@@ -548,6 +624,14 @@ export interface DataDriver {
548
624
  * `fetchCollection` pipeline, enabling include-aware eager-loading.
549
625
  */
550
626
  restFetchService?: RestFetchService;
627
+ /**
628
+ * This driver reads the row an update or a delete addresses inside that
629
+ * write's own transaction, and runs {@link SaveProps.precondition} /
630
+ * {@link DeleteProps.precondition} on it there. A caller may then leave its
631
+ * own read of the row out. A driver without it ignores the precondition,
632
+ * so a caller must read the row itself.
633
+ */
634
+ readonly checksRowPreconditions?: boolean;
551
635
  /**
552
636
  * Return the admin capabilities of this driver.
553
637
  * @see SQLAdmin
@@ -4,5 +4,6 @@ export * from "./data.js";
4
4
  export * from "./data_driver.js";
5
5
  export * from "./effective_role.js";
6
6
  export * from "./storage.js";
7
+ export * from "./storage-references.js";
7
8
  export * from "./email.js";
8
9
  export * from "./client.js";
@@ -0,0 +1,114 @@
1
+ import type { StorageSource } from "./storage.js";
2
+ /**
3
+ * The scheme of a **storage reference**: a stored object named inside text.
4
+ *
5
+ * Text that embeds a file — a markdown body above all — cannot hold a private
6
+ * object's URL: that URL carries a download token which expires in minutes,
7
+ * after which every reader of the text gets a 401 for an image that is still in
8
+ * the bucket. It holds a reference instead,
9
+ * `rebase-storage:posts/cover.png` (`?storageId=media` for a named source),
10
+ * and whoever renders the text exchanges each reference for a fresh URL with
11
+ * {@link resolveStorageReferences} — `resolveStorageReferences(text, client)`
12
+ * from `@rebasepro/client` on a site.
13
+ *
14
+ * @group Models
15
+ */
16
+ export declare const STORAGE_REFERENCE_SCHEME = "rebase-storage:";
17
+ /**
18
+ * The object a storage reference names.
19
+ *
20
+ * @group Models
21
+ */
22
+ export interface StorageReferenceTarget {
23
+ /** The object's key in its storage source. */
24
+ key: string;
25
+ /** The named storage source holding it. Absent for the default source. */
26
+ storageId?: string;
27
+ }
28
+ /**
29
+ * The reference for an object — what text stores in place of its URL.
30
+ *
31
+ * Every part is percent-encoded, so the reference contains no space,
32
+ * parenthesis or quote and survives as one markdown link destination whatever
33
+ * the file was called.
34
+ *
35
+ * @group Models
36
+ */
37
+ export declare function storageReference(key: string, storageId?: string | null): string;
38
+ /**
39
+ * Whether a string — an image's `src`, a link's `href` — is a storage reference.
40
+ *
41
+ * @group Models
42
+ */
43
+ export declare function isStorageReference(value: string | null | undefined): value is string;
44
+ /**
45
+ * The object a storage reference names, or `null` for anything else — a URL, a
46
+ * bare key, or a reference whose key would climb out of its prefix.
47
+ *
48
+ * @group Models
49
+ */
50
+ export declare function parseStorageReference(value: string | null | undefined): StorageReferenceTarget | null;
51
+ /**
52
+ * The object a private file's download URL was minted for, or `null` for any
53
+ * other URL.
54
+ *
55
+ * Text written before storage references stored such URLs —
56
+ * `…/storage/file/posts/cover.png?token=…` — and their tokens expired minutes
57
+ * later. The token still names the object it was minted for, so the URL can be
58
+ * traded for a fresh one, or for a reference. Read without verifying the
59
+ * token: it grants nothing here, it only says which object is meant, and the
60
+ * key in the URL must agree with it. Host-agnostic, since a URL written
61
+ * through one origin may be read through another.
62
+ *
63
+ * @group Models
64
+ */
65
+ export declare function storageTargetOfDownloadUrl(url: string): StorageReferenceTarget | null;
66
+ /**
67
+ * Every object `text` names: its storage references, and the private files'
68
+ * download URLs saved in it before references existed. An object named more
69
+ * than once may be listed more than once.
70
+ *
71
+ * @group Models
72
+ */
73
+ export declare function storageTargetsInText(text: string): StorageReferenceTarget[];
74
+ /**
75
+ * `text` with every private file's download URL replaced by the storage
76
+ * reference for its object — for rewriting text saved before references, so
77
+ * it stores what new text stores. Any other URL is left as it is.
78
+ *
79
+ * @group Models
80
+ */
81
+ export declare function storageReferencesForDownloadUrls(text: string): string;
82
+ /**
83
+ * Where {@link resolveStorageReferences} gets URLs from: a function that
84
+ * answers one, or anything shaped like a Rebase client — its `storage` for the
85
+ * default source and `createStorageSource(id)` for a named one.
86
+ *
87
+ * @group Models
88
+ */
89
+ export type StorageReferenceResolver = ((target: StorageReferenceTarget) => Promise<string | null | undefined>) | {
90
+ storage?: StorageSource;
91
+ createStorageSource?(storageId: string): StorageSource;
92
+ };
93
+ /**
94
+ * Exchange every storage reference in `text` for a URL, at the moment the text
95
+ * is rendered.
96
+ *
97
+ * ```ts
98
+ * const html = markdown.render(await resolveStorageReferences(post.body, client));
99
+ * ```
100
+ *
101
+ * Each distinct object is asked for once — `getSignedUrl(key)` on its source —
102
+ * so a private object gets a freshly minted download URL every time the text is
103
+ * rendered. A reference that cannot be resolved (no such object, an unknown
104
+ * source) is left as it was, so a missing object reads as a missing image
105
+ * rather than as a failed render.
106
+ *
107
+ * A private file's download URL saved in the text before references existed
108
+ * is renewed the same way: its expired token still names the object (see
109
+ * {@link storageTargetOfDownloadUrl}). One that cannot be resolved is left as
110
+ * it was.
111
+ *
112
+ * @group Models
113
+ */
114
+ export declare function resolveStorageReferences(text: string, from: StorageReferenceResolver): Promise<string>;
@@ -8,13 +8,58 @@
8
8
  */
9
9
  export declare const PUBLIC_STORAGE_PREFIX = "public/";
10
10
  /**
11
- * True when a storage key/path points at a public object (lives under
12
- * {@link PUBLIC_STORAGE_PREFIX}). The check is applied to the key *within the
13
- * bucket* — strip any `bucket/` and `scheme://` prefixes first.
11
+ * True when a storage **key** is public: it lies under
12
+ * {@link PUBLIC_STORAGE_PREFIX}.
13
+ *
14
+ * The key is read exactly as it is — the canonical key within its bucket, the
15
+ * one the server stores, serves, and hands a `storageAuthorize` hook as
16
+ * `key` — and nothing is parsed out of it. That is the difference from
17
+ * {@link isPublicStoragePath}, which reads a client-side *path* and so strips
18
+ * a `scheme://` and a leading `default/` bucket segment first. On a key both are
19
+ * wrong: `default/public/x` is a key in a folder called `default`, and a
20
+ * request path `notes://public/x` names the key `notes:/public/x`. Neither is
21
+ * public, and the server decides on this function so that it never says they
22
+ * are.
23
+ *
24
+ * @group Models
25
+ */
26
+ export declare function isPublicStorageKey(key: string | null | undefined): boolean;
27
+ /**
28
+ * True when a storage key or client-side path points at a public object
29
+ * (lives under {@link PUBLIC_STORAGE_PREFIX}).
30
+ *
31
+ * For a path as a client holds it: a `scheme://` prefix and a single leading
32
+ * `default/` bucket segment are stripped first. The server and a
33
+ * `storageAuthorize` hook, which hold the canonical key, use
34
+ * {@link isPublicStorageKey}.
14
35
  *
15
36
  * @group Models
16
37
  */
17
38
  export declare function isPublicStoragePath(path: string | null | undefined): boolean;
39
+ /**
40
+ * The path the storage routes address an object by, from a key or from the
41
+ * `storageUrl` an upload returned (`<scheme>://<bucket>/<key>`, which is what a
42
+ * property with `includeBucketUrl: true` keeps in its column).
43
+ *
44
+ * The routes address a source's objects by key. On S3 and GCS the bucket in a
45
+ * `storageUrl` is always the source's own — the server refuses any other — so
46
+ * it is dropped: sent along, `s3://acme-media/products/a.png` reached the
47
+ * server as the key `acme-media/products/a.png`, the preview found nothing and
48
+ * a delete reported success while the object stayed. It worked on local disk
49
+ * only, because `local://default/<key>` happens to carry the one bucket
50
+ * segment the routes recognise — which is kept, as is any other local bucket.
51
+ *
52
+ * A bare key whose first folder is `default` is sent behind that bucket
53
+ * segment. The routes read one leading `default/` (in any case) as the bucket,
54
+ * so the key `default/photo.png` sent as it is names the root `photo.png`:
55
+ * {@link defaultBucketPath} is the spelling that names the key itself.
56
+ *
57
+ * Shared by every storage source that talks to those routes, so they address
58
+ * an object alike.
59
+ *
60
+ * @group Models
61
+ */
62
+ export declare function storageObjectPath(keyOrUrl: string, bucket?: string): string;
18
63
  /**
19
64
  * @group Models
20
65
  */
@@ -35,18 +80,15 @@ export interface UploadFileProps {
35
80
  * and the property path within it (`coverImage`, `meta.avatar`,
36
81
  * `gallery` for an array of files).
37
82
  *
38
- * The server reads it to enforce that property's own `storage.maxSize` and
39
- * `storage.acceptedFiles`, which were declared per property, published in
40
- * the generated types, rendered by the panel's file picker, and until now
41
- * enforced by nothing on the server — so a `curl` past the picker put a
42
- * 40 MB executable in a bucket whose config said "images, under 200 KB".
83
+ * The server holds the upload to that property's own `storage.maxSize`
84
+ * and `storage.acceptedFiles`. It holds every upload to the limits of the
85
+ * properties whose `storagePath` its key falls in anyway, named or not;
86
+ * naming the property adds that property's rule, which matters for a
87
+ * property whose `storagePath` is a function the server cannot read.
43
88
  *
44
89
  * Advisory in one direction only. The rules are resolved from the server's
45
- * own registry by slug, so naming a property can make an upload *stricter*
46
- * or leave it at the global cap; it can never widen anything.
47
- *
48
- * Omitted, the upload is checked against the deployment's global
49
- * `maxFileSize` exactly as before.
90
+ * own registry by slug, so naming a property can make an upload *stricter*;
91
+ * it can never widen anything.
50
92
  */
51
93
  context?: UploadPropertyContext;
52
94
  }
@@ -237,4 +279,22 @@ export declare interface StorageReference {
237
279
  * this reference is the root.
238
280
  */
239
281
  parent: StorageReference | null;
282
+ /**
283
+ * The object's size in bytes, as the listing read it. Set on the objects
284
+ * of a listing from the built-in local, S3 and GCS sources; absent on
285
+ * folders and from a source that cannot list it without a request per
286
+ * object. Since 0.24.
287
+ */
288
+ size?: number;
289
+ /**
290
+ * When the object was last written, as an ISO 8601 timestamp — set where
291
+ * {@link size} is.
292
+ */
293
+ updated?: string;
294
+ /**
295
+ * The object's stored content type, as the listing read it: set by local
296
+ * storage and GCS. An S3 listing does not carry it, so on S3 it is absent
297
+ * and `getSignedUrl()` (or the metadata route) still says.
298
+ */
299
+ contentType?: string;
240
300
  }
package/dist/errors.d.ts CHANGED
@@ -23,7 +23,21 @@
23
23
  *
24
24
  * @group Errors
25
25
  */
26
- export type RebaseErrorCode = "BAD_REQUEST" | "UNAUTHORIZED" | "FORBIDDEN" | "NOT_FOUND" | "CONFLICT" | "INTERNAL_ERROR" | "SERVICE_UNAVAILABLE" | "NETWORK_ERROR" | "OFFLINE" | "DB_PERMISSION_DENIED" | "SCHEMA_DRIFT" | (string & {});
26
+ export type RebaseErrorCode = "BAD_REQUEST" | "UNAUTHORIZED" | "FORBIDDEN" | "NOT_FOUND" | "CONFLICT" | "INTERNAL_ERROR" | "SERVICE_UNAVAILABLE" | "NETWORK_ERROR" | "OFFLINE" | "DB_PERMISSION_DENIED" | "SCHEMA_DRIFT"
27
+ /**
28
+ * Realtime: the socket closed after a request was written and before the
29
+ * server answered — the request may or may not have run, and it is never
30
+ * re-sent. On a live subscription's `onError`: the connection has been
31
+ * down longer than a blip; the subscription is kept and resumes, with a
32
+ * fresh `onUpdate`, when the socket is back.
33
+ */
34
+ | "CONNECTION_LOST"
35
+ /**
36
+ * Realtime: no answer within the request timeout of the call — either it
37
+ * waited for a socket the whole time and was never sent, or it was written
38
+ * and never answered.
39
+ */
40
+ | "REQUEST_TIMEOUT" | (string & {});
27
41
  /**
28
42
  * Structured initializer for {@link RebaseApiError}.
29
43
  *