@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 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
- ## [Unreleased]
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.2",
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>",
@@ -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
  }