@everystack/admin 0.2.2 → 0.2.3
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/package.json
CHANGED
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
*/
|
|
16
16
|
|
|
17
17
|
import { sql, type SQL } from 'drizzle-orm';
|
|
18
|
+
import { toCamelCaseKeys } from '../utils/naming';
|
|
18
19
|
import type { ResourceDashboardConfig, DimensionConfig, StatDialConfig } from '../types';
|
|
19
20
|
|
|
20
21
|
interface DbExecutor {
|
|
@@ -413,6 +414,61 @@ function createEventListHandler(
|
|
|
413
414
|
};
|
|
414
415
|
}
|
|
415
416
|
|
|
417
|
+
// ────────────────────────────────────────────────────────────
|
|
418
|
+
// Event Detail RPC
|
|
419
|
+
//
|
|
420
|
+
// The show view's data source for detailVia: 'rpc' resources — one row
|
|
421
|
+
// by primary key, the static where still applied (an STI discriminator
|
|
422
|
+
// must gate detail exactly like it gates the list), keys camelCased to
|
|
423
|
+
// match field definitions. Keeps telemetry off the REST surface entirely.
|
|
424
|
+
// ────────────────────────────────────────────────────────────
|
|
425
|
+
|
|
426
|
+
const MAX_ID_LENGTH = 256;
|
|
427
|
+
|
|
428
|
+
function createEventDetailHandler(
|
|
429
|
+
tableName: string,
|
|
430
|
+
config: ResourceDashboardConfig,
|
|
431
|
+
db: DbExecutor,
|
|
432
|
+
): (body: unknown) => Promise<unknown> {
|
|
433
|
+
const pkColumn = Array.isArray(config.primaryKey)
|
|
434
|
+
? config.primaryKey[0]
|
|
435
|
+
: config.primaryKey ?? 'id';
|
|
436
|
+
|
|
437
|
+
return async (body: unknown) => {
|
|
438
|
+
const id = (body as { id?: unknown } | null)?.id;
|
|
439
|
+
if (
|
|
440
|
+
(typeof id !== 'string' && typeof id !== 'number') ||
|
|
441
|
+
String(id).length === 0 ||
|
|
442
|
+
String(id).length > MAX_ID_LENGTH
|
|
443
|
+
) {
|
|
444
|
+
throw new Error('Invalid id');
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
const conditions: SQL[] = [sql`${sql.raw(pkColumn)} = ${String(id)}`];
|
|
448
|
+
for (const predicate of config.where ?? []) {
|
|
449
|
+
conditions.push(sql`${sql.raw(predicate.column)} = ${predicate.equals}`);
|
|
450
|
+
}
|
|
451
|
+
let where = conditions[0];
|
|
452
|
+
for (let i = 1; i < conditions.length; i++) {
|
|
453
|
+
where = sql`${where} AND ${conditions[i]}`;
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
const result = await db.execute(sql`
|
|
457
|
+
SELECT row_to_json(r) AS row
|
|
458
|
+
FROM (SELECT * FROM ${sql.raw(tableName)} WHERE ${where} LIMIT 1) r
|
|
459
|
+
`);
|
|
460
|
+
|
|
461
|
+
const rows = extractRows(result);
|
|
462
|
+
const first = rows[0] as { row: Record<string, unknown> | string } | undefined;
|
|
463
|
+
if (!first) {
|
|
464
|
+
throw new Error(`Record not found: ${id}`);
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
const raw = typeof first.row === 'string' ? JSON.parse(first.row) : first.row;
|
|
468
|
+
return toCamelCaseKeys(raw);
|
|
469
|
+
};
|
|
470
|
+
}
|
|
471
|
+
|
|
416
472
|
// ────────────────────────────────────────────────────────────
|
|
417
473
|
// Public API
|
|
418
474
|
// ────────────────────────────────────────────────────────────
|
|
@@ -420,6 +476,7 @@ function createEventListHandler(
|
|
|
420
476
|
export interface ResourceDashboardRpcs {
|
|
421
477
|
dashboardData: (body: unknown) => Promise<unknown>;
|
|
422
478
|
eventList: (body: unknown) => Promise<unknown>;
|
|
479
|
+
eventDetail: (body: unknown) => Promise<unknown>;
|
|
423
480
|
}
|
|
424
481
|
|
|
425
482
|
/**
|
|
@@ -438,6 +495,7 @@ export function createResourceDashboardRpcs(
|
|
|
438
495
|
return {
|
|
439
496
|
dashboardData: createDashboardDataHandler(tableName, config, db),
|
|
440
497
|
eventList: createEventListHandler(tableName, config, db),
|
|
498
|
+
eventDetail: createEventDetailHandler(tableName, config, db),
|
|
441
499
|
};
|
|
442
500
|
}
|
|
443
501
|
|
|
@@ -464,6 +522,7 @@ export function createAllDashboardRpcs(
|
|
|
464
522
|
const prefix = resource.name.replace(/_([a-z])/g, (_, c) => c.toUpperCase());
|
|
465
523
|
rpcs[`${prefix}DashboardData`] = handlers.dashboardData;
|
|
466
524
|
rpcs[`${prefix}EventList`] = handlers.eventList;
|
|
525
|
+
rpcs[`${prefix}EventDetail`] = handlers.eventDetail;
|
|
467
526
|
}
|
|
468
527
|
|
|
469
528
|
return rpcs;
|
|
@@ -12,7 +12,8 @@ import { useDataAdapter } from '../hooks/useAdminData';
|
|
|
12
12
|
import { LoadingState, ErrorState, Button, Card, Divider } from '@everystack/ui';
|
|
13
13
|
import { ImageDisplay } from '../components/ImageDisplay';
|
|
14
14
|
import { visibleRowActions } from '../utils/row-actions';
|
|
15
|
-
import
|
|
15
|
+
import { toCamelCase } from '../utils/naming';
|
|
16
|
+
import type { FieldDefinition, DataRecord, ResourceConfig, RowActionConfig } from '../types';
|
|
16
17
|
import type { DataAdapter } from '../adapters/types';
|
|
17
18
|
|
|
18
19
|
export interface ResourceShowProps {
|
|
@@ -47,14 +48,13 @@ export function ResourceShow({
|
|
|
47
48
|
setLoading(true);
|
|
48
49
|
setError(null);
|
|
49
50
|
try {
|
|
50
|
-
|
|
51
|
-
setRecord(result);
|
|
51
|
+
setRecord(await fetchDetail(adapter, resource, id, resourceConfig));
|
|
52
52
|
} catch (err) {
|
|
53
53
|
setError(err instanceof Error ? err : new Error(String(err)));
|
|
54
54
|
} finally {
|
|
55
55
|
setLoading(false);
|
|
56
56
|
}
|
|
57
|
-
}, [adapter, resource, id]);
|
|
57
|
+
}, [adapter, resource, id, resourceConfig]);
|
|
58
58
|
|
|
59
59
|
useEffect(() => {
|
|
60
60
|
fetchRecord();
|
|
@@ -132,6 +132,30 @@ export function ResourceShow({
|
|
|
132
132
|
);
|
|
133
133
|
}
|
|
134
134
|
|
|
135
|
+
// =============================================================================
|
|
136
|
+
// Detail fetching
|
|
137
|
+
// =============================================================================
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* detailVia: 'rpc' fetches through the role-gated {prefix}EventDetail RPC —
|
|
141
|
+
* the resource never enters the REST surface (telemetry tables stay out of
|
|
142
|
+
* exposedTables). Everything else uses adapter.fetchOne as before.
|
|
143
|
+
*/
|
|
144
|
+
async function fetchDetail(
|
|
145
|
+
adapter: DataAdapter,
|
|
146
|
+
resource: string,
|
|
147
|
+
id: string | number,
|
|
148
|
+
resourceConfig: ResourceConfig | undefined,
|
|
149
|
+
): Promise<DataRecord> {
|
|
150
|
+
if (resourceConfig?.dashboard?.detailVia === 'rpc') {
|
|
151
|
+
if (!adapter.rpc) {
|
|
152
|
+
throw new Error(`Resource "${resource}" uses detailVia: 'rpc' but the adapter has no rpc()`);
|
|
153
|
+
}
|
|
154
|
+
return (await adapter.rpc(`${toCamelCase(resource)}EventDetail`, { id })) as DataRecord;
|
|
155
|
+
}
|
|
156
|
+
return adapter.fetchOne(resource, id);
|
|
157
|
+
}
|
|
158
|
+
|
|
135
159
|
// =============================================================================
|
|
136
160
|
// RowActionButton
|
|
137
161
|
// =============================================================================
|
package/src/types.ts
CHANGED
|
@@ -195,6 +195,12 @@ export interface ResourceDashboardConfig {
|
|
|
195
195
|
searchColumns?: string[];
|
|
196
196
|
/** Navigate to detail on row press (default: true) */
|
|
197
197
|
linkToDetail?: boolean;
|
|
198
|
+
/**
|
|
199
|
+
* How the show view fetches the record. 'rpc' uses the role-gated
|
|
200
|
+
* {prefix}EventDetail RPC — the whole resource stays off the REST
|
|
201
|
+
* surface (telemetry tables never enter exposedTables). Default 'rest'.
|
|
202
|
+
*/
|
|
203
|
+
detailVia?: 'rpc' | 'rest';
|
|
198
204
|
}
|
|
199
205
|
|
|
200
206
|
// =============================================================================
|