@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 +10 -0
- package/package.json +2 -2
- package/src/adapters/types.ts +12 -6
- package/src/types.ts +19 -5
- package/src/utils/resource-query.ts +26 -11
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.
|
|
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.
|
|
137
|
+
"@everystack/api": "0.4.8",
|
|
138
138
|
"@everystack/ui": "0.4.6"
|
|
139
139
|
},
|
|
140
140
|
"scripts": {
|
package/src/adapters/types.ts
CHANGED
|
@@ -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'
|
|
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
|
|
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
|
|
374
|
-
* through a field `render`.
|
|
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.
|
|
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
|
|
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.
|
|
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
|
-
|
|
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,
|
|
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);
|