@everystack/admin 0.4.7 → 0.4.8

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,6 +4,16 @@ All notable changes to `@everystack/admin` are documented here.
4
4
 
5
5
  Format based on [Keep a Changelog](https://keepachangelog.com/).
6
6
 
7
+ ## [0.4.8] - 2026-10-18
8
+
9
+ ### Added
10
+
11
+ - a table-mapped resource reaches the adapter with its resource name
12
+
13
+ ### Documentation
14
+
15
+ - select examples use declared relation names; an alias answers 400
16
+
7
17
  ## [0.4.7] - 2026-10-18
8
18
 
9
19
  ### BREAKING CHANGES
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@everystack/admin",
3
- "version": "0.4.7",
3
+ "version": "0.4.8",
4
4
  "description": "Declarative admin dashboard framework for PostgREST APIs",
5
5
  "license": "AGPL-3.0-only",
6
6
  "author": "Scalable Technology, Inc. <licensing@scalable.technology>",
@@ -134,7 +134,7 @@
134
134
  "react-test-renderer": "19.2.0",
135
135
  "ts-jest": "29.4.9",
136
136
  "typescript": "5.9.3",
137
- "@everystack/api": "0.4.7",
137
+ "@everystack/api": "0.4.8",
138
138
  "@everystack/ui": "0.4.6"
139
139
  },
