@everystack/api 0.4.2 → 0.4.4
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/CHANGELOG.md +90 -1
- package/package.json +1 -1
- package/src/client/index.ts +13 -3
- package/src/handler/index.ts +808 -156
- package/src/handler/service-unavailable.ts +48 -0
- package/src/handler/thrown.ts +32 -0
package/CHANGELOG.md
CHANGED
|
@@ -4,13 +4,102 @@ All notable changes to `@everystack/api` are documented here.
|
|
|
4
4
|
|
|
5
5
|
Format based on [Keep a Changelog](https://keepachangelog.com/).
|
|
6
6
|
|
|
7
|
-
## [
|
|
7
|
+
## [0.4.4] - 2026-10-15
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
|
|
11
|
+
- PATCH honours ?deleted=include|only so a soft-deleted row can be restored
|
|
12
|
+
|
|
13
|
+
### Fixed
|
|
14
|
+
|
|
15
|
+
- refuse a JOIN embed list that names no real column
|
|
16
|
+
- restore through PATCH is off unless softDelete.restore names the caller's role
|
|
17
|
+
- a PATCH whose final body has nothing to write answers 400, not 500
|
|
18
|
+
|
|
19
|
+
## [0.4.3] - 2026-10-14
|
|
20
|
+
|
|
21
|
+
### BREAKING CHANGES
|
|
22
|
+
|
|
23
|
+
- rowOwnership fails closed
|
|
24
|
+
- a public route with a token answers 503 when the verifier is down
|
|
25
|
+
|
|
26
|
+
### Added
|
|
27
|
+
|
|
28
|
+
- a throwing after-hook reaches onError as kind 'hook'
|
|
29
|
+
|
|
30
|
+
### Fixed
|
|
31
|
+
|
|
32
|
+
- guard explicit hidden column query references
|
|
33
|
+
- check select depth before hidden column traversal
|
|
34
|
+
- audit refused hidden column requests
|
|
35
|
+
- exclude hidden columns from relation embeds
|
|
36
|
+
- resolve embed exclusions by column identity
|
|
37
|
+
- fail closed on unresolved embed hidden columns
|
|
38
|
+
- warn on invalid hidden column configuration
|
|
39
|
+
- rowOwnership exceptRoles lifts the backstop for manage roles
|
|
40
|
+
- an excepted role must filter its PATCH; role claim must be a string
|
|
41
|
+
- a write filter must build a SQL condition
|
|
42
|
+
- an infrastructure outage answers 503, never 401, and never leaks driver text
|
|
43
|
+
- classify a wrapped outage cause, and close the review findings
|
|
44
|
+
- the outage 503 is never cached
|
|
45
|
+
- an empty owner value is not an owner
|
|
46
|
+
- an owner cannot reassign the ownership column
|
|
47
|
+
- rowOwnership throws on an unknown table
|
|
48
|
+
- a non-string role claim never satisfies a role gate
|
|
49
|
+
- rowOwnership.exceptRoles requires pgSettings
|
|
50
|
+
- an excepted write needs a role from pgSettings
|
|
51
|
+
- the unbacked-exemption 500 reaches onError
|
|
52
|
+
- a throwing before-hook reaches onError
|
|
53
|
+
- an auth outage 503 reaches onError
|
|
54
|
+
- the auth-outage report never throws and names only known routes
|
|
55
|
+
- an RPC name resolves only to an own key of rpc
|
|
56
|
+
- a JWKS timeout and a bad service credential answer 503
|
|
57
|
+
- a table request that hits an outage answers 503
|
|
58
|
+
- an outage fault names only a declared RPC
|
|
59
|
+
- reporting a fault never throws on an unreadable error
|
|
60
|
+
- an outage inside a before-hook answers 503
|
|
61
|
+
- an after-hook fault no longer claims the write committed
|
|
62
|
+
- after-hooks run after the commit
|
|
63
|
+
- before-hook faults are reported after the transaction
|
|
64
|
+
- guard every read of a thrown value in the handler
|
|
65
|
+
- resolve embeds and table-name twins by own keys only
|
|
66
|
+
|
|
67
|
+
### Documentation
|
|
68
|
+
|
|
69
|
+
- say that an RPC name resolves only to an own key
|
|
70
|
+
- say why the 2xx guard stands and what a retry wrapper must reset
|
|
71
|
+
- rowOwnership exceptRoles
|
|
72
|
+
- rowOwnership fails closed, and createHandler validates it
|
|
73
|
+
- name the breaks, and correct the non-owner write response
|
|
74
|
+
- an owner cannot change the ownership column on PATCH
|
|
75
|
+
- the protectedFields strip is per spelling; the owner compare is on strings
|
|
76
|
+
- rowOwnership refuses a table it cannot find anywhere
|
|
77
|
+
- say what the unknown-table check cannot catch
|
|
78
|
+
- a thrown non-Error has no stack in the onError fault
|
|
79
|
+
- tell a verifier that logs in as the caller to catch class 28
|
|
80
|
+
- publicRoutes and publicRpc skip the token, not the client check
|
|
81
|
+
- afterRpc runs after the commit, the table after-hooks before it
|
|
82
|
+
|
|
83
|
+
## [0.4.2] - 2026-09-22
|
|
84
|
+
|
|
85
|
+
### Fixed
|
|
86
|
+
|
|
87
|
+
- refresh expired JWT instead of silently failing every request
|
|
88
|
+
|
|
89
|
+
## [0.4.1] - 2026-09-22
|
|
8
90
|
|
|
9
91
|
### Added
|
|
10
92
|
|
|
11
93
|
- auto-generated changelogs from conventional commits
|
|
12
94
|
- automate the version-drift check — a manual note is not a gate
|
|
13
95
|
|
|
96
|
+
### Fixed
|
|
97
|
+
|
|
98
|
+
- coerce date-typed column values before insert/update
|
|
99
|
+
- review follow-ups on date-column coercion
|
|
100
|
+
- second review pass -- ISO strictness, null rows, sibling array-safety
|
|
101
|
+
- reject offset-less datetime strings (ambiguous: server-local, not UTC)
|
|
102
|
+
|
|
14
103
|
## [0.4.0] - 2026-07-14
|
|
15
104
|
|
|
16
105
|
### Added
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@everystack/api",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.4",
|
|
4
4
|
"description": "PostgREST-compatible HTTP handler + typed fetch client for Drizzle ORM",
|
|
5
5
|
"license": "AGPL-3.0-only",
|
|
6
6
|
"author": "Scalable Technology, Inc. <licensing@scalable.technology>",
|
package/src/client/index.ts
CHANGED
|
@@ -39,6 +39,9 @@ export interface CountResponse<T> extends ClientResponse<T> {
|
|
|
39
39
|
total: number;
|
|
40
40
|
}
|
|
41
41
|
|
|
42
|
+
/** Soft-delete scope: `include` reads or writes live and deleted rows, `only` deleted rows. */
|
|
43
|
+
export type DeletedMode = 'include' | 'only';
|
|
44
|
+
|
|
42
45
|
export interface QueryBuilder {
|
|
43
46
|
eq(column: string, value: string): QueryBuilder;
|
|
44
47
|
neq(column: string, value: string): QueryBuilder;
|
|
@@ -68,6 +71,9 @@ export interface QueryBuilder {
|
|
|
68
71
|
order(column: string, direction?: 'asc' | 'desc'): QueryBuilder;
|
|
69
72
|
limit(count: number): QueryBuilder;
|
|
70
73
|
offset(count: number): QueryBuilder;
|
|
74
|
+
/** Soft-delete tables: ?deleted=include|only on select and update. `.deleted('only').update({ deletedAt: null })` restores,
|
|
75
|
+
* for a role the handler lists in `softDelete.restore`; for anyone else update ignores it. */
|
|
76
|
+
deleted(mode: DeletedMode): QueryBuilder;
|
|
71
77
|
execute(): Promise<ClientResponse<unknown[]>>;
|
|
72
78
|
executeWithCount(): Promise<CountResponse<unknown[]>>;
|
|
73
79
|
single(): Promise<ClientResponse<unknown>>;
|
|
@@ -123,6 +129,7 @@ function createQueryBuilder(options: ClientOptions, table: string): QueryBuilder
|
|
|
123
129
|
let orderParam: string | null = null;
|
|
124
130
|
let limitParam: number | null = null;
|
|
125
131
|
let offsetParam: number | null = null;
|
|
132
|
+
let deletedParam: DeletedMode | null = null;
|
|
126
133
|
|
|
127
134
|
const builder: QueryBuilder = {
|
|
128
135
|
eq(column, value) { filters.push([column, `eq.${value}`]); return builder; },
|
|
@@ -151,9 +158,10 @@ function createQueryBuilder(options: ClientOptions, table: string): QueryBuilder
|
|
|
151
158
|
order(column, direction = 'asc') { orderParam = `${column}.${direction}`; return builder; },
|
|
152
159
|
limit(count) { limitParam = count; return builder; },
|
|
153
160
|
offset(count) { offsetParam = count; return builder; },
|
|
161
|
+
deleted(mode) { deletedParam = mode; return builder; },
|
|
154
162
|
|
|
155
163
|
async execute(): Promise<ClientResponse<unknown[]>> {
|
|
156
|
-
const url = buildUrl(options.baseUrl, table, filters, selectParam, orderParam, limitParam, offsetParam);
|
|
164
|
+
const url = buildUrl(options.baseUrl, table, filters, selectParam, orderParam, limitParam, offsetParam, deletedParam);
|
|
157
165
|
const headers = await buildHeaders(options);
|
|
158
166
|
const { response, retried } = await fetchWithRetry(url, { method: 'GET', headers }, options);
|
|
159
167
|
|
|
@@ -168,7 +176,7 @@ function createQueryBuilder(options: ClientOptions, table: string): QueryBuilder
|
|
|
168
176
|
},
|
|
169
177
|
|
|
170
178
|
async executeWithCount(): Promise<CountResponse<unknown[]>> {
|
|
171
|
-
const url = buildUrl(options.baseUrl, table, filters, selectParam, orderParam, limitParam, offsetParam);
|
|
179
|
+
const url = buildUrl(options.baseUrl, table, filters, selectParam, orderParam, limitParam, offsetParam, deletedParam);
|
|
172
180
|
const headers = await buildHeaders(options);
|
|
173
181
|
headers['Prefer'] = 'count=exact';
|
|
174
182
|
|
|
@@ -220,7 +228,7 @@ function createQueryBuilder(options: ClientOptions, table: string): QueryBuilder
|
|
|
220
228
|
},
|
|
221
229
|
|
|
222
230
|
async update(data: Record<string, unknown>): Promise<ClientResponse<unknown[]>> {
|
|
223
|
-
const url = buildUrl(options.baseUrl, table, filters, selectParam, orderParam, null, null);
|
|
231
|
+
const url = buildUrl(options.baseUrl, table, filters, selectParam, orderParam, null, null, deletedParam);
|
|
224
232
|
const headers = await buildHeaders(options);
|
|
225
233
|
headers['Content-Type'] = 'application/json';
|
|
226
234
|
|
|
@@ -340,6 +348,7 @@ function buildUrl(
|
|
|
340
348
|
order: string | null,
|
|
341
349
|
limit: number | null,
|
|
342
350
|
offset: number | null,
|
|
351
|
+
deleted: DeletedMode | null = null,
|
|
343
352
|
): string {
|
|
344
353
|
const path = `${baseUrl}/${table}`;
|
|
345
354
|
const base = typeof globalThis.location !== 'undefined' ? globalThis.location.origin : undefined;
|
|
@@ -355,6 +364,7 @@ function buildUrl(
|
|
|
355
364
|
if (order) url.searchParams.set('order', order);
|
|
356
365
|
if (limit !== null) url.searchParams.set('limit', String(limit));
|
|
357
366
|
if (offset !== null) url.searchParams.set('offset', String(offset));
|
|
367
|
+
if (deleted) url.searchParams.set('deleted', deleted);
|
|
358
368
|
|
|
359
369
|
return url.toString();
|
|
360
370
|
}
|