@omelhorsite/sdk 0.3.0 → 0.4.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 +112 -130
- package/dist/index.js +1045 -1242
- package/dist/types/index.d.ts +1 -0
- package/dist/types/internal/attachments.d.ts +17 -0
- package/dist/types/internal/helpers.d.ts +9 -0
- package/dist/types/listing.d.ts +90 -0
- package/dist/types/resources/account.d.ts +5 -2
- package/dist/types/resources/admin/authorizedApplications.d.ts +133 -0
- package/dist/types/resources/admin/chests.d.ts +51 -0
- package/dist/types/resources/admin/eventAlerts.d.ts +60 -0
- package/dist/types/resources/admin/identities.d.ts +99 -0
- package/dist/types/resources/admin/index.d.ts +77 -0
- package/dist/types/resources/admin/jobs.d.ts +97 -0
- package/dist/types/resources/admin/myOauthApplications.d.ts +282 -0
- package/dist/types/resources/admin/notepads.d.ts +49 -0
- package/dist/types/resources/admin/oauthApplications.d.ts +309 -0
- package/dist/types/resources/admin/quotas.d.ts +114 -0
- package/dist/types/resources/admin/shortLinks.d.ts +201 -0
- package/dist/types/resources/admin/types.d.ts +222 -0
- package/dist/types/resources/admin/vocalSeparations.d.ts +98 -0
- package/dist/types/resources/admin.d.ts +17 -13
- package/dist/types/resources/auth/sessions.d.ts +6 -9
- package/dist/types/resources/content/analysis.d.ts +62 -0
- package/dist/types/resources/content/blogs.d.ts +471 -0
- package/dist/types/resources/content/feedbacks.d.ts +263 -0
- package/dist/types/resources/content/index.d.ts +52 -0
- package/dist/types/resources/content/intel/articles.d.ts +230 -0
- package/dist/types/resources/content/intel/config.d.ts +135 -0
- package/dist/types/resources/content/intel/index.d.ts +53 -0
- package/dist/types/resources/content/intel/items.d.ts +91 -0
- package/dist/types/resources/content/intel/reports.d.ts +108 -0
- package/dist/types/resources/content/intel/scripts.d.ts +152 -0
- package/dist/types/resources/content/intel/sources.d.ts +236 -0
- package/dist/types/resources/content/intel/stats.d.ts +105 -0
- package/dist/types/resources/content/intel/types.d.ts +86 -0
- package/dist/types/resources/content/jokes.d.ts +93 -0
- package/dist/types/resources/content/notifications.d.ts +147 -0
- package/dist/types/resources/content/serviceUsages.d.ts +73 -0
- package/dist/types/resources/content/servicesStatus.d.ts +194 -0
- package/dist/types/resources/content/siteConfig.d.ts +43 -0
- package/dist/types/resources/content/spaceInvaders.d.ts +154 -0
- package/dist/types/resources/content.d.ts +41 -236
- package/dist/types/resources/jobs.d.ts +7 -4
- package/dist/types/resources/library/annotations.d.ts +162 -0
- package/dist/types/resources/library/books.d.ts +534 -0
- package/dist/types/resources/library/chat.d.ts +259 -0
- package/dist/types/resources/library/index.d.ts +39 -0
- package/dist/types/resources/library/shelves.d.ts +237 -0
- package/dist/types/resources/library/types.d.ts +120 -0
- package/dist/types/resources/library.d.ts +20 -22
- package/dist/types/resources/movies/addons.d.ts +388 -0
- package/dist/types/resources/movies/collections.d.ts +352 -0
- package/dist/types/resources/movies/index.d.ts +29 -0
- package/dist/types/resources/movies/types.d.ts +50 -0
- package/dist/types/resources/movies/watchProgress.d.ts +277 -0
- package/dist/types/resources/movies.d.ts +27 -53
- package/dist/types/resources/music/artists.d.ts +3 -1
- package/dist/types/resources/music/imports.d.ts +9 -8
- package/dist/types/resources/music/playlists.d.ts +8 -7
- package/dist/types/resources/music/songs.d.ts +5 -21
- package/dist/types/resources/shortLinks.d.ts +5 -2
- package/dist/types/resources/social/groupChats.d.ts +458 -0
- package/dist/types/resources/social/index.d.ts +19 -0
- package/dist/types/resources/social/messages.d.ts +424 -0
- package/dist/types/resources/social/relationships.d.ts +322 -0
- package/dist/types/resources/social/types.d.ts +37 -0
- package/dist/types/resources/social.d.ts +21 -3
- package/dist/types/resources/storage.d.ts +24 -6
- package/dist/types/resources/tickets.d.ts +19 -10
- package/package.json +1 -1
|
@@ -0,0 +1,534 @@
|
|
|
1
|
+
/** The `library.books` namespace, with the inputs, limits and upload helpers only it uses. */
|
|
2
|
+
import { Resource } from "../../http";
|
|
3
|
+
import type { ListParams } from "../../listing";
|
|
4
|
+
import type { FileInput, FileOutput, Id, NativeFile, Paginated, RequestOptions } from "../../types";
|
|
5
|
+
import type { Book, BookAnnotation, BookAnnotationLocation, BookFormat, BookId, BookVisibility, PublicLibrary } from "./types";
|
|
6
|
+
/**
|
|
7
|
+
* Ceiling `POST /books` enforces on the uploaded file: 100 MiB.
|
|
8
|
+
*
|
|
9
|
+
* NOT the ceiling you will actually hit in production. See
|
|
10
|
+
* {@link BOOK_UPLOAD_EDGE_LIMIT_BYTES} and {@link LibraryBooksNamespace.create}.
|
|
11
|
+
*/
|
|
12
|
+
export declare const BOOK_MAX_FILE_BYTES = 104857600;
|
|
13
|
+
/**
|
|
14
|
+
* The ceiling that bites first: roughly 100 MB of request body, refused by
|
|
15
|
+
* Cloudflare before the origin is reached.
|
|
16
|
+
*
|
|
17
|
+
* Deliberately a decimal 100 MB against Rails' binary 100 MiB. The gap is real
|
|
18
|
+
* and it is exactly the band where the failure stops making sense: a 102 MB
|
|
19
|
+
* book is inside the limit Rails would allow and outside the one the edge
|
|
20
|
+
* allows, so it fails with a `413` whose body is Cloudflare's HTML error page
|
|
21
|
+
* and mentions neither books nor a size. There is no chunked upload route for
|
|
22
|
+
* books - `POST /books` is a single multipart request - so this ceiling is
|
|
23
|
+
* real and cannot be worked around by splitting the file.
|
|
24
|
+
*/
|
|
25
|
+
export declare const BOOK_UPLOAD_EDGE_LIMIT_BYTES = 100000000;
|
|
26
|
+
/** Longest title accepted. Longer titles fail validation with a 400. */
|
|
27
|
+
export declare const BOOK_TITLE_MAX_LENGTH = 300;
|
|
28
|
+
/** Longest author accepted. */
|
|
29
|
+
export declare const BOOK_AUTHOR_MAX_LENGTH = 200;
|
|
30
|
+
/**
|
|
31
|
+
* Most tags a book keeps.
|
|
32
|
+
*
|
|
33
|
+
* The server does NOT reject a longer list: it strips, downcases,
|
|
34
|
+
* de-duplicates and then takes the first 30 in silence. Send more and the
|
|
35
|
+
* excess is gone with no error to notice.
|
|
36
|
+
*/
|
|
37
|
+
export declare const BOOK_MAX_TAGS = 30;
|
|
38
|
+
/** Longest tag kept. Longer tags are TRUNCATED, not rejected. */
|
|
39
|
+
export declare const BOOK_TAG_MAX_LENGTH = 40;
|
|
40
|
+
/**
|
|
41
|
+
* Book-file reads an ANONYMOUS caller gets per hour, per IP, across
|
|
42
|
+
* `GET /books/:id/file` and `GET /books/:id/download` together.
|
|
43
|
+
*
|
|
44
|
+
* A signed-in caller is exempt. It exists because an anonymous visitor
|
|
45
|
+
* pulling book bytes is the one expensive thing sharing a book opens up.
|
|
46
|
+
*/
|
|
47
|
+
export declare const ANONYMOUS_BOOK_FILE_RATE_LIMIT_PER_HOUR = 120;
|
|
48
|
+
/** Filter columns of `GET /books`, on top of {@link BASE_FILTER_COLUMNS}. */
|
|
49
|
+
export declare const BOOK_FILTER_COLUMNS: readonly ["title", "author", "format", "user_id"];
|
|
50
|
+
/** `extra_options` keys of `GET /books`. */
|
|
51
|
+
export declare const BOOK_EXTRA_OPTION_KEYS: readonly ["scope", "owner_handle", "tags"];
|
|
52
|
+
/** `extra_options` of `GET /books`. {@link ListBooksParams} has camelCased shortcuts for each key. */
|
|
53
|
+
export interface BookExtraOptions {
|
|
54
|
+
readonly scope?: "mine" | "explore";
|
|
55
|
+
readonly owner_handle?: string;
|
|
56
|
+
readonly tags?: readonly string[];
|
|
57
|
+
}
|
|
58
|
+
/** Shortcuts over the filter columns of `GET /books`. */
|
|
59
|
+
export interface BookFilters {
|
|
60
|
+
/** Partial, accent-insensitive match on the title (`search[title]`). */
|
|
61
|
+
readonly title?: string;
|
|
62
|
+
/** Partial, accent-insensitive match on the author (`search[author]`). */
|
|
63
|
+
readonly author?: string;
|
|
64
|
+
/** Exact match. Use it to split a shelf into PDFs and EPUBs. */
|
|
65
|
+
readonly format?: BookFormat;
|
|
66
|
+
/** Exact owner id. For a handle, use {@link ListBooksParams.ownerHandle}. */
|
|
67
|
+
readonly userId?: Id;
|
|
68
|
+
/** Exact ids. An array becomes `IN (...)`. */
|
|
69
|
+
readonly ids?: readonly BookId[];
|
|
70
|
+
}
|
|
71
|
+
/** Arguments for {@link LibraryBooksNamespace.list}. */
|
|
72
|
+
export interface ListBooksParams extends BookFilters, ListParams<(typeof BOOK_FILTER_COLUMNS)[number], BookExtraOptions> {
|
|
73
|
+
/**
|
|
74
|
+
* `"mine"` narrows to the caller's own books, `"explore"` to everybody
|
|
75
|
+
* else's. Omitted, the listing is own + all public, mixed together.
|
|
76
|
+
*
|
|
77
|
+
* `"explore"` is a no-op for an anonymous caller (there is no "else" to
|
|
78
|
+
* exclude), which is correct rather than a bug: an anonymous listing is
|
|
79
|
+
* already nothing but other people's public books.
|
|
80
|
+
*/
|
|
81
|
+
readonly scope?: "mine" | "explore";
|
|
82
|
+
/**
|
|
83
|
+
* Narrow to one owner by handle, matched case-insensitively.
|
|
84
|
+
*
|
|
85
|
+
* An unknown handle returns an EMPTY list, not a 404. Do not read empty as
|
|
86
|
+
* "this person has no public books".
|
|
87
|
+
*
|
|
88
|
+
* Sending both this and `scope: "explore"` is contradictory when the handle
|
|
89
|
+
* is your own; {@link LibraryBooksNamespace.explore} drops the scope whenever
|
|
90
|
+
* a handle is given.
|
|
91
|
+
*/
|
|
92
|
+
readonly ownerHandle?: string;
|
|
93
|
+
/**
|
|
94
|
+
* Books carrying ALL of these tags (a `jsonb @>` containment test, so it is
|
|
95
|
+
* AND, never OR). Stripped and downcased before matching, exactly as tags
|
|
96
|
+
* are stored.
|
|
97
|
+
*/
|
|
98
|
+
readonly tags?: readonly string[];
|
|
99
|
+
/** `"column:asc"` / `"column:desc"`. See {@link LibraryBooksNamespace.list}. */
|
|
100
|
+
readonly order?: string;
|
|
101
|
+
/** Random ordering. Disables the `ETag`, and is not stable across pages. */
|
|
102
|
+
readonly random?: boolean;
|
|
103
|
+
}
|
|
104
|
+
/** Arguments for {@link LibraryBooksNamespace.create}. */
|
|
105
|
+
export interface CreateBookInput {
|
|
106
|
+
/**
|
|
107
|
+
* The book. Required, and the ONLY thing that decides the format: the server
|
|
108
|
+
* reads the extension, falls back to the content type, and refuses anything
|
|
109
|
+
* that is not a PDF or an EPUB.
|
|
110
|
+
*
|
|
111
|
+
* On React Native pass the picker's `{ uri, name, type }` object directly -
|
|
112
|
+
* it is appended verbatim and streamed off disk by the native layer. Make
|
|
113
|
+
* sure `name` carries the real extension, because a `uri` alone does not.
|
|
114
|
+
*/
|
|
115
|
+
readonly file: FileInput | NativeFile;
|
|
116
|
+
/** Optional cover image. Re-encoded to a WebP thumbnail server-side. */
|
|
117
|
+
readonly cover?: FileInput | NativeFile;
|
|
118
|
+
/**
|
|
119
|
+
* Defaults to the FILENAME without its extension when omitted or blank.
|
|
120
|
+
* That is a real fallback, not an error path, so an untitled upload gets
|
|
121
|
+
* `"war-and-peace"` rather than being rejected.
|
|
122
|
+
*/
|
|
123
|
+
readonly title?: string;
|
|
124
|
+
readonly author?: string;
|
|
125
|
+
readonly description?: string;
|
|
126
|
+
readonly isbn?: string;
|
|
127
|
+
readonly language?: string;
|
|
128
|
+
/** Coerced with `to_i` server-side, so `"abc"` becomes `0`, not an error. */
|
|
129
|
+
readonly pageCount?: number;
|
|
130
|
+
/** Stripped, downcased, de-duplicated, truncated, and cut to the first 30. */
|
|
131
|
+
readonly tags?: readonly string[];
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* Arguments for {@link LibraryBooksNamespace.update}.
|
|
135
|
+
*
|
|
136
|
+
* `null` CLEARS a column. It works in both encodings: as JSON it is a literal
|
|
137
|
+
* `null`, and as multipart it is written as the `\b` sentinel, which the
|
|
138
|
+
* server decodes back to a null for update params exactly as it does for
|
|
139
|
+
* filters. `undefined` leaves the column alone.
|
|
140
|
+
*/
|
|
141
|
+
export interface UpdateBookInput {
|
|
142
|
+
readonly title?: string;
|
|
143
|
+
readonly author?: string | null;
|
|
144
|
+
readonly description?: string | null;
|
|
145
|
+
readonly isbn?: string | null;
|
|
146
|
+
readonly language?: string | null;
|
|
147
|
+
readonly pageCount?: number | null;
|
|
148
|
+
/** The one field that decides who can see the book. Not settable at create. */
|
|
149
|
+
readonly visibility?: BookVisibility;
|
|
150
|
+
/** Replaces the whole list. There is no add or remove. */
|
|
151
|
+
readonly tags?: readonly string[];
|
|
152
|
+
/**
|
|
153
|
+
* Replaces the cover. Its presence is what forces the request to multipart;
|
|
154
|
+
* every other field goes as JSON.
|
|
155
|
+
*
|
|
156
|
+
* There is no way to REMOVE a cover through this endpoint: the field is
|
|
157
|
+
* only read when present, and `cover: null` clears nothing.
|
|
158
|
+
*/
|
|
159
|
+
readonly cover?: FileInput | NativeFile;
|
|
160
|
+
}
|
|
161
|
+
/** A byte range of a book file, as {@link LibraryBooksNamespace.fileRange} returns it. */
|
|
162
|
+
export interface BookFileRange {
|
|
163
|
+
/** The bytes. */
|
|
164
|
+
readonly data: ArrayBuffer;
|
|
165
|
+
/** First byte offset, inclusive, as the server reported it. */
|
|
166
|
+
readonly start: number;
|
|
167
|
+
/** Last byte offset, INCLUSIVE. HTTP ranges are closed at both ends. */
|
|
168
|
+
readonly end: number;
|
|
169
|
+
/** Total size of the file, from the `Content-Range` header. */
|
|
170
|
+
readonly total: number;
|
|
171
|
+
/** Pinned to the book's format server-side, never sniffed from the bytes. */
|
|
172
|
+
readonly contentType: string;
|
|
173
|
+
}
|
|
174
|
+
/** The `library.books` namespace, reachable as `oms.library.books`. */
|
|
175
|
+
export declare class LibraryBooksNamespace extends Resource {
|
|
176
|
+
/**
|
|
177
|
+
* `GET /books` - browse the library.
|
|
178
|
+
*
|
|
179
|
+
* WORKS ANONYMOUSLY, and what it returns depends on who is asking: every
|
|
180
|
+
* `public` book plus, for a signed-in caller, their own books whatever
|
|
181
|
+
* their visibility. `unlisted` books are absent by
|
|
182
|
+
* construction - that is what unlisted means - so a reader's own unlisted
|
|
183
|
+
* book DOES appear here (it is theirs) while somebody else's never will.
|
|
184
|
+
*
|
|
185
|
+
* Pagination is FORCED: a request with no page modifier gets `1:500`, and
|
|
186
|
+
* any larger size is clamped back to 500. That is a DoS guard - an
|
|
187
|
+
* unbounded listing holds a server thread and a database connection for as
|
|
188
|
+
* long as the serialisation takes.
|
|
189
|
+
* The SDK always sends a page modifier, so {@link Paginated.pageSize} always
|
|
190
|
+
* reports the size the rows were counted against.
|
|
191
|
+
*
|
|
192
|
+
* Order defaults to whatever Postgres returns, which is NOT stable across
|
|
193
|
+
* pages. Pass an explicit `order` for any paged walk; `"created_at:desc"` is
|
|
194
|
+
* what every library screen uses. An unknown column in `order` is IGNORED in
|
|
195
|
+
* silence, so a typo costs you the ordering and no error.
|
|
196
|
+
*
|
|
197
|
+
* The response carries an `ETag` and honours `If-None-Match` (except with
|
|
198
|
+
* `random`), so a repeated identical listing is cheap for the server even
|
|
199
|
+
* though the SDK does not cache it for you.
|
|
200
|
+
*
|
|
201
|
+
* @throws {OmsApiError} 400 naming the offending key when a filter is not
|
|
202
|
+
* one of `title`, `author`, `format`, `user_id`, `id`, `created_at`,
|
|
203
|
+
* `updated_at`. Filters fail closed on purpose: silently dropping an
|
|
204
|
+
* unknown one used to answer with the UNFILTERED set.
|
|
205
|
+
*/
|
|
206
|
+
list(params?: ListBooksParams, options?: RequestOptions): Promise<Paginated<Book>>;
|
|
207
|
+
/**
|
|
208
|
+
* The caller's own books, whatever their visibility. Sugar for
|
|
209
|
+
* {@link list} with `scope: "mine"`.
|
|
210
|
+
*
|
|
211
|
+
* Needs a credential to mean anything: the scope narrows to the caller's own
|
|
212
|
+
* `user_id`, and for an anonymous caller that matches no row. An anonymous
|
|
213
|
+
* call therefore returns an EMPTY list rather than a 401 - the endpoint is
|
|
214
|
+
* genuinely public and has nothing to complain about.
|
|
215
|
+
*/
|
|
216
|
+
mine(params?: Omit<ListBooksParams, "scope">, options?: RequestOptions): Promise<Paginated<Book>>;
|
|
217
|
+
/**
|
|
218
|
+
* Other people's public books. Sugar for {@link list} with
|
|
219
|
+
* `scope: "explore"`, or with `ownerHandle` when one is given.
|
|
220
|
+
*
|
|
221
|
+
* The scope is DROPPED as soon as a handle is supplied: asking for
|
|
222
|
+
* "everybody but me, and also only this person" reads as a filter but
|
|
223
|
+
* behaves as a way to make your own library invisible when you pass your
|
|
224
|
+
* own handle.
|
|
225
|
+
*/
|
|
226
|
+
explore(params?: Omit<ListBooksParams, "scope">, options?: RequestOptions): Promise<Paginated<Book>>;
|
|
227
|
+
/**
|
|
228
|
+
* `GET /books/:id` - one book.
|
|
229
|
+
*
|
|
230
|
+
* The route an `unlisted` share link resolves through, so it works
|
|
231
|
+
* anonymously and its scope is deliberately WIDER than the listing's:
|
|
232
|
+
* public, unlisted, plus your own.
|
|
233
|
+
*
|
|
234
|
+
* Returns exactly what a row of {@link list} carries.
|
|
235
|
+
*
|
|
236
|
+
* @throws {OmsApiError} 404 `"Resource not found"` - for an id that does not
|
|
237
|
+
* exist AND for a private book belonging to someone else, indistinguishably.
|
|
238
|
+
* The scope is applied before the id is compared, which is what stops the
|
|
239
|
+
* endpoint being an existence oracle for other people's private books.
|
|
240
|
+
*/
|
|
241
|
+
get(id: BookId, options?: RequestOptions): Promise<Book>;
|
|
242
|
+
/**
|
|
243
|
+
* `POST /books` - uploads a PDF or an EPUB and adds it to the library.
|
|
244
|
+
*
|
|
245
|
+
* Multipart, synchronous, and slow: Rails buffers the whole body, writes the
|
|
246
|
+
* file to storage, and derives a WebP thumbnail when a cover came with it.
|
|
247
|
+
* The per-attempt deadline defaults to five minutes here instead of the
|
|
248
|
+
* client's usual one, and the call is NOT retried - a replay after a lost
|
|
249
|
+
* answer uploads the book twice.
|
|
250
|
+
*
|
|
251
|
+
* ## Two size ceilings, and the one you will actually hit
|
|
252
|
+
*
|
|
253
|
+
* Rails refuses anything over {@link BOOK_MAX_FILE_BYTES} (100 MiB) with
|
|
254
|
+
* `413 "File too big (max 100 MB)"`. In production that message is nearly
|
|
255
|
+
* unreachable, because Cloudflare sits in front and refuses a request body
|
|
256
|
+
* over roughly {@link BOOK_UPLOAD_EDGE_LIMIT_BYTES} with its OWN `413`
|
|
257
|
+
* before the origin sees a byte. That one arrives as an
|
|
258
|
+
* {@link OmsApiError} whose body is an HTML error page and which mentions
|
|
259
|
+
* neither books nor a limit, so a caller matching on the message will not
|
|
260
|
+
* recognise it. Match on `status === 413` instead, and say "the file is too
|
|
261
|
+
* large" whatever the body claims.
|
|
262
|
+
*
|
|
263
|
+
* There is no chunked route for books, so splitting the file is not an
|
|
264
|
+
* option; a 400 MB scan has to be shrunk before it can be uploaded. (This is
|
|
265
|
+
* the same ceiling `songs.import` documents, hit far more often here: a
|
|
266
|
+
* scanned book is routinely bigger than an album.)
|
|
267
|
+
*
|
|
268
|
+
* This method checks the size LOCALLY first when the size is knowable - a
|
|
269
|
+
* `Blob`, a `Uint8Array`, or a picked file whose descriptor reported one -
|
|
270
|
+
* and throws before spending several minutes uploading something that will
|
|
271
|
+
* be refused. A `ReadableStream` reports nothing and is sent as-is.
|
|
272
|
+
*
|
|
273
|
+
* ## What the server decides for you
|
|
274
|
+
*
|
|
275
|
+
* - **Format**, from the filename extension, falling back to the content
|
|
276
|
+
* type. A `.pdf` that is really an EPUB is stored as a PDF and will not
|
|
277
|
+
* open; the reader pins `Content-Type` to this value and sends
|
|
278
|
+
* `X-Content-Type-Options: nosniff`, so nothing downstream corrects it.
|
|
279
|
+
* - **Title**, when you send none: the filename without its extension.
|
|
280
|
+
* - **Visibility**: always `private`. The field is never read at creation,
|
|
281
|
+
* so passing one changes nothing. Sharing a freshly uploaded book is
|
|
282
|
+
* always a second call to {@link update}.
|
|
283
|
+
* - **`file_size_bytes`**, from the stored node. Not from anything you send.
|
|
284
|
+
*
|
|
285
|
+
* @throws {TypeError} before any request when the file is over the edge
|
|
286
|
+
* ceiling and its size was knowable locally.
|
|
287
|
+
* @throws {OmsAuthError} 401 `"Session required"` when anonymous. Note it is
|
|
288
|
+
* checked in the action, so this is a 401 and not the 403 an OAuth token
|
|
289
|
+
* would get.
|
|
290
|
+
* @throws {OmsApiError} 413 for an oversized body (from Rails, or from the
|
|
291
|
+
* edge - see above); 400 `"No file provided"`, `"Unsupported file type,
|
|
292
|
+
* only PDF and EPUB are accepted"`, `"Library storage unavailable"`, or the
|
|
293
|
+
* model's validation messages.
|
|
294
|
+
*/
|
|
295
|
+
create(input: CreateBookInput, options?: RequestOptions): Promise<Book>;
|
|
296
|
+
/**
|
|
297
|
+
* `PATCH /books/:id` - edits metadata, visibility, and optionally the cover.
|
|
298
|
+
*
|
|
299
|
+
* JSON normally; multipart as soon as {@link UpdateBookInput.cover} is
|
|
300
|
+
* present, because that is the only way to carry a file. Both encodings reach
|
|
301
|
+
* the same code path, and this method papers over the one place they would
|
|
302
|
+
* otherwise differ: clearing a column. Every multipart field is a string, so
|
|
303
|
+
* there is no `null` to send - the backend's `\b` sentinel is decoded for
|
|
304
|
+
* update params exactly as it is for filters, and this writes it for you when
|
|
305
|
+
* you pass `null`.
|
|
306
|
+
*
|
|
307
|
+
* This is where a book becomes shareable. `visibility` is the only field that
|
|
308
|
+
* changes who can reach it, and it cannot be set at upload time.
|
|
309
|
+
*
|
|
310
|
+
* Replacing the cover leaves the OLD cover nodes behind as orphans until the
|
|
311
|
+
* book itself is destroyed: a new node is created and the book pointed at
|
|
312
|
+
* it, and only the previous compressed twin is destroyed. Not a bug you can
|
|
313
|
+
* fix from here, but it is why re-cropping a cover repeatedly costs storage
|
|
314
|
+
* quota.
|
|
315
|
+
*
|
|
316
|
+
* @throws {OmsAuthError} 401 when the book is not yours - the ownership
|
|
317
|
+
* check answers 401, not 403.
|
|
318
|
+
* @throws {OmsApiError} 404 when the id is unknown or the book is someone
|
|
319
|
+
* else's PRIVATE one (the lookup is scoped first); 400 with the model's
|
|
320
|
+
* validation messages, e.g. a `visibility` outside
|
|
321
|
+
* {@link BOOK_VISIBILITIES} or a title over
|
|
322
|
+
* {@link BOOK_TITLE_MAX_LENGTH}.
|
|
323
|
+
*/
|
|
324
|
+
update(id: BookId, input: UpdateBookInput, options?: RequestOptions): Promise<Book>;
|
|
325
|
+
/**
|
|
326
|
+
* `DELETE /books/:id` - removes the book, its file, its covers, its
|
|
327
|
+
* annotations and its place on every shelf.
|
|
328
|
+
*
|
|
329
|
+
* Genuinely destructive and not recoverable: the stored PDF or EPUB goes
|
|
330
|
+
* with the row, and so do every reader's highlights on it.
|
|
331
|
+
*
|
|
332
|
+
* @throws {OmsAuthError} 401 when the book is not yours.
|
|
333
|
+
* @throws {OmsApiError} 404 the second time, because the row is already gone.
|
|
334
|
+
* That is why this is never retried on a torn connection: a replay would
|
|
335
|
+
* report "not found" for a delete that worked perfectly well.
|
|
336
|
+
*/
|
|
337
|
+
delete(id: BookId, options?: RequestOptions): Promise<void>;
|
|
338
|
+
/**
|
|
339
|
+
* `GET /books/libraries` - every reader who opted their library public and
|
|
340
|
+
* has at least one public book.
|
|
341
|
+
*
|
|
342
|
+
* A directory of PEOPLE, not of books - see {@link PublicLibrary}. Anonymous,
|
|
343
|
+
* unpaged, and cheap: the set is small by construction and the counts come
|
|
344
|
+
* from one grouped query.
|
|
345
|
+
*
|
|
346
|
+
* A reader appears only when BOTH halves are true: `library_public` on their
|
|
347
|
+
* user record (set through `account.update`, not from this namespace) AND at
|
|
348
|
+
* least one book at `public`. Making a single book public does not publish a
|
|
349
|
+
* library, and publishing a library with only unlisted books lists nobody.
|
|
350
|
+
*/
|
|
351
|
+
libraries(options?: RequestOptions): Promise<PublicLibrary[]>;
|
|
352
|
+
/**
|
|
353
|
+
* `GET /books/:id/file` - the raw bytes, for an in-app reader.
|
|
354
|
+
*
|
|
355
|
+
* The one media route in this namespace that does NOT redirect: Rails serves
|
|
356
|
+
* the bytes itself, same-origin to the API, precisely so a reader
|
|
357
|
+
* (`react-pdf`, `epub.js`) can pull them over XHR without a cross-origin hop
|
|
358
|
+
* into object storage. It sends `Accept-Ranges: bytes`, pins `Content-Type`
|
|
359
|
+
* to the validated format, and sets `X-Content-Type-Options: nosniff` so a
|
|
360
|
+
* doctored `.pdf` full of HTML cannot be rendered as a document.
|
|
361
|
+
*
|
|
362
|
+
* Conditional GET is deliberately DISABLED on this route
|
|
363
|
+
* (`Cache-Control: no-cache, no-store, must-revalidate`): the body would
|
|
364
|
+
* otherwise carry an `ETag` and the browser would revalidate into an empty
|
|
365
|
+
* `304` that an XHR reader cannot parse. So every call is a full transfer
|
|
366
|
+
* and caching is the client's job.
|
|
367
|
+
*
|
|
368
|
+
* **This reads the whole book into memory twice** - once on the server and
|
|
369
|
+
* once here. For a large PDF prefer
|
|
370
|
+
* {@link fileRange}, which is what a PDF viewer does on its own, or
|
|
371
|
+
* {@link fileUrl} plus a native downloader on a phone.
|
|
372
|
+
*
|
|
373
|
+
* Anonymous callers are limited to
|
|
374
|
+
* {@link ANONYMOUS_BOOK_FILE_RATE_LIMIT_PER_HOUR} reads an hour per IP across
|
|
375
|
+
* this route and {@link download} together; a signed-in caller is not
|
|
376
|
+
* limited by it. The route is also exempt from the general 600/min ceiling,
|
|
377
|
+
* so a Range-reading viewer opening one book cannot trip it.
|
|
378
|
+
*
|
|
379
|
+
* @throws {OmsApiError} 404 `"No file attached"` when the row exists but its
|
|
380
|
+
* node lost its blob, and `"Resource not found"` when the book is not
|
|
381
|
+
* visible to the caller; 429 for an anonymous caller over the hourly cap.
|
|
382
|
+
*/
|
|
383
|
+
file(id: BookId, options?: RequestOptions): Promise<FileOutput>;
|
|
384
|
+
/**
|
|
385
|
+
* `GET /books/:id/file` with a `Range` header - one slice of the book.
|
|
386
|
+
*
|
|
387
|
+
* This is how a PDF viewer opens a 60 MB scan in under a second: it reads the
|
|
388
|
+
* trailer, then the pages it needs, and never downloads the rest. The server
|
|
389
|
+
* answers `206` with `Content-Range` and reads only the slice, so the origin
|
|
390
|
+
* does not read the whole file either.
|
|
391
|
+
*
|
|
392
|
+
* `end` is INCLUSIVE, as HTTP ranges are: `{ start: 0, end: 1023 }` is the
|
|
393
|
+
* first 1024 bytes. Omit `end` for "from here to the end of the file". A
|
|
394
|
+
* suffix range (the LAST n bytes) is not offered here because the server's
|
|
395
|
+
* `Content-Range` is the only way back to absolute offsets and this method
|
|
396
|
+
* would have to guess; send the header yourself through `http.raw` if you
|
|
397
|
+
* need one.
|
|
398
|
+
*
|
|
399
|
+
* ONE range per request. The server answers `416` to a multi-range header
|
|
400
|
+
* rather than building a multipart/byteranges body, so do not batch.
|
|
401
|
+
*
|
|
402
|
+
* @throws {OmsApiError} 416 when the range starts past the end of the file,
|
|
403
|
+
* or when more than one range was asked for. The response carries
|
|
404
|
+
* a `Content-Range` naming the real size, with a star where the range
|
|
405
|
+
* would be. That is how a caller learns the size after guessing wrong.
|
|
406
|
+
* @throws {OmsError} when the server answered `200` instead of `206` - a
|
|
407
|
+
* proxy that stripped the `Range` header, which would otherwise be read as
|
|
408
|
+
* a slice and quietly corrupt the reader's buffer.
|
|
409
|
+
*/
|
|
410
|
+
fileRange(id: BookId, range: {
|
|
411
|
+
start: number;
|
|
412
|
+
end?: number;
|
|
413
|
+
}, options?: RequestOptions): Promise<BookFileRange>;
|
|
414
|
+
/**
|
|
415
|
+
* Absolute URL of the book's bytes, for a reader that fetches them itself.
|
|
416
|
+
*
|
|
417
|
+
* Synchronous, and it carries NO CREDENTIAL - it cannot, because a token
|
|
418
|
+
* provider may be async and this is called while a component renders. What
|
|
419
|
+
* that means in practice depends on the book:
|
|
420
|
+
*
|
|
421
|
+
* - a `public` or `unlisted` book opens with no credential at all;
|
|
422
|
+
* - a `private` one needs one, and the caller has to attach it. For pdf.js
|
|
423
|
+
* that is `httpHeaders: { Authorization: 'Bearer ...' }` on a token client,
|
|
424
|
+
* or `withCredentials: true` on a cookie client. Both, on a client that has
|
|
425
|
+
* both, would be a request the API refuses to disambiguate.
|
|
426
|
+
*
|
|
427
|
+
* Prefer NOT to append `?token=...`. The API does accept a query token, but
|
|
428
|
+
* it puts a live session credential into access logs and into anything that
|
|
429
|
+
* copies the URL - a pasted link hands over the account, and the token never
|
|
430
|
+
* expires on its own.
|
|
431
|
+
*
|
|
432
|
+
* A query token is NOT counted as anonymous: a valid `?token=` resolves to a
|
|
433
|
+
* live session and the request is keyed to that session in the
|
|
434
|
+
* authenticated 600/min bucket. The leak is the reason to avoid it; the
|
|
435
|
+
* budget is not.
|
|
436
|
+
*/
|
|
437
|
+
fileUrl(id: BookId): string;
|
|
438
|
+
/**
|
|
439
|
+
* `GET /books/:id/cover` - `302` to the cover image in object storage.
|
|
440
|
+
*
|
|
441
|
+
* Prefer {@link coverUrl} in markup. This method exists for when the bytes
|
|
442
|
+
* themselves are wanted, and it sends the caller's credential because it has
|
|
443
|
+
* to: the book is resolved through the caller's visibility, so your own
|
|
444
|
+
* private book's cover is a `404` without one.
|
|
445
|
+
*
|
|
446
|
+
* ## It does not work from a browser, and that is not fixable here
|
|
447
|
+
*
|
|
448
|
+
* The `302` goes to `minio.omelhorsite.pt`. Per the Fetch standard a CORS
|
|
449
|
+
* request redirected cross-origin gets an opaque origin on the next hop, MinIO
|
|
450
|
+
* answers a null origin with `Access-Control-Allow-Origin: *`, and a wildcard
|
|
451
|
+
* is illegal for a credentialed request - so the browser rejects the response
|
|
452
|
+
* before any JavaScript sees it. `account.picture` solves this by sending no
|
|
453
|
+
* credential; that is not available here, because a private cover needs one.
|
|
454
|
+
*
|
|
455
|
+
* In a browser, put {@link coverUrl} in an `<img>` and let the platform
|
|
456
|
+
* follow the redirect with no CORS check at all. In Bun, in a Worker and on
|
|
457
|
+
* React Native, where no CORS is enforced, this method is fine.
|
|
458
|
+
*
|
|
459
|
+
* @throws {OmsApiError} 404 `"No cover"` when the book has none, and
|
|
460
|
+
* `"Resource not found"` when it is not visible to the caller.
|
|
461
|
+
*/
|
|
462
|
+
cover(id: BookId, options?: RequestOptions): Promise<FileOutput>;
|
|
463
|
+
/**
|
|
464
|
+
* Absolute URL of the cover, for an `<img>` or a `background-image`.
|
|
465
|
+
*
|
|
466
|
+
* Synchronous, uncredentialed, and safe to put in markup for a `public` or
|
|
467
|
+
* `unlisted` book. A PRIVATE book's cover answers `404` through this URL
|
|
468
|
+
* unless the platform attaches the session cookie for you - which an `<img>`
|
|
469
|
+
* on the same site does and an element with `crossorigin="anonymous"` does
|
|
470
|
+
* not. Give the element an `onError` that falls back to a generated cover.
|
|
471
|
+
*
|
|
472
|
+
* The redirect target is a presigned URL, so do not cache this URL's
|
|
473
|
+
* RESOLVED destination anywhere; cache this URL instead.
|
|
474
|
+
*/
|
|
475
|
+
coverUrl(id: BookId): string;
|
|
476
|
+
/**
|
|
477
|
+
* `GET /books/:id/download` - `302` to the book file as an attachment.
|
|
478
|
+
*
|
|
479
|
+
* The difference from {@link file} is only the disposition: this one is
|
|
480
|
+
* `attachment` with a slugged filename (`war-and-peace.pdf`), and it goes
|
|
481
|
+
* through object storage rather than through Rails. Same CORS caveat as
|
|
482
|
+
* {@link cover} - see it - and the same anonymous ceiling of
|
|
483
|
+
* {@link ANONYMOUS_BOOK_FILE_RATE_LIMIT_PER_HOUR} per hour per IP.
|
|
484
|
+
*
|
|
485
|
+
* On a phone, do not pull a whole book through JavaScript for the sake of
|
|
486
|
+
* saving it: hand {@link downloadUrl} to a native downloader.
|
|
487
|
+
*/
|
|
488
|
+
download(id: BookId, options?: RequestOptions): Promise<FileOutput>;
|
|
489
|
+
/**
|
|
490
|
+
* Absolute URL that saves the book as a file. Synchronous and
|
|
491
|
+
* uncredentialed, with the same visibility caveat as {@link coverUrl}.
|
|
492
|
+
*
|
|
493
|
+
* Suitable for an `<a href>` or for a native downloader. The filename comes
|
|
494
|
+
* from the server's `Content-Disposition`, so there is no need for a
|
|
495
|
+
* `download` attribute - which a cross-origin link ignores anyway.
|
|
496
|
+
*/
|
|
497
|
+
downloadUrl(id: BookId): string;
|
|
498
|
+
/**
|
|
499
|
+
* `POST /books/:id/progress` - upserts "where I left off" for this reader.
|
|
500
|
+
*
|
|
501
|
+
* An UPSERT, not a create: there is exactly one progress marker per reader
|
|
502
|
+
* per book however many times this is called, and no id is needed. Call it
|
|
503
|
+
* freely as the reader turns pages.
|
|
504
|
+
*
|
|
505
|
+
* Private to the reader even on a public book, and it works on somebody
|
|
506
|
+
* else's public book - which is the point: reading a shared book should
|
|
507
|
+
* remember where you were.
|
|
508
|
+
*
|
|
509
|
+
* `location` is opaque `jsonb`. A blank or omitted one is stored as `{}`,
|
|
510
|
+
* which reads back as "no position recorded" rather than as page zero.
|
|
511
|
+
*
|
|
512
|
+
* Returns the annotation, so the row that comes back is the same shape
|
|
513
|
+
* {@link LibraryAnnotationsNamespace} deals in and can be dropped straight
|
|
514
|
+
* into a client-side cache of markers.
|
|
515
|
+
*
|
|
516
|
+
* @throws {OmsAuthError} 401 when anonymous - unlike the read routes, this
|
|
517
|
+
* one needs a session.
|
|
518
|
+
* @throws {OmsApiError} 404 when the book is not visible to the caller.
|
|
519
|
+
*/
|
|
520
|
+
saveProgress(id: BookId, location: BookAnnotationLocation, options?: RequestOptions): Promise<BookAnnotation>;
|
|
521
|
+
/** JSON body for an update. `null` stays `null`; the transport does not touch a body. */
|
|
522
|
+
private updateBody;
|
|
523
|
+
/**
|
|
524
|
+
* Multipart fields for an update, with the one encoding difference fixed.
|
|
525
|
+
*
|
|
526
|
+
* `null` becomes the `\b` sentinel, which the server decodes back to a null
|
|
527
|
+
* for update params exactly as it does for filters - a form field has no
|
|
528
|
+
* other way to say "clear this column". An empty `tags` list is sent as a
|
|
529
|
+
* single empty string, because appending an empty array appends nothing and
|
|
530
|
+
* an absent key means "leave the tags alone", which is the opposite of what
|
|
531
|
+
* clearing them means. The server drops the blank entry, leaving `[]`.
|
|
532
|
+
*/
|
|
533
|
+
private multipartUpdateFields;
|
|
534
|
+
}
|