140
140
  "scripts": {
@@ -41,35 +41,41 @@ export interface DataAdapter {
41
41
  * depends on it, and the types cannot enforce it: a custom adapter that drops it
42
42
  * lets an id-addressed call reach a row outside the `where` slice.
43
43
  * EverystackAdapter honors it on the REST path; its crudRpcs branch ignores it.
44
+ *
45
+ * `params.resource` is the resource name, set only when `ResourceConfig.table` maps the
46
+ * resource to another table (then `resource` above is the table). See ListParams.resource.
44
47
  */
45
48
  fetchOne<T extends DataRecord = DataRecord>(
46
49
  resource: string,
47
50
  id: string | number,
48
- params?: { select?: string; deleted?: DeletedMode; filter?: Record<string, unknown> }
51
+ params?: { select?: string; deleted?: DeletedMode; filter?: Record<string, unknown>; resource?: string }
49
52
  ): Promise<T>;
50
53
 
51
54
  /**
52
- * Create a new record
55
+ * Create a new record. `params.resource` is the resource name, set only when
56
+ * `ResourceConfig.table` maps the resource to another table (then `resource` above is the
57
+ * table). See ListParams.resource.
53
58
  */
54
- create<T extends DataRecord = DataRecord>(resource: string, data: Omit<T, 'id'>): Promise<T>;
59
+ create<T extends DataRecord = DataRecord>(resource: string, data: Omit<T, 'id'>, params?: { resource?: string }): Promise<T>;
55
60
 
56
61
  /**
57
62
  * Update an existing record. `params.deleted` lets the write match a
58
63
  * soft-deleted row: Restore sends `{ [column]: null }` with `{ deleted: 'only' }`.
59
64
  * `params.filter` is ANDed with the id in the write itself (see fetchOne).
65
+ * `params.resource`: see fetchOne.
60
66
  */
61
67
  update<T extends DataRecord = DataRecord>(
62
68
  resource: string,
63
69
  id: string | number,
64
70
  data: Partial<T>,
65
- params?: { deleted?: DeletedMode; filter?: Record<string, unknown> }
71
+ params?: { deleted?: DeletedMode; filter?: Record<string, unknown>; resource?: string }
66
72
  ): Promise<T>;
67
73
 
68
74
  /**
69
75
  * Delete a record. `params.filter` is ANDed with the id in the delete itself
70
- * (see fetchOne).
76
+ * (see fetchOne). `params.resource`: see fetchOne.
71
77
  */
72
- delete(resource: string, id: string | number, params?: { filter?: Record<string, unknown> }): Promise<void>;
78
+ delete(resource: string, id: string | number, params?: { filter?: Record<string, unknown>; resource?: string }): Promise<void>;
73
79
 
74
80
  /**
75
81
  * Fetch aggregate data (count, sum, avg, min, max) with optional grouping.
package/src/types.ts CHANGED
@@ -370,8 +370,9 @@ export interface ResourceConfig {
370
370
  primaryKey?: string;
371
371
  /**
372
372
  * PostgREST select string for list, show and edit, embeds included, e.g.
373
- * '*,user:users(name)'. One string for all three. Show an embedded value
374
- * through a field `render`. RPC-backed resources ignore it.
373
+ * '*,user(name)', where `user` is a relation declared in the handler's `relations`.
374
+ * One string for all three. Show an embedded value through a field `render`.
375
+ * RPC-backed resources ignore it.
375
376
  */
376
377
  select?: string;
377
378
  /**
@@ -379,7 +380,9 @@ export interface ResourceConfig {
379
380
  * share a table, each with its own `where` (single-table inheritance). The URL, nav,
380
381
  * `relation.resource`, scopes and `where` stay keyed by the resource name; the adapter receives
381
382
  * the table name, so an adapter's own per-resource keys (EverystackAdapter `listRpcs` and
382
- * `crudRpcs`) use the table name. A resource `dashboard` does NOT follow `table`: its
383
+ * `crudRpcs`) use the table name. When the table is not the name, the call also carries the
384
+ * resource name as `params.resource` (a third argument on create), so a custom adapter can still
385
+ * tell two resources on one table apart. A resource `dashboard` does NOT follow `table`: its
383
386
  * `dashboard.tableName` still defaults to the resource name, so set it when `table` is set.
384
387
  */
385
388
  table?: string;
@@ -725,6 +728,15 @@ export type DeletedMode = 'include' | 'only';
725
728
  export type WhereValues = Record<string, string | number | boolean>;
726
729
 
727
730
  export interface ListParams {
731
+ /**
732
+ * The resource name, set only when `ResourceConfig.table` maps the resource to another table.
733
+ * The adapter's `resource` argument is then the table, and this is the resource that asked, so
734
+ * an adapter with per-resource flows can still tell two resources on one table apart. Absent
735
+ * when the table is the name: the `resource` argument IS the resource. The library sets it; a caller
736
+ * must not pass it, and for a resource that sets no `table` it is passed through as given. The same key rides on
737
+ * AggregateParams and on the params of fetchOne, create, update and delete.
738
+ */
739
+ resource?: string;
728
740
  /** Page number (1-indexed) */
729
741
  page?: number;
730
742
  /** Items per page */
@@ -747,8 +759,8 @@ export interface ListParams {
747
759
  */
748
760
  searchFields?: string[];
749
761
  /**
750
- * PostgREST select string, embeds included, e.g. '*,user:users(name)'.
751
- * Passed verbatim to the query builder. Ignored by RPC-backed resources
762
+ * PostgREST select string, embeds included, e.g. '*,user(name)' for a relation
763
+ * declared in the handler's `relations`. Passed verbatim to the query builder. Ignored by RPC-backed resources
752
764
  * (listRpcs); an RPC decides its own shape.
753
765
  */
754
766
  select?: string;
@@ -793,6 +805,8 @@ export interface AggregateQuery {
793
805
  }
794
806
 
795
807
  export interface AggregateParams {
808
+ /** The resource name when `ResourceConfig.table` maps it to another table. See ListParams.resource. */
809
+ resource?: string;
796
810
  /** Aggregate functions to compute */
797
811
  aggregates: AggregateQuery[];
798
812
  /** Columns to group by */
@@ -45,6 +45,15 @@ export function whereFilter(filter: Filter | undefined, where: WhereValues): Fil
45
45
  return out;
46
46
  }
47
47
 
48
+ /**
49
+ * `params` carrying the resource name, for a resource whose `table` is not its name. The name is
50
+ * set last, so a caller cannot override it. For any other resource `params` is returned as is, so
51
+ * an adapter sees no new key.
52
+ */
53
+ function named<P extends object>(resource: string, q: ResourceQuery, params: P): P {
54
+ return q.table === resource ? params : { ...params, resource };
55
+ }
56
+
48
57
  /** What the wrapper does for one resource: the table it names, its `where`, if any, and its id column. */
49
58
  interface ResourceQuery {
50
59
  table: string;
@@ -139,7 +148,10 @@ function checkRow<T>(resource: string, q: ResourceQuery, where: WhereValues, row
139
148
  /**
140
149
  * The adapter, with every call for a resource that sets `table` or `where` rewritten on the way in.
141
150
  * The resource name becomes its table for fetchList, aggregate, fetchOne, create, update, delete and
142
- * subscribe. `where` narrows the call to its slice: lists, counts and aggregates get it as a filter;
151
+ * subscribe. When that table is not the name, the call also carries the resource name as
152
+ * `params.resource` (a third argument on create), so an adapter that serves two resources on one
153
+ * table can tell them apart. subscribe does not carry it: its events are delivered under the
154
+ * resource name already. `where` narrows the call to its slice: lists, counts and aggregates get it as a filter;
143
155
  * fetchOne, update and delete get it as `params.filter` beside the id, in the one request they send;
144
156
  * create and update bodies get the `where` values over the form's. The row that create, update or
145
157
  * fetchOne returns is checked against `where`, and one outside it throws, naming it. subscribe
@@ -160,30 +172,33 @@ export function withResourceQuery(adapter: DataAdapter, config: AdminConfig): Da
160
172
  fetchList<T extends DataRecord = DataRecord>(resource: string, params: ListParams): Promise<ListResult<T>> {
161
173
  const q = queries.get(resource);
162
174
  if (!q) return adapter.fetchList<T>(resource, params);
163
- return adapter.fetchList<T>(q.table, q.where ? { ...params, filter: whereFilter(params.filter, q.where) } : params);
175
+ return adapter.fetchList<T>(q.table, named(resource, q, q.where ? { ...params, filter: whereFilter(params.filter, q.where) } : params));
164
176
  },
165
177
  aggregate(resource: string, params: AggregateParams): Promise<AggregateResult> {
166
178
  const q = queries.get(resource);
167
179
  if (!q) return adapter.aggregate!(resource, params);
168
- return adapter.aggregate!(q.table, q.where ? { ...params, filter: whereFilter(params.filter, q.where) } : params);
180
+ return adapter.aggregate!(q.table, named(resource, q, q.where ? { ...params, filter: whereFilter(params.filter, q.where) } : params));
169
181
  },
170
182
  fetchOne<T extends DataRecord = DataRecord>(resource: string, id: string | number, ...rest: [params?: FetchOneParams]): Promise<T> {
171
183
  const q = queries.get(resource);
172
184
  if (!q) return adapter.fetchOne<T>(resource, id, ...rest);
173
- if (!q.where) return adapter.fetchOne<T>(q.table, id, ...rest);
174
185
  const [params] = rest;
186
+ // No where means the table differs from the name (queriesOf), so the name is always sent.
187
+ if (!q.where) return adapter.fetchOne<T>(q.table, id, named(resource, q, { ...params }));
175
188
  const where = q.where;
176
189
  return adapter
177
- .fetchOne<T>(q.table, id, { ...params, filter: whereFilter(params?.filter, where) })
190
+ .fetchOne<T>(q.table, id, named(resource, q, { ...params, filter: whereFilter(params?.filter, where) }))
178
191
  .then(row => checkRow(resource, q, where, row, false));
179
192
  },
180
193
  create<T extends DataRecord = DataRecord>(resource: string, data: Omit<T, 'id'>): Promise<T> {
181
194
  const q = queries.get(resource);
182
195
  if (!q) return adapter.create<T>(resource, data);
183
- if (!q.where) return adapter.create<T>(q.table, data);
196
+ // A third argument only when the table is not the name: every other create keeps its two.
197
+ const rest: [params?: { resource?: string }] = q.table === resource ? [] : [{ resource }];
198
+ if (!q.where) return adapter.create<T>(q.table, data, ...rest);
184
199
  const where = q.where;
185
200
  warnOverridden(resource, data, where);
186
- return adapter.create<T>(q.table, { ...data, ...where }).then(row => checkRow(resource, q, where, row, true));
201
+ return adapter.create<T>(q.table, { ...data, ...where }, ...rest).then(row => checkRow(resource, q, where, row, true));
187
202
  },
188
203
  update<T extends DataRecord = DataRecord>(
189
204
  resource: string,
@@ -193,20 +208,20 @@ export function withResourceQuery(adapter: DataAdapter, config: AdminConfig): Da
193
208
  ): Promise<T> {
194
209
  const q = queries.get(resource);
195
210
  if (!q) return adapter.update<T>(resource, id, data, ...rest);
196
- if (!q.where) return adapter.update<T>(q.table, id, data, ...rest);
197
211
  const [params] = rest;
212
+ if (!q.where) return adapter.update<T>(q.table, id, data, named(resource, q, { ...params }));
198
213
  const where = q.where;
199
214
  warnOverridden(resource, data, where);
200
215
  return adapter
201
- .update<T>(q.table, id, { ...data, ...where } as Partial<T>, { ...params, filter: whereFilter(params?.filter, where) })
216
+ .update<T>(q.table, id, { ...data, ...where } as Partial<T>, named(resource, q, { ...params, filter: whereFilter(params?.filter, where) }))
202
217
  .then(row => checkRow(resource, q, where, row, true));
203
218
  },
204
219
  delete(resource: string, id: string | number, ...rest: [params?: DeleteParams]): Promise<void> {
205
220
  const q = queries.get(resource);
206
221
  if (!q) return adapter.delete(resource, id, ...rest);
207
- if (!q.where) return adapter.delete(q.table, id, ...rest);
208
222
  const [params] = rest;
209
- return adapter.delete(q.table, id, { ...params, filter: whereFilter(params?.filter, q.where) });
223
+ if (!q.where) return adapter.delete(q.table, id, named(resource, q, { ...params }));
224
+ return adapter.delete(q.table, id, named(resource, q, { ...params, filter: whereFilter(params?.filter, q.where) }));
210
225
  },
211
226
  subscribe<T extends DataRecord = DataRecord>(resource: string, callback: (event: SubscriptionEvent<T>) => void): () => void {
212
227
  const q = queries.get(resource);