akanjs 3.0.0-alpha.59 → 3.0.0-alpha.60
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/client/clientRuntime.ts +3 -0
- package/client/types.ts +3 -3
- package/dictionary/dictInfo.ts +3 -2
- package/document/documentQuery.ts +1 -0
- package/document/filterMeta.ts +77 -11
- package/fetch/client/fetchClient.ts +7 -5
- package/fetch/client/httpClient.ts +3 -1
- package/fetch/fetchType/appliedReturn.type.ts +6 -0
- package/local/apps/serverLifecycle/serverLifecycle-local.db-shm +0 -0
- package/local/apps/serverLifecycle/serverLifecycle-local_solid.db-shm +0 -0
- package/package.json +1 -1
- package/service/predefinedAdaptor/database.adaptor.ts +23 -13
- package/signal/schema/JsonSchemaBuilder.ts +5 -1
- package/signal/serializer/fetch.serializer.ts +35 -3
- package/signal/signalContext.ts +13 -1
- package/signal/slice.ts +21 -17
- package/signal/types.ts +5 -0
- package/types/client/clientRuntime.d.ts +5 -0
- package/types/client/types.d.ts +5 -3
- package/types/dictionary/dictInfo.d.ts +1 -1
- package/types/document/filterMeta.d.ts +24 -3
- package/types/fetch/client/fetchClient.d.ts +3 -0
- package/types/fetch/fetchType/appliedReturn.type.d.ts +5 -0
- package/types/service/predefinedAdaptor/database.adaptor.d.ts +7 -1
- package/types/signal/slice.d.ts +9 -7
- package/types/signal/types.d.ts +5 -0
- package/types/ui/Data/Dashboard.d.ts +4 -2
- package/types/ui/Data/ListContainer.d.ts +8 -4
- package/types/ui/Data/QueryMaker.d.ts +3 -3
- package/types/ui/Data/index.d.ts +1 -0
- package/types/ui/Model/AdminPanel.d.ts +3 -3
- package/types/ui/Signal/Arg.d.ts +4 -3
- package/ui/Data/Dashboard.tsx +2 -2
- package/ui/Data/ListContainer.tsx +14 -5
- package/ui/Data/QueryMaker.tsx +62 -12
- package/ui/Data/index.ts +2 -1
- package/ui/Model/AdminPanel.tsx +4 -3
- package/ui/Signal/Arg.tsx +20 -7
- package/ui/Signal/makeExample.ts +1 -1
package/client/clientRuntime.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { SliceMeta } from "akanjs/fetch";
|
|
2
|
+
import type { SerializedArg } from "akanjs/signal";
|
|
2
3
|
import type { ReactNode } from "react";
|
|
3
4
|
import type { TransMessageOption } from "./makePageProto";
|
|
4
5
|
|
|
@@ -49,6 +50,8 @@ type RuntimeFetch = typeof globalThis.fetch & {
|
|
|
49
50
|
slice: Record<string, SliceMeta>;
|
|
50
51
|
/** Sort keys per model refName, registered from each serialized signal's filter. */
|
|
51
52
|
sortKeyMap?: Map<string, string[]>;
|
|
53
|
+
/** Filter queries per model refName, with the args each one takes — what the root slice picks from. */
|
|
54
|
+
filterQueryMap?: Map<string, { [queryKey: string]: SerializedArg[] }>;
|
|
52
55
|
ws: RuntimeWs;
|
|
53
56
|
[key: string]: unknown;
|
|
54
57
|
};
|
package/client/types.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { BaseInsight } from "akanjs/constant";
|
|
2
|
-
import type { FetchInitForm, SliceMeta } from "akanjs/fetch";
|
|
2
|
+
import type { FetchInitForm, QuerySetting, SliceMeta } from "akanjs/fetch";
|
|
3
3
|
import type { ReactElement, ReactNode } from "react";
|
|
4
4
|
|
|
5
5
|
export type ReactFontStyle = "normal" | "italic" | "oblique";
|
|
@@ -109,7 +109,7 @@ const slugFontPart = (value: string) =>
|
|
|
109
109
|
export interface ModelsProps<M extends { id: string }> {
|
|
110
110
|
className?: string;
|
|
111
111
|
slice?: SliceMeta;
|
|
112
|
-
query?:
|
|
112
|
+
query?: QuerySetting;
|
|
113
113
|
init?: FetchInitForm<any, any>;
|
|
114
114
|
onClickItem?: (model: M) => unknown;
|
|
115
115
|
}
|
|
@@ -127,7 +127,7 @@ export type ModelProps<T extends string, L extends { id: string }> = { [key in T
|
|
|
127
127
|
export interface ModelDashboardProps<Summary> {
|
|
128
128
|
className?: string;
|
|
129
129
|
summary: Summary;
|
|
130
|
-
queryMap?:
|
|
130
|
+
queryMap?: { [column: string]: QuerySetting };
|
|
131
131
|
columns?: (keyof Summary)[];
|
|
132
132
|
hidePresents?: boolean;
|
|
133
133
|
slice?: SliceMeta;
|
package/dictionary/dictInfo.ts
CHANGED
|
@@ -206,12 +206,13 @@ export class ModelDictInfo<
|
|
|
206
206
|
} as unknown as BaseModelCrudGetSignalTranslation<T, [string, string]>;
|
|
207
207
|
}
|
|
208
208
|
static baseSliceDictionary: {
|
|
209
|
-
[key in ""]: FunctionTranslation<[string, string], "
|
|
209
|
+
[key in ""]: FunctionTranslation<[string, string], "queryKey" | "args">;
|
|
210
210
|
} = {
|
|
211
211
|
"": fn(["Universal", "유니버설"])
|
|
212
212
|
.desc(["Universal Slice", "유니버설 슬라이스"])
|
|
213
213
|
.arg((t) => ({
|
|
214
|
-
|
|
214
|
+
queryKey: t(["Query", "쿼리"]).desc(["Filter query to run", "실행할 필터 쿼리"]),
|
|
215
|
+
args: t(["Arguments", "인자"]).desc(["Arguments of the filter query", "필터 쿼리의 인자"]),
|
|
215
216
|
})),
|
|
216
217
|
};
|
|
217
218
|
|
|
@@ -157,6 +157,7 @@ export const createDocumentQueryHelper = () => ({
|
|
|
157
157
|
lte: (value: unknown) => op("lte", value),
|
|
158
158
|
between: (from: unknown, to: unknown) => op("between", [from, to]),
|
|
159
159
|
exists: (path: string) => ({ [path]: op("exists") }),
|
|
160
|
+
|
|
160
161
|
missing: (path: string) => ({ [path]: op("missing") }),
|
|
161
162
|
empty: (path: string) => ({ [path]: op("empty") }),
|
|
162
163
|
has: (value: unknown) => op("has", value),
|
package/document/filterMeta.ts
CHANGED
|
@@ -1,16 +1,18 @@
|
|
|
1
|
-
import type { Cls, MergeAllDoubleKeyOfObjects, MergeAllKeyOfTypes } from "akanjs/base";
|
|
2
|
-
import { FILTER_DICT_SHAPE, FILTER_META } from "akanjs/base";
|
|
3
|
-
import
|
|
4
|
-
BaseObject,
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
1
|
+
import type { Cls, EnumInstance, MergeAllDoubleKeyOfObjects, MergeAllKeyOfTypes } from "akanjs/base";
|
|
2
|
+
import { FILTER_DICT_SHAPE, FILTER_META, getNonArrayModel, isEnum } from "akanjs/base";
|
|
3
|
+
import {
|
|
4
|
+
type BaseObject,
|
|
5
|
+
type ConstantFieldType,
|
|
6
|
+
type ConstantFieldTypeInput,
|
|
7
|
+
type DocumentModel,
|
|
8
|
+
deserialize,
|
|
9
|
+
type FieldToValue,
|
|
10
|
+
type PlainTypeToFieldType,
|
|
11
|
+
type QueryOf,
|
|
12
|
+
type Serialized,
|
|
11
13
|
} from "akanjs/constant";
|
|
12
14
|
|
|
13
|
-
import type
|
|
15
|
+
import { type DocumentQuery, type DocumentQueryHelper, documentQueryHelper } from "./documentQuery";
|
|
14
16
|
import type { ConstantFilterMeta } from "./types";
|
|
15
17
|
|
|
16
18
|
const isObjectRecord = (value: unknown): value is Record<string, unknown> =>
|
|
@@ -98,6 +100,70 @@ export const fillMissingFilterArgs = (filterInfo: FilterInfo, args: unknown[]) =
|
|
|
98
100
|
return [...args, ...Array(filterInfo.args.length - args.length).fill(undefined)];
|
|
99
101
|
};
|
|
100
102
|
|
|
103
|
+
export interface FilterArgInfo {
|
|
104
|
+
name: string;
|
|
105
|
+
argRef: ConstantFieldType;
|
|
106
|
+
arrDepth: number;
|
|
107
|
+
enum?: EnumInstance;
|
|
108
|
+
nullable: boolean;
|
|
109
|
+
ref?: string;
|
|
110
|
+
}
|
|
111
|
+
/** Unwraps a filter arg's declaration the way `EndpointInfo.getArgInfo` unwraps an endpoint's. */
|
|
112
|
+
export const getFilterArgInfo = (arg: FilterInfo["args"][number]): FilterArgInfo => {
|
|
113
|
+
const [singleArg, arrDepth] = getNonArrayModel(arg.argRef as Cls);
|
|
114
|
+
const argIsEnum = isEnum(singleArg);
|
|
115
|
+
return {
|
|
116
|
+
name: arg.name,
|
|
117
|
+
argRef: (argIsEnum ? (singleArg as EnumInstance).type : singleArg) as ConstantFieldType,
|
|
118
|
+
arrDepth,
|
|
119
|
+
enum: argIsEnum ? (singleArg as EnumInstance) : undefined,
|
|
120
|
+
nullable: !!arg.option?.nullable,
|
|
121
|
+
ref: arg.option?.ref,
|
|
122
|
+
};
|
|
123
|
+
};
|
|
124
|
+
export const getFilterArgInfos = (filterInfo: FilterInfo): FilterArgInfo[] => filterInfo.args.map(getFilterArgInfo);
|
|
125
|
+
|
|
126
|
+
/** A caller named a filter that does not exist, or gave it arguments it cannot take. Its callers answer 400. */
|
|
127
|
+
export class FilterQueryError extends Error {}
|
|
128
|
+
|
|
129
|
+
const tryDeserializeFilterArg = (arg: FilterArgInfo, value: unknown, key: string) => {
|
|
130
|
+
try {
|
|
131
|
+
return deserialize(arg.argRef, arg.arrDepth, value, { key: arg.name, nullable: arg.nullable });
|
|
132
|
+
} catch (error) {
|
|
133
|
+
throw new FilterQueryError(
|
|
134
|
+
`Invalid filter argument "${arg.name}" for key: ${key}: ${error instanceof Error ? error.message : String(error)}`,
|
|
135
|
+
);
|
|
136
|
+
}
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Compiles a `(queryKey, args)` pair into the query its filter declares — the root slice's whole contract.
|
|
141
|
+
* Every arg is parsed by the type the filter declared for it, so a Date arg reaches the query as a Dayjs and
|
|
142
|
+
* an id that is not one is refused here rather than becoming a query that matches nothing. Args past the
|
|
143
|
+
* declared ones are dropped: the caller names a filter, never a query.
|
|
144
|
+
*/
|
|
145
|
+
export const resolveFilterQuery = (
|
|
146
|
+
filterRef: FilterCls,
|
|
147
|
+
queryKey?: string | null,
|
|
148
|
+
args?: unknown[] | null,
|
|
149
|
+
): QueryOf<any> => {
|
|
150
|
+
const key = queryKey || "any";
|
|
151
|
+
const filterInfo = getFilterMeta(filterRef).query[key];
|
|
152
|
+
const queryFn = filterInfo?.queryFn;
|
|
153
|
+
if (!queryFn) throw new FilterQueryError(`No filter query for key: ${key}`);
|
|
154
|
+
const given = Array.isArray(args) ? args : [];
|
|
155
|
+
const queryArgs = getFilterArgInfos(filterInfo).map((arg, idx) => {
|
|
156
|
+
const value = given[idx];
|
|
157
|
+
if (!arg.nullable && (value === null || value === undefined))
|
|
158
|
+
throw new FilterQueryError(`Missing filter argument "${arg.name}" for key: ${key}`);
|
|
159
|
+
const parsed = tryDeserializeFilterArg(arg, value, key);
|
|
160
|
+
|
|
161
|
+
return parsed === null ? undefined : parsed;
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
return queryFn(...queryArgs, documentQueryHelper) as QueryOf<any>;
|
|
165
|
+
};
|
|
166
|
+
|
|
101
167
|
export const assertFilterFitsCrud = (refName: string, queryKey: string, className: string) => {
|
|
102
168
|
if (queryKey.toLowerCase() !== refName.toLowerCase()) return;
|
|
103
169
|
throw new Error(
|
|
@@ -81,6 +81,7 @@ export class FetchClient {
|
|
|
81
81
|
readonly handler: Record<string, FetchHandler>;
|
|
82
82
|
readonly slice: Record<string, SliceMeta> = {};
|
|
83
83
|
readonly sortKeyMap = new Map<string, string[]>();
|
|
84
|
+
readonly filterQueryMap = new Map<string, { [queryKey: string]: SerializedArg[] }>();
|
|
84
85
|
readonly #originWs = new Map<string, WsClient>();
|
|
85
86
|
readonly #handlerStore: Record<string, FetchHandler> = {};
|
|
86
87
|
readonly #handlerFactory = new Map<string, FetchHandlerFactory>();
|
|
@@ -178,10 +179,10 @@ export class FetchClient {
|
|
|
178
179
|
this.#registerSlice(refName, suffix, slice, signal.prefix);
|
|
179
180
|
}
|
|
180
181
|
if (signal.filter) {
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
182
|
+
|
|
183
|
+
const filter = this.serializedSignal[refName]?.filter ?? signal.filter;
|
|
184
|
+
this.#registerFilterSortKey(refName, filter.sortKeys);
|
|
185
|
+
this.#registerFilterQuery(refName, filter.filter);
|
|
185
186
|
}
|
|
186
187
|
}
|
|
187
188
|
return this;
|
|
@@ -279,7 +280,8 @@ export class FetchClient {
|
|
|
279
280
|
#registerFilterSortKey(refName: string, sortKeys: string[]) {
|
|
280
281
|
this.sortKeyMap.set(refName, sortKeys);
|
|
281
282
|
}
|
|
282
|
-
#registerFilterQuery(
|
|
283
|
+
#registerFilterQuery(refName: string, filter: { [queryKey: string]: SerializedArg[] }) {
|
|
284
|
+
this.filterQueryMap.set(refName, filter);
|
|
283
285
|
}
|
|
284
286
|
#makeHttpFn(key: string, endpoint: SerializedEndpoint, prefix?: string) {
|
|
285
287
|
const argLength = endpoint.args.length;
|
|
@@ -114,7 +114,9 @@ export class HttpClient {
|
|
|
114
114
|
searchArgs.forEach((arg) => {
|
|
115
115
|
const argValue = argMap.get(arg.name);
|
|
116
116
|
if (argValue === null || argValue === undefined) return;
|
|
117
|
-
|
|
117
|
+
|
|
118
|
+
if (arg.refName === "Any") searchParams.set(arg.name, JSON.stringify(argValue));
|
|
119
|
+
else if (arg.arrDepth && Array.isArray(argValue))
|
|
118
120
|
argValue.forEach((value) => {
|
|
119
121
|
searchParams.append(arg.name, String(value));
|
|
120
122
|
});
|
|
@@ -8,6 +8,12 @@ export type SliceMeta = {
|
|
|
8
8
|
argLength: number;
|
|
9
9
|
};
|
|
10
10
|
|
|
11
|
+
/** What the root slice takes: one of the model's declared filter queries, and the args that filter asks for. */
|
|
12
|
+
export interface QuerySetting {
|
|
13
|
+
queryKey: string;
|
|
14
|
+
args?: unknown[];
|
|
15
|
+
}
|
|
16
|
+
|
|
11
17
|
export type ServerInit<
|
|
12
18
|
RefName extends string,
|
|
13
19
|
Light,
|
|
Binary file
|
|
Binary file
|
package/package.json
CHANGED
|
@@ -385,6 +385,8 @@ interface SqlDialect {
|
|
|
385
385
|
docColumn(): string;
|
|
386
386
|
docValuePlaceholder(): string;
|
|
387
387
|
extract(path: string): string;
|
|
388
|
+
projectExpr(path: string): string;
|
|
389
|
+
decodeProjected(value: unknown): unknown;
|
|
388
390
|
eq(path: string, value: unknown): SqlFrag;
|
|
389
391
|
ne(path: string, value: unknown): SqlFrag;
|
|
390
392
|
compare(path: string, op: "gt" | "gte" | "lt" | "lte", value: unknown): SqlFrag;
|
|
@@ -423,6 +425,13 @@ export class SqliteDialect implements SqlDialect {
|
|
|
423
425
|
extract(path: string) {
|
|
424
426
|
return `json_extract(${this.docColumn()}, ${this.#path(path)})`;
|
|
425
427
|
}
|
|
428
|
+
|
|
429
|
+
projectExpr(path: string) {
|
|
430
|
+
return `${this.docColumn()} -> ${this.#path(path)}`;
|
|
431
|
+
}
|
|
432
|
+
decodeProjected(value: unknown) {
|
|
433
|
+
return typeof value === "string" ? JSON.parse(value) : value;
|
|
434
|
+
}
|
|
426
435
|
eq(path: string, value: unknown): SqlFrag {
|
|
427
436
|
return value === null
|
|
428
437
|
? { sql: `${this.extract(path)} IS NULL`, params: [] }
|
|
@@ -554,6 +563,13 @@ export class PostgresDialect implements SqlDialect {
|
|
|
554
563
|
extract(path: string) {
|
|
555
564
|
return this.#jsonb(path);
|
|
556
565
|
}
|
|
566
|
+
|
|
567
|
+
projectExpr(path: string) {
|
|
568
|
+
return this.#jsonb(path);
|
|
569
|
+
}
|
|
570
|
+
decodeProjected(value: unknown) {
|
|
571
|
+
return value;
|
|
572
|
+
}
|
|
557
573
|
eq(path: string, value: unknown): SqlFrag {
|
|
558
574
|
return value === null
|
|
559
575
|
? { sql: `${this.#jsonb(path)} IS NULL`, params: [] }
|
|
@@ -747,6 +763,11 @@ class QueryCompiler {
|
|
|
747
763
|
return BASE_COLUMNS.has(path) ? quoteIdent(path) : this.dialect.extract(path);
|
|
748
764
|
}
|
|
749
765
|
|
|
766
|
+
projectExpr(path: string) {
|
|
767
|
+
this.assertPath(path);
|
|
768
|
+
return BASE_COLUMNS.has(path) ? quoteIdent(path) : this.dialect.projectExpr(path);
|
|
769
|
+
}
|
|
770
|
+
|
|
750
771
|
private compileNode(query: DocumentQuery, ctx: CompileContext): { sql: string; params: unknown[] } {
|
|
751
772
|
if (this.isQueryNode(query)) {
|
|
752
773
|
if (query.kind === "search") {
|
|
@@ -1416,7 +1437,7 @@ export class SqlDocumentStore {
|
|
|
1416
1437
|
const jsonFields = fields.filter((field) => !BASE_COLUMNS.has(field));
|
|
1417
1438
|
const baseColumns = [...BASE_COLUMNS].map((field) => quoteIdent(field));
|
|
1418
1439
|
const jsonColumns = jsonFields.map(
|
|
1419
|
-
(field, idx) => `${this.compiler.
|
|
1440
|
+
(field, idx) => `${this.compiler.projectExpr(field)} AS ${quoteIdent(this.projectionAlias(idx))}`,
|
|
1420
1441
|
);
|
|
1421
1442
|
return [...baseColumns, ...jsonColumns].join(", ");
|
|
1422
1443
|
}
|
|
@@ -1434,7 +1455,7 @@ export class SqlDocumentStore {
|
|
|
1434
1455
|
};
|
|
1435
1456
|
const jsonFields = fields.filter((field) => !BASE_COLUMNS.has(field));
|
|
1436
1457
|
for (const [idx, field] of jsonFields.entries()) {
|
|
1437
|
-
const value = this.
|
|
1458
|
+
const value = this.dialect.decodeProjected(row[this.projectionAlias(idx)]);
|
|
1438
1459
|
const props = (this.database.doc[FIELD_META] as unknown as FieldMap)[field]?.getProps?.();
|
|
1439
1460
|
if (value === null && !props?.nullable) {
|
|
1440
1461
|
if (props?.default != null) {
|
|
@@ -1514,17 +1535,6 @@ export class SqlDocumentStore {
|
|
|
1514
1535
|
);
|
|
1515
1536
|
}
|
|
1516
1537
|
|
|
1517
|
-
private parseProjectedValue(value: unknown) {
|
|
1518
|
-
if (typeof value !== "string") return value;
|
|
1519
|
-
const trimmed = value.trim();
|
|
1520
|
-
if (!trimmed || (trimmed[0] !== "{" && trimmed[0] !== "[")) return value;
|
|
1521
|
-
try {
|
|
1522
|
-
return JSON.parse(trimmed);
|
|
1523
|
-
} catch {
|
|
1524
|
-
return value;
|
|
1525
|
-
}
|
|
1526
|
-
}
|
|
1527
|
-
|
|
1528
1538
|
private decodeDocumentPayload(payload: Record<string, unknown>) {
|
|
1529
1539
|
const fields = this.database.doc[FIELD_META] as unknown as FieldMap;
|
|
1530
1540
|
const result: Record<string, unknown> = {};
|
|
@@ -38,7 +38,11 @@ export class JsonSchemaBuilder {
|
|
|
38
38
|
}
|
|
39
39
|
|
|
40
40
|
arg(arg: SerializedArg): JsonSchema {
|
|
41
|
-
const schema = arg.
|
|
41
|
+
const schema = arg.oneOf
|
|
42
|
+
? JsonSchemaBuilder.#inlineEnum(arg.oneOf)
|
|
43
|
+
: arg.enum
|
|
44
|
+
? this.#enum(arg.enum)
|
|
45
|
+
: this.#ref(arg.refName, arg.modelType);
|
|
42
46
|
return JsonSchemaBuilder.#nullable(JsonSchemaBuilder.#arrayed(schema, arg.arrDepth ?? 0), !!arg.nullable);
|
|
43
47
|
}
|
|
44
48
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { type Cls, ENDPOINT_META, PrimitiveRegistry, type PrimitiveScalar, SLICE_META } from "akanjs/base";
|
|
2
2
|
import { Logger } from "akanjs/common";
|
|
3
3
|
import { ConstantRegistry, type ConstantType } from "akanjs/constant";
|
|
4
|
-
import { getFilterMeta } from "akanjs/document";
|
|
4
|
+
import { type FilterArgInfo, getFilterArgInfos, getFilterMeta } from "akanjs/document";
|
|
5
5
|
import type { LiveRegistry } from "akanjs/service";
|
|
6
6
|
import type {
|
|
7
7
|
ArgInfo,
|
|
@@ -10,6 +10,7 @@ import type {
|
|
|
10
10
|
EndpointInfo,
|
|
11
11
|
SerializedArg,
|
|
12
12
|
SerializedEndpoint,
|
|
13
|
+
SerializedFilter,
|
|
13
14
|
SerializedReturns,
|
|
14
15
|
SerializedSignal,
|
|
15
16
|
SerializedSlice,
|
|
@@ -71,6 +72,35 @@ export class FetchSerializer {
|
|
|
71
72
|
};
|
|
72
73
|
}
|
|
73
74
|
|
|
75
|
+
static #serializeFilterArg(argInfo: FilterArgInfo): SerializedArg {
|
|
76
|
+
const { refName, modelType } = FetchSerializer.#resolveRefInfo(argInfo.argRef as Cls);
|
|
77
|
+
return {
|
|
78
|
+
type: "search",
|
|
79
|
+
refName,
|
|
80
|
+
name: argInfo.name,
|
|
81
|
+
...(modelType ? { modelType: modelType as SerializedArg["modelType"] } : {}),
|
|
82
|
+
...(argInfo.arrDepth ? { arrDepth: argInfo.arrDepth } : {}),
|
|
83
|
+
...(argInfo.nullable ? { nullable: true } : {}),
|
|
84
|
+
...(argInfo.enum ? { enum: argInfo.enum.refName } : {}),
|
|
85
|
+
...(argInfo.ref ? { ref: argInfo.ref } : {}),
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* The model's filter surface, which is what the root slice takes instead of a raw query. A client cannot
|
|
90
|
+
* offer a filter it cannot name, nor fill args it cannot type — so both travel, unlike the query map that
|
|
91
|
+
* used to stay server-side.
|
|
92
|
+
*/
|
|
93
|
+
static #serializeFilter(sliceCls: SliceCls): SerializedFilter | undefined {
|
|
94
|
+
const filterMeta = getFilterMeta(sliceCls.srv.db.filter, { allowEmpty: true });
|
|
95
|
+
if (!filterMeta) return undefined;
|
|
96
|
+
const filter = Object.fromEntries(
|
|
97
|
+
Object.entries(filterMeta.query).map(([key, filterInfo]) => [
|
|
98
|
+
key,
|
|
99
|
+
getFilterArgInfos(filterInfo).map(FetchSerializer.#serializeFilterArg),
|
|
100
|
+
]),
|
|
101
|
+
);
|
|
102
|
+
return { filter, sortKeys: Object.keys(filterMeta.sort) };
|
|
103
|
+
}
|
|
74
104
|
static #serializeSlice(sliceInfo: SliceInfo): SerializedSlice {
|
|
75
105
|
const guards = sliceInfo.signalOption.guards?.map((g) => g.name);
|
|
76
106
|
return {
|
|
@@ -92,12 +122,14 @@ export class FetchSerializer {
|
|
|
92
122
|
for (const [key, endpointInfo] of Object.entries(endpointMeta)) {
|
|
93
123
|
endpoint[key] = FetchSerializer.#serializeEndpoint(endpointInfo);
|
|
94
124
|
}
|
|
125
|
+
const filter = FetchSerializer.#serializeFilter(sliceCls);
|
|
95
126
|
|
|
96
|
-
const
|
|
127
|
+
const queryKeyArg = slice[""]?.args.find((arg) => arg.name === "queryKey");
|
|
128
|
+
if (queryKeyArg && filter) queryKeyArg.oneOf = Object.keys(filter.filter);
|
|
97
129
|
return {
|
|
98
130
|
...(prefix ? { prefix } : {}),
|
|
99
131
|
...(Object.keys(slice).length ? { slice } : {}),
|
|
100
|
-
...(
|
|
132
|
+
...(filter ? { filter } : {}),
|
|
101
133
|
...(sliceCls.getGuards.filter((g) => g.name !== "None").length
|
|
102
134
|
? { getGuards: sliceCls.getGuards.map((g) => g.name) }
|
|
103
135
|
: {}),
|
package/signal/signalContext.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import {
|
|
2
|
+
Any,
|
|
2
3
|
type BackendEnv,
|
|
3
4
|
type Cls,
|
|
4
5
|
FIELD_META,
|
|
@@ -457,6 +458,16 @@ export class HttpExecutionContext<Appended = unknown> {
|
|
|
457
458
|
if (!this.#url) this.#url = new URL(this.req.url);
|
|
458
459
|
return this.#url;
|
|
459
460
|
}
|
|
461
|
+
/** The read side of `HttpClient.makeUrl`'s `Any` rule: the query string carries the value JSON-encoded. */
|
|
462
|
+
static #parseAny(name: string, raw: string | string[] | null): unknown {
|
|
463
|
+
if (raw === null) return null;
|
|
464
|
+
if (Array.isArray(raw)) return raw.map((value) => HttpExecutionContext.#parseAny(name, value));
|
|
465
|
+
try {
|
|
466
|
+
return JSON.parse(raw);
|
|
467
|
+
} catch {
|
|
468
|
+
throw new Exception.BadRequest(`Invalid JSON in "${name}"`);
|
|
469
|
+
}
|
|
470
|
+
}
|
|
460
471
|
async getArgs(endpointInfo: EndpointInfo): Promise<unknown[]> {
|
|
461
472
|
if (endpointInfo.args.length === 0) return [];
|
|
462
473
|
this.params = this.req.params;
|
|
@@ -496,7 +507,8 @@ export class HttpExecutionContext<Appended = unknown> {
|
|
|
496
507
|
nullable: arg.option?.nullable,
|
|
497
508
|
});
|
|
498
509
|
case "search": {
|
|
499
|
-
const
|
|
510
|
+
const raw = arg.arrDepth ? this.url.searchParams.getAll(arg.name) : this.url.searchParams.get(arg.name);
|
|
511
|
+
const value = arg.argRef === Any ? HttpExecutionContext.#parseAny(arg.name, raw) : raw;
|
|
500
512
|
const result = deserialize(arg.argRef, arg.arrDepth, value, {
|
|
501
513
|
key: arg.name,
|
|
502
514
|
nullable: arg.option?.nullable,
|
package/signal/slice.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { Any, type Assign, type MergeAllKeyOfObjects, SLICE_DICT_SHAPE, SLICE_META } from "akanjs/base";
|
|
2
2
|
import { applyMixins } from "akanjs/common";
|
|
3
|
-
import type
|
|
4
|
-
import type { FilterInstance } from "akanjs/document";
|
|
3
|
+
import { type FilterInstance, FilterQueryError, resolveFilterQuery } from "akanjs/document";
|
|
5
4
|
import { type Adaptor, type AdaptorCls, dangerouslyAdapt, type ServiceModel } from "akanjs/service";
|
|
5
|
+
import { Exception } from "./exception";
|
|
6
6
|
import type { GuardCls } from "./guard";
|
|
7
7
|
import {
|
|
8
8
|
buildSlice,
|
|
@@ -52,7 +52,7 @@ interface RootSliceOption {
|
|
|
52
52
|
prefix?: string;
|
|
53
53
|
}
|
|
54
54
|
|
|
55
|
-
type
|
|
55
|
+
type RootSliceQueryKey<Filter extends FilterInstance> = Extract<keyof Filter["query"], string>;
|
|
56
56
|
|
|
57
57
|
type ExtendSliceInfoObj<
|
|
58
58
|
SrvModule extends ServiceModel,
|
|
@@ -91,7 +91,7 @@ export function slice<
|
|
|
91
91
|
_Light = CnstLight<SrvModule>,
|
|
92
92
|
_Insight = CnstInsight<SrvModule>,
|
|
93
93
|
_Filter extends FilterInstance = DbFilter<SrvModule>,
|
|
94
|
-
|
|
94
|
+
_QueryKey extends string = RootSliceQueryKey<_Filter>,
|
|
95
95
|
>(
|
|
96
96
|
srv: SrvModule,
|
|
97
97
|
option: RootSliceOption,
|
|
@@ -111,24 +111,18 @@ export function slice<
|
|
|
111
111
|
_Insight,
|
|
112
112
|
_Filter,
|
|
113
113
|
SrvMap<SrvModule>,
|
|
114
|
-
["
|
|
115
|
-
[
|
|
114
|
+
["queryKey", "args"],
|
|
115
|
+
[queryKey?: _QueryKey | null, args?: unknown[] | null],
|
|
116
116
|
[],
|
|
117
|
-
[
|
|
117
|
+
[_QueryKey | undefined, unknown[] | undefined]
|
|
118
118
|
>;
|
|
119
119
|
}
|
|
120
120
|
: ExtendSliceInfoObj<SrvModule, LibSlices>
|
|
121
121
|
>
|
|
122
122
|
> {
|
|
123
123
|
if (!srv.cnst || !srv.db) throw new Error("cnst and db are required");
|
|
124
|
-
const
|
|
125
|
-
|
|
126
|
-
srv.cnst.input,
|
|
127
|
-
srv.cnst.full,
|
|
128
|
-
srv.cnst.light,
|
|
129
|
-
srv.cnst.insight,
|
|
130
|
-
srv.db.filter,
|
|
131
|
-
);
|
|
124
|
+
const filterRef = srv.db.filter;
|
|
125
|
+
const init = buildSlice(srv.srv.refName, srv.cnst.input, srv.cnst.full, srv.cnst.light, srv.cnst.insight, filterRef);
|
|
132
126
|
const toGuards = (guard?: GuardCls | GuardCls[]) => (guard ? (Array.isArray(guard) ? guard : [guard]) : []);
|
|
133
127
|
const rootGuards = toGuards(option.guards?.root);
|
|
134
128
|
const getGuards = toGuards(option.guards?.get);
|
|
@@ -152,9 +146,19 @@ export function slice<
|
|
|
152
146
|
static removeGuards = removeGuards;
|
|
153
147
|
static [SLICE_META] = Object.assign(
|
|
154
148
|
{
|
|
149
|
+
|
|
155
150
|
[""]: init({ guards: rootGuards })
|
|
156
|
-
.search<"
|
|
157
|
-
.
|
|
151
|
+
.search<"queryKey", string>("queryKey", String)
|
|
152
|
+
.search<"args", unknown[]>("args", Any)
|
|
153
|
+
.exec((queryKey, args) => {
|
|
154
|
+
try {
|
|
155
|
+
return resolveFilterQuery(filterRef, queryKey, args);
|
|
156
|
+
} catch (error) {
|
|
157
|
+
|
|
158
|
+
if (error instanceof FilterQueryError) throw new Exception.BadRequest(error.message);
|
|
159
|
+
throw error;
|
|
160
|
+
}
|
|
161
|
+
}),
|
|
158
162
|
},
|
|
159
163
|
sliceBuilder(init as Parameters<BuildSlice>[0]),
|
|
160
164
|
);
|
package/signal/types.ts
CHANGED
|
@@ -120,12 +120,17 @@ export interface SerializedArg {
|
|
|
120
120
|
nullable?: boolean;
|
|
121
121
|
example?: string | number | boolean | Date;
|
|
122
122
|
enum?: string;
|
|
123
|
+
/** The values this arg accepts, when they are a fixed list the caller has to pick from. */
|
|
124
|
+
oneOf?: (string | number)[];
|
|
125
|
+
/** For an id a filter declared against a model: the model it points at, so a UI can offer a picker. */
|
|
126
|
+
ref?: string;
|
|
123
127
|
}
|
|
124
128
|
export interface SerializedEndpoint extends SerializedSignalOption {
|
|
125
129
|
type: "query" | "mutation" | "pubsub" | "message" | "prompt";
|
|
126
130
|
returns: SerializedReturns;
|
|
127
131
|
}
|
|
128
132
|
export interface SerializedFilter {
|
|
133
|
+
/** Every filter query the model declares, by key, with the args each one takes. */
|
|
129
134
|
filter: { [key: string]: SerializedArg[] };
|
|
130
135
|
sortKeys: string[];
|
|
131
136
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { SliceMeta } from "akanjs/fetch";
|
|
2
|
+
import type { SerializedArg } from "akanjs/signal";
|
|
2
3
|
import type { ReactNode } from "react";
|
|
3
4
|
import type { TransMessageOption } from "./makePageProto.d.ts";
|
|
4
5
|
type RuntimeTranslate = ((key: string, param?: Record<string, string | number>) => string) & {
|
|
@@ -41,6 +42,10 @@ type RuntimeFetch = typeof globalThis.fetch & {
|
|
|
41
42
|
slice: Record<string, SliceMeta>;
|
|
42
43
|
/** Sort keys per model refName, registered from each serialized signal's filter. */
|
|
43
44
|
sortKeyMap?: Map<string, string[]>;
|
|
45
|
+
/** Filter queries per model refName, with the args each one takes — what the root slice picks from. */
|
|
46
|
+
filterQueryMap?: Map<string, {
|
|
47
|
+
[queryKey: string]: SerializedArg[];
|
|
48
|
+
}>;
|
|
44
49
|
ws: RuntimeWs;
|
|
45
50
|
[key: string]: unknown;
|
|
46
51
|
};
|
package/types/client/types.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { BaseInsight } from "akanjs/constant";
|
|
2
|
-
import type { FetchInitForm, SliceMeta } from "akanjs/fetch";
|
|
2
|
+
import type { FetchInitForm, QuerySetting, SliceMeta } from "akanjs/fetch";
|
|
3
3
|
import type { ReactElement, ReactNode } from "react";
|
|
4
4
|
export type ReactFontStyle = "normal" | "italic" | "oblique";
|
|
5
5
|
export type ReactFontDisplay = "auto" | "block" | "swap" | "fallback" | "optional";
|
|
@@ -37,7 +37,7 @@ export interface ModelsProps<M extends {
|
|
|
37
37
|
}> {
|
|
38
38
|
className?: string;
|
|
39
39
|
slice?: SliceMeta;
|
|
40
|
-
query?:
|
|
40
|
+
query?: QuerySetting;
|
|
41
41
|
init?: FetchInitForm<any, any>;
|
|
42
42
|
onClickItem?: (model: M) => unknown;
|
|
43
43
|
}
|
|
@@ -57,7 +57,9 @@ export type ModelProps<T extends string, L extends {
|
|
|
57
57
|
export interface ModelDashboardProps<Summary> {
|
|
58
58
|
className?: string;
|
|
59
59
|
summary: Summary;
|
|
60
|
-
queryMap?:
|
|
60
|
+
queryMap?: {
|
|
61
|
+
[column: string]: QuerySetting;
|
|
62
|
+
};
|
|
61
63
|
columns?: (keyof Summary)[];
|
|
62
64
|
hidePresents?: boolean;
|
|
63
65
|
slice?: SliceMeta;
|
|
@@ -73,7 +73,7 @@ export declare class ModelDictInfo<Languages extends [string, ...string[]] = [st
|
|
|
73
73
|
};
|
|
74
74
|
static getBaseSignalDictionary<T extends string>(refName: T): BaseModelCrudGetSignalTranslation<T, [string, string]>;
|
|
75
75
|
static baseSliceDictionary: {
|
|
76
|
-
[key in ""]: FunctionTranslation<[string, string], "
|
|
76
|
+
[key in ""]: FunctionTranslation<[string, string], "queryKey" | "args">;
|
|
77
77
|
};
|
|
78
78
|
languages: Languages;
|
|
79
79
|
modelTranslation?: FieldTranslation<Languages>;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import type { Cls, MergeAllDoubleKeyOfObjects, MergeAllKeyOfTypes } from "akanjs/base";
|
|
1
|
+
import type { Cls, EnumInstance, MergeAllDoubleKeyOfObjects, MergeAllKeyOfTypes } from "akanjs/base";
|
|
2
2
|
import { FILTER_DICT_SHAPE, FILTER_META } from "akanjs/base";
|
|
3
|
-
import type
|
|
4
|
-
import type
|
|
3
|
+
import { type BaseObject, type ConstantFieldType, type ConstantFieldTypeInput, type DocumentModel, type FieldToValue, type PlainTypeToFieldType, type QueryOf, type Serialized } from "akanjs/constant";
|
|
4
|
+
import { type DocumentQuery, type DocumentQueryHelper } from "./documentQuery.d.ts";
|
|
5
5
|
import type { ConstantFilterMeta } from "./types.d.ts";
|
|
6
6
|
export declare const isFilterModel: (filterRef: Cls<unknown, {
|
|
7
7
|
[FILTER_META]?: ConstantFilterMeta;
|
|
@@ -19,6 +19,27 @@ export declare const getFilterInfoByKey: <ArgNames extends string[] = [], Args e
|
|
|
19
19
|
export declare const setFilterInfoByKey: <ArgNames extends string[] = [], Args extends any[] = any[], Model = any>(modelRef: Cls<Model>, key: string, filterInfo: FilterInfo<ArgNames, Args, Model>) => void;
|
|
20
20
|
export declare const getFilterSortByKey: (modelRef: FilterCls, key: string) => unknown;
|
|
21
21
|
export declare const fillMissingFilterArgs: (filterInfo: FilterInfo, args: unknown[]) => any[];
|
|
22
|
+
export interface FilterArgInfo {
|
|
23
|
+
name: string;
|
|
24
|
+
argRef: ConstantFieldType;
|
|
25
|
+
arrDepth: number;
|
|
26
|
+
enum?: EnumInstance;
|
|
27
|
+
nullable: boolean;
|
|
28
|
+
ref?: string;
|
|
29
|
+
}
|
|
30
|
+
/** Unwraps a filter arg's declaration the way `EndpointInfo.getArgInfo` unwraps an endpoint's. */
|
|
31
|
+
export declare const getFilterArgInfo: (arg: FilterInfo["args"][number]) => FilterArgInfo;
|
|
32
|
+
export declare const getFilterArgInfos: (filterInfo: FilterInfo) => FilterArgInfo[];
|
|
33
|
+
/** A caller named a filter that does not exist, or gave it arguments it cannot take. Its callers answer 400. */
|
|
34
|
+
export declare class FilterQueryError extends Error {
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Compiles a `(queryKey, args)` pair into the query its filter declares — the root slice's whole contract.
|
|
38
|
+
* Every arg is parsed by the type the filter declared for it, so a Date arg reaches the query as a Dayjs and
|
|
39
|
+
* an id that is not one is refused here rather than becoming a query that matches nothing. Args past the
|
|
40
|
+
* declared ones are dropped: the caller names a filter, never a query.
|
|
41
|
+
*/
|
|
42
|
+
export declare const resolveFilterQuery: (filterRef: FilterCls, queryKey?: string | null, args?: unknown[] | null) => QueryOf<any>;
|
|
22
43
|
export declare const assertFilterFitsCrud: (refName: string, queryKey: string, className: string) => void;
|
|
23
44
|
export type BaseFilterSortKey = "latest" | "oldest" | "relevance";
|
|
24
45
|
export type BaseFilterQueryKey = "any";
|
|
@@ -27,6 +27,9 @@ export declare class FetchClient {
|
|
|
27
27
|
readonly handler: Record<string, FetchHandler>;
|
|
28
28
|
readonly slice: Record<string, SliceMeta>;
|
|
29
29
|
readonly sortKeyMap: Map<string, string[]>;
|
|
30
|
+
readonly filterQueryMap: Map<string, {
|
|
31
|
+
[queryKey: string]: SerializedArg[];
|
|
32
|
+
}>;
|
|
30
33
|
serializedSignal: {
|
|
31
34
|
[key: string]: SerializedSignal;
|
|
32
35
|
};
|
|
@@ -6,6 +6,11 @@ export type SliceMeta = {
|
|
|
6
6
|
sliceName: string;
|
|
7
7
|
argLength: number;
|
|
8
8
|
};
|
|
9
|
+
/** What the root slice takes: one of the model's declared filter queries, and the args that filter asks for. */
|
|
10
|
+
export interface QuerySetting {
|
|
11
|
+
queryKey: string;
|
|
12
|
+
args?: unknown[];
|
|
13
|
+
}
|
|
9
14
|
export type ServerInit<RefName extends string, Light, Insight = any, QueryArgs = any, Filter extends FilterInstance = any, _CapitalizedRefName extends string = Capitalize<RefName>, _LightObj = GetStateObject<Light>, _InsightObj = GetStateObject<Insight>, _Sort = ExtractSort<Filter>> = SliceMeta & {
|
|
10
15
|
[K in `${RefName}ObjList`]: _LightObj[];
|
|
11
16
|
} & {
|
|
@@ -201,6 +201,8 @@ interface SqlDialect {
|
|
|
201
201
|
docColumn(): string;
|
|
202
202
|
docValuePlaceholder(): string;
|
|
203
203
|
extract(path: string): string;
|
|
204
|
+
projectExpr(path: string): string;
|
|
205
|
+
decodeProjected(value: unknown): unknown;
|
|
204
206
|
eq(path: string, value: unknown): SqlFrag;
|
|
205
207
|
ne(path: string, value: unknown): SqlFrag;
|
|
206
208
|
compare(path: string, op: "gt" | "gte" | "lt" | "lte", value: unknown): SqlFrag;
|
|
@@ -224,6 +226,8 @@ export declare class SqliteDialect implements SqlDialect {
|
|
|
224
226
|
docColumn(): string;
|
|
225
227
|
docValuePlaceholder(): string;
|
|
226
228
|
extract(path: string): string;
|
|
229
|
+
projectExpr(path: string): string;
|
|
230
|
+
decodeProjected(value: unknown): any;
|
|
227
231
|
eq(path: string, value: unknown): SqlFrag;
|
|
228
232
|
ne(path: string, value: unknown): SqlFrag;
|
|
229
233
|
compare(path: string, op: "gt" | "gte" | "lt" | "lte", value: unknown): SqlFrag;
|
|
@@ -247,6 +251,8 @@ export declare class PostgresDialect implements SqlDialect {
|
|
|
247
251
|
docColumn(): string;
|
|
248
252
|
docValuePlaceholder(): string;
|
|
249
253
|
extract(path: string): string;
|
|
254
|
+
projectExpr(path: string): string;
|
|
255
|
+
decodeProjected(value: unknown): unknown;
|
|
250
256
|
eq(path: string, value: unknown): SqlFrag;
|
|
251
257
|
ne(path: string, value: unknown): SqlFrag;
|
|
252
258
|
compare(path: string, op: "gt" | "gte" | "lt" | "lte", value: unknown): SqlFrag;
|
|
@@ -272,6 +278,7 @@ declare class QueryCompiler {
|
|
|
272
278
|
compile(query?: DocumentQuery): CompiledQuery;
|
|
273
279
|
orderBy(sort?: Record<string, 1 | -1>): string;
|
|
274
280
|
fieldExpr(path: string): string;
|
|
281
|
+
projectExpr(path: string): string;
|
|
275
282
|
private compileNode;
|
|
276
283
|
private compileField;
|
|
277
284
|
private assertPath;
|
|
@@ -379,7 +386,6 @@ export declare class SqlDocumentStore {
|
|
|
379
386
|
private findOneForWrite;
|
|
380
387
|
private pickByIdForWrite;
|
|
381
388
|
private writeUpdatedDocument;
|
|
382
|
-
private parseProjectedValue;
|
|
383
389
|
private decodeDocumentPayload;
|
|
384
390
|
private decodeFieldValue;
|
|
385
391
|
private decodeMapValue;
|
package/types/signal/slice.d.ts
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { type Assign, type MergeAllKeyOfObjects, SLICE_DICT_SHAPE, SLICE_META } from "akanjs/base";
|
|
2
|
-
import
|
|
3
|
-
import type { FilterInstance } from "akanjs/document";
|
|
2
|
+
import { type FilterInstance } from "akanjs/document";
|
|
4
3
|
import { type Adaptor, type AdaptorCls, type ServiceModel } from "akanjs/service";
|
|
5
4
|
import type { GuardCls } from "./guard.d.ts";
|
|
6
5
|
import { type SliceBuilder, type SliceInfo, type SliceInfoArgNames, type SliceInfoArgs, type SliceInfoInternalArgs, type SliceInfoServerArgs, type SliceInfoSrvs } from "./sliceInfo.d.ts";
|
|
@@ -42,19 +41,22 @@ interface RootSliceOption {
|
|
|
42
41
|
};
|
|
43
42
|
prefix?: string;
|
|
44
43
|
}
|
|
45
|
-
type
|
|
44
|
+
type RootSliceQueryKey<Filter extends FilterInstance> = Extract<keyof Filter["query"], string>;
|
|
46
45
|
type ExtendSliceInfoObj<SrvModule extends ServiceModel, LibSlices extends SliceCls[], _Input = CnstInput<SrvModule>, _Full = CnstFull<SrvModule>, _Light = CnstLight<SrvModule>, _Insight = CnstInsight<SrvModule>, _Filter extends FilterInstance = DbFilter<SrvModule>, _Merged = MergeAllKeyOfObjects<LibSlices, typeof SLICE_META>> = {
|
|
47
46
|
[K in keyof _Merged]: _Merged[K] extends SliceInfo ? SliceInfo<SrvRefName<SrvModule>, _Input, _Full, _Light, _Insight, _Filter, SliceInfoSrvs<_Merged[K]>, SliceInfoArgNames<_Merged[K]>, SliceInfoArgs<_Merged[K]>, SliceInfoInternalArgs<_Merged[K]>, SliceInfoServerArgs<_Merged[K]>> : never;
|
|
48
47
|
};
|
|
49
48
|
/** Builds database-backed slice APIs for list, insight, init, view, edit, create, update, and remove flows. */
|
|
50
|
-
export declare function slice<SrvModule extends ServiceModel, BuildSlice extends SliceBuilder<SrvModule>, LibSlices extends SliceCls[], _Input = CnstInput<SrvModule>, _Full = CnstFull<SrvModule>, _Light = CnstLight<SrvModule>, _Insight = CnstInsight<SrvModule>, _Filter extends FilterInstance = DbFilter<SrvModule>,
|
|
49
|
+
export declare function slice<SrvModule extends ServiceModel, BuildSlice extends SliceBuilder<SrvModule>, LibSlices extends SliceCls[], _Input = CnstInput<SrvModule>, _Full = CnstFull<SrvModule>, _Light = CnstLight<SrvModule>, _Insight = CnstInsight<SrvModule>, _Filter extends FilterInstance = DbFilter<SrvModule>, _QueryKey extends string = RootSliceQueryKey<_Filter>>(srv: SrvModule, option: RootSliceOption, sliceBuilder: BuildSlice, ...libSlices: LibSlices): SliceCls<SrvModule, Assign<ReturnType<BuildSlice>, LibSlices extends [] ? {
|
|
51
50
|
[""]: SliceInfo<SrvRefName<SrvModule>, _Input, _Full, _Light, _Insight, _Filter, SrvMap<SrvModule>, [
|
|
52
|
-
"
|
|
51
|
+
"queryKey",
|
|
52
|
+
"args"
|
|
53
53
|
], [
|
|
54
|
-
|
|
54
|
+
queryKey?: _QueryKey | null,
|
|
55
|
+
args?: unknown[] | null
|
|
55
56
|
], [
|
|
56
57
|
], [
|
|
57
|
-
|
|
58
|
+
_QueryKey | undefined,
|
|
59
|
+
unknown[] | undefined
|
|
58
60
|
]>;
|
|
59
61
|
} : ExtendSliceInfoObj<SrvModule, LibSlices>>>;
|
|
60
62
|
export {};
|
package/types/signal/types.d.ts
CHANGED
|
@@ -107,12 +107,17 @@ export interface SerializedArg {
|
|
|
107
107
|
nullable?: boolean;
|
|
108
108
|
example?: string | number | boolean | Date;
|
|
109
109
|
enum?: string;
|
|
110
|
+
/** The values this arg accepts, when they are a fixed list the caller has to pick from. */
|
|
111
|
+
oneOf?: (string | number)[];
|
|
112
|
+
/** For an id a filter declared against a model: the model it points at, so a UI can offer a picker. */
|
|
113
|
+
ref?: string;
|
|
110
114
|
}
|
|
111
115
|
export interface SerializedEndpoint extends SerializedSignalOption {
|
|
112
116
|
type: "query" | "mutation" | "pubsub" | "message" | "prompt";
|
|
113
117
|
returns: SerializedReturns;
|
|
114
118
|
}
|
|
115
119
|
export interface SerializedFilter {
|
|
120
|
+
/** Every filter query the model declares, by key, with the args each one takes. */
|
|
116
121
|
filter: {
|
|
117
122
|
[key: string]: SerializedArg[];
|
|
118
123
|
};
|
|
@@ -1,10 +1,12 @@
|
|
|
1
|
-
import type { SliceMeta } from "akanjs/fetch";
|
|
1
|
+
import type { QuerySetting, SliceMeta } from "akanjs/fetch";
|
|
2
2
|
export interface DashboardProps<T extends string, State> {
|
|
3
3
|
className?: string;
|
|
4
4
|
summary: Record<string, unknown>;
|
|
5
5
|
slice: SliceMeta;
|
|
6
6
|
/** Columns that narrow the listing when clicked. A column absent from the map renders as a plain tile. */
|
|
7
|
-
queryMap?:
|
|
7
|
+
queryMap?: {
|
|
8
|
+
[column: string]: QuerySetting;
|
|
9
|
+
};
|
|
8
10
|
columns?: string[];
|
|
9
11
|
presents?: string[];
|
|
10
12
|
hidePresents?: boolean;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { type DataAction, type DataColumn, type DataTool, type ModelInsightProps, type ModelProps } from "akanjs/client";
|
|
2
|
-
import type { FetchInitForm, SliceMeta } from "akanjs/fetch";
|
|
2
|
+
import type { FetchInitForm, QuerySetting, SliceMeta } from "akanjs/fetch";
|
|
3
3
|
import { type ReactNode } from "react";
|
|
4
4
|
export interface ListContainerProps<T extends string, State, Input, Full extends {
|
|
5
5
|
id: string;
|
|
@@ -12,8 +12,12 @@ export interface ListContainerProps<T extends string, State, Input, Full extends
|
|
|
12
12
|
cardListClassName?: string;
|
|
13
13
|
/** Initial rendering mode. The toolbar toggle switches it from here. */
|
|
14
14
|
type?: "card" | "list";
|
|
15
|
-
/**
|
|
16
|
-
query?:
|
|
15
|
+
/** Fixed filter query for this listing. Given one, the panel is scoped and offers no query maker. */
|
|
16
|
+
query?: QuerySetting;
|
|
17
|
+
/** Summary column to filter query. A `?filter=<column>` link opens the listing on the query it names. */
|
|
18
|
+
queryMap?: {
|
|
19
|
+
[column: string]: QuerySetting;
|
|
20
|
+
};
|
|
17
21
|
/** Initial fetch form: page, limit, sort, and the default values a new model starts from. */
|
|
18
22
|
init?: FetchInitForm<Input, any>;
|
|
19
23
|
/** Generated slice metadata for the target model. */
|
|
@@ -46,4 +50,4 @@ export default function ListContainer<T extends string, State, Input, Full exten
|
|
|
46
50
|
id: string;
|
|
47
51
|
}, Light extends {
|
|
48
52
|
id: string;
|
|
49
|
-
}>({ className, cardListClassName, type, query, init, create, slice, title, sort, columns, actions, tools, renderDashboard, renderItem, renderTemplate, renderTitle, renderView, renderQueryMaker, renderInsight, renderLoading, }: ListContainerProps<T, State, Input, Full, Light>): import("react/jsx-runtime").JSX.Element;
|
|
53
|
+
}>({ className, cardListClassName, type, query, queryMap, init, create, slice, title, sort, columns, actions, tools, renderDashboard, renderItem, renderTemplate, renderTitle, renderView, renderQueryMaker, renderInsight, renderLoading, }: ListContainerProps<T, State, Input, Full, Light>): import("react/jsx-runtime").JSX.Element;
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import type { SliceMeta } from "akanjs/fetch";
|
|
1
|
+
import type { QuerySetting, SliceMeta } from "akanjs/fetch";
|
|
2
2
|
interface QueryMakerProps {
|
|
3
3
|
className?: string;
|
|
4
4
|
slice: SliceMeta;
|
|
5
|
-
query?:
|
|
5
|
+
query?: QuerySetting;
|
|
6
6
|
}
|
|
7
|
-
export default function QueryMaker({ className, slice, query }: QueryMakerProps): null;
|
|
7
|
+
export default function QueryMaker({ className, slice, query }: QueryMakerProps): import("react/jsx-runtime").JSX.Element | null;
|
|
8
8
|
export {};
|
package/types/ui/Data/index.d.ts
CHANGED
|
@@ -5,5 +5,6 @@ export declare const Data: {
|
|
|
5
5
|
Item: typeof import("./Item.d.ts").default;
|
|
6
6
|
ListContainer: typeof import("./ListContainer.d.ts").default;
|
|
7
7
|
Pagination: typeof import("./Pagination.d.ts").default;
|
|
8
|
+
QueryMaker: typeof import("./QueryMaker.d.ts").default;
|
|
8
9
|
TableList: typeof import("./TableList.d.ts").default;
|
|
9
10
|
};
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { SliceMeta } from "akanjs/fetch";
|
|
1
|
+
import type { QuerySetting, SliceMeta } from "akanjs/fetch";
|
|
2
2
|
import type { ListContainerProps } from "../Data/ListContainer.d.ts";
|
|
3
3
|
interface AdminPanelProps<T extends string, State, Input, Full extends {
|
|
4
4
|
id: string;
|
|
@@ -21,9 +21,9 @@ interface AdminPanelProps<T extends string, State, Input, Full extends {
|
|
|
21
21
|
template?: any;
|
|
22
22
|
unit?: any;
|
|
23
23
|
view?: any;
|
|
24
|
-
/** Summary column to query
|
|
24
|
+
/** Summary column to filter query. Only a column this map names becomes a link to its filtered listing. */
|
|
25
25
|
queryMap?: {
|
|
26
|
-
[
|
|
26
|
+
[column: string]: QuerySetting;
|
|
27
27
|
};
|
|
28
28
|
/** Keys of the app's `summary` state shown as dashboard tiles above the list. */
|
|
29
29
|
summaryColumns?: string[];
|
package/types/ui/Signal/Arg.d.ts
CHANGED
|
@@ -9,7 +9,7 @@ declare function Arg({ argType, value, onChange }: ArgProps): import("react/jsx-
|
|
|
9
9
|
declare namespace Arg {
|
|
10
10
|
var Table: ({ refName, endpointKey, args }: ArgTableProps) => import("react/jsx-runtime").JSX.Element;
|
|
11
11
|
var Param: ({ endpointKey, arg, value, onChange }: ArgParamProps) => import("react/jsx-runtime").JSX.Element;
|
|
12
|
-
var Query: ({ endpointKey, arg, value, onChange }: ArgQueryProps) => import("react/jsx-runtime").JSX.Element;
|
|
12
|
+
var Query: ({ endpointKey, arg, label, value, onChange }: ArgQueryProps) => import("react/jsx-runtime").JSX.Element;
|
|
13
13
|
var FormData: ({ endpointKey, arg, value, onChange }: ArgFormDataProps) => import("react/jsx-runtime").JSX.Element;
|
|
14
14
|
var ID: ({ value, onChange }: ArgIDProps) => import("react/jsx-runtime").JSX.Element;
|
|
15
15
|
var Int: ({ value, onChange }: ArgIntProps) => import("react/jsx-runtime").JSX.Element;
|
|
@@ -35,6 +35,7 @@ interface ArgParamProps {
|
|
|
35
35
|
interface ArgQueryProps {
|
|
36
36
|
endpointKey: string;
|
|
37
37
|
arg: SerializedArg;
|
|
38
|
+
label?: string;
|
|
38
39
|
value: any;
|
|
39
40
|
onChange: (value: any) => void;
|
|
40
41
|
}
|
|
@@ -45,7 +46,7 @@ interface ArgFormDataProps {
|
|
|
45
46
|
onChange: (value: any) => void;
|
|
46
47
|
}
|
|
47
48
|
interface ArgIDProps {
|
|
48
|
-
value: string;
|
|
49
|
+
value: string | null;
|
|
49
50
|
onChange: (value: string) => void;
|
|
50
51
|
}
|
|
51
52
|
interface ArgIntProps {
|
|
@@ -57,7 +58,7 @@ interface ArgFloatProps {
|
|
|
57
58
|
onChange: (value: number) => void;
|
|
58
59
|
}
|
|
59
60
|
interface ArgStringProps {
|
|
60
|
-
value: string;
|
|
61
|
+
value: string | null;
|
|
61
62
|
onChange: (value: string) => void;
|
|
62
63
|
}
|
|
63
64
|
interface ArgBooleanProps {
|
package/ui/Data/Dashboard.tsx
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { cn, usePage } from "akanjs/client";
|
|
3
|
-
import type { SliceMeta } from "akanjs/fetch";
|
|
3
|
+
import type { QuerySetting, SliceMeta } from "akanjs/fetch";
|
|
4
4
|
import { st } from "akanjs/store";
|
|
5
5
|
|
|
6
6
|
import { Link } from "../Link";
|
|
@@ -11,7 +11,7 @@ export interface DashboardProps<T extends string, State> {
|
|
|
11
11
|
summary: Record<string, unknown>;
|
|
12
12
|
slice: SliceMeta;
|
|
13
13
|
/** Columns that narrow the listing when clicked. A column absent from the map renders as a plain tile. */
|
|
14
|
-
queryMap?:
|
|
14
|
+
queryMap?: { [column: string]: QuerySetting };
|
|
15
15
|
columns?: string[];
|
|
16
16
|
presents?: string[];
|
|
17
17
|
hidePresents?: boolean;
|
|
@@ -12,7 +12,7 @@ import {
|
|
|
12
12
|
} from "akanjs/client";
|
|
13
13
|
import { capitalize } from "akanjs/common";
|
|
14
14
|
import { type BaseInsight, ConstantRegistry, labelOf } from "akanjs/constant";
|
|
15
|
-
import type { FetchInitForm, SliceMeta } from "akanjs/fetch";
|
|
15
|
+
import type { FetchInitForm, QuerySetting, SliceMeta } from "akanjs/fetch";
|
|
16
16
|
import { st } from "akanjs/store";
|
|
17
17
|
import { useScreenScope } from "akanjs/webkit";
|
|
18
18
|
import { type ReactNode, useEffect, useState } from "react";
|
|
@@ -36,6 +36,7 @@ import { Select } from "../Select";
|
|
|
36
36
|
import DataCardList from "./CardList";
|
|
37
37
|
import { columnKey, downloadBlob, toCsvBlob, toJsonBlob } from "./dataExport";
|
|
38
38
|
import { dictLabel } from "./dataText";
|
|
39
|
+
import DataQueryMaker from "./QueryMaker";
|
|
39
40
|
import DataTableList from "./TableList";
|
|
40
41
|
|
|
41
42
|
const controlClassName = "h-9";
|
|
@@ -55,8 +56,10 @@ export interface ListContainerProps<
|
|
|
55
56
|
cardListClassName?: string;
|
|
56
57
|
/** Initial rendering mode. The toolbar toggle switches it from here. */
|
|
57
58
|
type?: "card" | "list";
|
|
58
|
-
/**
|
|
59
|
-
query?:
|
|
59
|
+
/** Fixed filter query for this listing. Given one, the panel is scoped and offers no query maker. */
|
|
60
|
+
query?: QuerySetting;
|
|
61
|
+
/** Summary column to filter query. A `?filter=<column>` link opens the listing on the query it names. */
|
|
62
|
+
queryMap?: { [column: string]: QuerySetting };
|
|
60
63
|
/** Initial fetch form: page, limit, sort, and the default values a new model starts from. */
|
|
61
64
|
init?: FetchInitForm<Input, any>;
|
|
62
65
|
/** Generated slice metadata for the target model. */
|
|
@@ -100,6 +103,7 @@ export default function ListContainer<
|
|
|
100
103
|
cardListClassName,
|
|
101
104
|
type = "card",
|
|
102
105
|
query,
|
|
106
|
+
queryMap,
|
|
103
107
|
init,
|
|
104
108
|
create = true,
|
|
105
109
|
slice,
|
|
@@ -167,10 +171,13 @@ export default function ListContainer<
|
|
|
167
171
|
const sortOfModel = storeUse[namesOfSlice.sortOfModel]() as string;
|
|
168
172
|
const modelInsight = storeUse[namesOfSlice.modelInsight]() as BaseInsight;
|
|
169
173
|
const modelListLoading = storeUse[namesOfSlice.modelListLoading]() as string | boolean;
|
|
174
|
+
const searchParams = st.use.searchParams({ agent: false });
|
|
175
|
+
const filter = Array.isArray(searchParams.filter) ? searchParams.filter[0] : searchParams.filter;
|
|
176
|
+
const initQuery = query ?? (filter ? queryMap?.[filter] : undefined);
|
|
170
177
|
useEffect(() => {
|
|
171
178
|
|
|
172
179
|
const queryArgs = new Array(slice.argLength).fill(null) as unknown[];
|
|
173
|
-
if (
|
|
180
|
+
if (initQuery) [queryArgs[0], queryArgs[1]] = [initQuery.queryKey, initQuery.args ?? []];
|
|
174
181
|
void storeDo[namesOfSlice.initModel](...queryArgs, { sort, ...init });
|
|
175
182
|
}, []);
|
|
176
183
|
|
|
@@ -254,7 +261,9 @@ export default function ListContainer<
|
|
|
254
261
|
if (!Stat || !summary) return null;
|
|
255
262
|
return summaryLoading ? <Loading.Skeleton className="mb-4" active /> : <Stat summary={summary} hidePresents />;
|
|
256
263
|
};
|
|
257
|
-
|
|
264
|
+
|
|
265
|
+
const RenderQueryMaker =
|
|
266
|
+
renderQueryMaker ?? (query ? undefined : () => <DataQueryMaker slice={slice} query={initQuery} />);
|
|
258
267
|
const RenderInsight = (): ReactNode => (renderInsight ? renderInsight({ insight: modelInsight }) : null);
|
|
259
268
|
const RenderTemplate = renderTemplate;
|
|
260
269
|
const RenderTools = (): ReactNode => {
|
package/ui/Data/QueryMaker.tsx
CHANGED
|
@@ -1,21 +1,71 @@
|
|
|
1
1
|
"use client";
|
|
2
|
-
|
|
3
|
-
import
|
|
2
|
+
import { cn, fetch, usePage } from "akanjs/client";
|
|
3
|
+
import { capitalize } from "akanjs/common";
|
|
4
|
+
import type { QuerySetting, SliceMeta } from "akanjs/fetch";
|
|
5
|
+
import type { SerializedArg } from "akanjs/signal";
|
|
6
|
+
import { st } from "akanjs/store";
|
|
7
|
+
import { useDebounce } from "akanjs/webkit";
|
|
8
|
+
import { useState } from "react";
|
|
9
|
+
import { Select } from "../Select";
|
|
10
|
+
import Arg from "../Signal/Arg";
|
|
11
|
+
import { dictLabel } from "./dataText";
|
|
4
12
|
|
|
5
13
|
interface QueryMakerProps {
|
|
6
14
|
className?: string;
|
|
7
15
|
slice: SliceMeta;
|
|
8
|
-
query?:
|
|
9
|
-
}
|
|
10
|
-
interface QuerySetting {
|
|
11
|
-
queryKey: string;
|
|
12
|
-
arg: Record<string, unknown>;
|
|
16
|
+
query?: QuerySetting;
|
|
13
17
|
}
|
|
14
|
-
|
|
15
|
-
const
|
|
18
|
+
|
|
19
|
+
const isFillableArg = (arg: SerializedArg) => !arg.modelType;
|
|
20
|
+
const defaultArg = (arg: SerializedArg) => ((arg.arrDepth ?? 0) > 0 ? [] : null);
|
|
16
21
|
|
|
17
22
|
export default function QueryMaker({ className, slice, query }: QueryMakerProps) {
|
|
18
|
-
const {
|
|
19
|
-
|
|
23
|
+
const { l } = usePage();
|
|
24
|
+
const { refName, sliceName } = slice;
|
|
25
|
+
const storeDo = st.do as unknown as { [key: string]: (...args: unknown[]) => Promise<void> };
|
|
26
|
+
const filterQuery = fetch.filterQueryMap?.get(refName) ?? {};
|
|
27
|
+
const queryKeys = Object.entries(filterQuery)
|
|
28
|
+
.filter(([, args]) => args.every(isFillableArg))
|
|
29
|
+
.map(([queryKey]) => queryKey);
|
|
30
|
+
const [setting, setSetting] = useState<Required<QuerySetting>>({
|
|
31
|
+
queryKey: query?.queryKey ?? queryKeys[0] ?? "any",
|
|
32
|
+
args: query?.args ?? [],
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
const applyQuery = useDebounce((setting: Required<QuerySetting>) => {
|
|
36
|
+
void storeDo[`setQueryArgsOf${capitalize(sliceName)}`](setting.queryKey, setting.args);
|
|
37
|
+
}, []);
|
|
38
|
+
const update = (setting: Required<QuerySetting>) => {
|
|
39
|
+
setSetting(setting);
|
|
40
|
+
applyQuery(setting);
|
|
41
|
+
};
|
|
42
|
+
const args = filterQuery[setting.queryKey] ?? [];
|
|
43
|
+
if (queryKeys.length < 2) return null;
|
|
44
|
+
return (
|
|
45
|
+
<div className={cn("mb-4 flex w-full flex-col gap-1 rounded-box border border-border p-3", className)}>
|
|
46
|
+
<Select<string>
|
|
47
|
+
className="w-full md:w-72"
|
|
48
|
+
value={setting.queryKey}
|
|
49
|
+
options={queryKeys.map((queryKey) => ({
|
|
50
|
+
label: dictLabel(l._, `${refName}.query.${queryKey}`, queryKey),
|
|
51
|
+
value: queryKey,
|
|
52
|
+
}))}
|
|
53
|
+
onChange={(queryKey) => {
|
|
54
|
+
update({ queryKey, args: (filterQuery[queryKey] ?? []).map(defaultArg) });
|
|
55
|
+
}}
|
|
56
|
+
/>
|
|
57
|
+
{args.map((arg, idx) => (
|
|
58
|
+
<Arg.Query
|
|
59
|
+
key={arg.name}
|
|
60
|
+
endpointKey={setting.queryKey}
|
|
61
|
+
arg={arg}
|
|
62
|
+
label={dictLabel(l._, `${refName}.query.${setting.queryKey}.arg.${arg.name}`, arg.name)}
|
|
63
|
+
value={setting.args[idx] ?? defaultArg(arg)}
|
|
64
|
+
onChange={(value: unknown) => {
|
|
65
|
+
update({ ...setting, args: args.map((a, i) => (i === idx ? value : (setting.args[i] ?? defaultArg(a)))) });
|
|
66
|
+
}}
|
|
67
|
+
/>
|
|
68
|
+
))}
|
|
69
|
+
</div>
|
|
70
|
+
);
|
|
20
71
|
}
|
|
21
|
-
|
package/ui/Data/index.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { CardList, Dashboard, Insight, Item, ListContainer, Pagination, TableList } from "./index_";
|
|
1
|
+
import { CardList, Dashboard, Insight, Item, ListContainer, Pagination, QueryMaker, TableList } from "./index_";
|
|
2
2
|
|
|
3
3
|
export const Data = {
|
|
4
4
|
CardList,
|
|
@@ -7,5 +7,6 @@ export const Data = {
|
|
|
7
7
|
Item,
|
|
8
8
|
ListContainer,
|
|
9
9
|
Pagination,
|
|
10
|
+
QueryMaker,
|
|
10
11
|
TableList,
|
|
11
12
|
};
|
package/ui/Model/AdminPanel.tsx
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { ModelProps } from "akanjs/client";
|
|
2
2
|
import type { BaseInsight } from "akanjs/constant";
|
|
3
|
-
import type { SliceMeta } from "akanjs/fetch";
|
|
3
|
+
import type { QuerySetting, SliceMeta } from "akanjs/fetch";
|
|
4
4
|
import type { ComponentType, ReactNode } from "react";
|
|
5
5
|
import { Data } from "../Data";
|
|
6
6
|
import type { ListContainerProps } from "../Data/ListContainer";
|
|
@@ -17,8 +17,8 @@ interface AdminPanelProps<T extends string, State, Input, Full extends { id: str
|
|
|
17
17
|
template?: any;
|
|
18
18
|
unit?: any;
|
|
19
19
|
view?: any;
|
|
20
|
-
/** Summary column to query
|
|
21
|
-
queryMap?: { [
|
|
20
|
+
/** Summary column to filter query. Only a column this map names becomes a link to its filtered listing. */
|
|
21
|
+
queryMap?: { [column: string]: QuerySetting };
|
|
22
22
|
/** Keys of the app's `summary` state shown as dashboard tiles above the list. */
|
|
23
23
|
summaryColumns?: string[];
|
|
24
24
|
/** Model insight keys shown above the list. The header already carries the total count. */
|
|
@@ -70,6 +70,7 @@ export default function AdminPanel<
|
|
|
70
70
|
return (
|
|
71
71
|
<Data.ListContainer
|
|
72
72
|
slice={slice}
|
|
73
|
+
queryMap={queryMap}
|
|
73
74
|
renderItem={Unit}
|
|
74
75
|
renderInsight={renderInsight}
|
|
75
76
|
renderDashboard={renderDashboard}
|
package/ui/Signal/Arg.tsx
CHANGED
|
@@ -10,6 +10,7 @@ import { buttonRecipe } from "../Button";
|
|
|
10
10
|
import { DatePicker } from "../DatePicker";
|
|
11
11
|
import { Input } from "../Input";
|
|
12
12
|
import { dictText, docDash, docUi } from "../Reference";
|
|
13
|
+
import { Select } from "../Select";
|
|
13
14
|
import { Tooltip } from "../Tooltip";
|
|
14
15
|
import UiObject from "./Object";
|
|
15
16
|
import { signalUi } from "./style";
|
|
@@ -137,20 +138,32 @@ Arg.Param = ArgParam;
|
|
|
137
138
|
interface ArgQueryProps {
|
|
138
139
|
endpointKey: string;
|
|
139
140
|
arg: SerializedArg;
|
|
141
|
+
label?: string;
|
|
140
142
|
value: any;
|
|
141
143
|
onChange: (value: any) => void;
|
|
142
144
|
}
|
|
143
|
-
const ArgQuery = ({ endpointKey, arg, value, onChange }: ArgQueryProps) => {
|
|
145
|
+
const ArgQuery = ({ endpointKey, arg, label, value, onChange }: ArgQueryProps) => {
|
|
144
146
|
const argRef = ConstantRegistry.getModelRef(arg.refName, arg.modelType);
|
|
145
147
|
if (!PrimitiveRegistry.has(argRef)) throw new Error(`Query arg - ${endpointKey}/${arg.name} must be scalar`);
|
|
146
148
|
else if ((arg.arrDepth ?? 0) > 1)
|
|
147
149
|
throw new Error(`Query arg - ${endpointKey}/${arg.name} must not be more than 2D array`);
|
|
148
150
|
const argType = PrimitiveRegistry.getName(argRef as typeof PrimitiveScalar) as DefaultPrimitiveName;
|
|
151
|
+
const enumRef = arg.enum ? ConstantRegistry.enum.get(arg.enum) : undefined;
|
|
152
|
+
const options: (string | number)[] = arg.oneOf ?? (enumRef ? [...enumRef.values] : []);
|
|
153
|
+
const multiple = (arg.arrDepth ?? 0) > 0;
|
|
149
154
|
return (
|
|
150
155
|
<div className={signalUi.inputRow}>
|
|
151
|
-
<div className={signalUi.inputLabel}>{arg.name}</div>
|
|
156
|
+
<div className={signalUi.inputLabel}>{label ?? arg.name}</div>
|
|
152
157
|
<div className="w-full">
|
|
153
|
-
{
|
|
158
|
+
{options.length ? (
|
|
159
|
+
<Select<string | number, boolean>
|
|
160
|
+
options={options.map((option) => ({ label: option, value: option }))}
|
|
161
|
+
multiple={multiple}
|
|
162
|
+
nullable={arg.nullable}
|
|
163
|
+
value={multiple ? ((value as (string | number)[] | null) ?? []) : (value as string | number)}
|
|
164
|
+
onChange={onChange}
|
|
165
|
+
/>
|
|
166
|
+
) : multiple && Array.isArray(value) ? (
|
|
154
167
|
<div className="flex flex-col gap-2">
|
|
155
168
|
{value.map((val, idx) => (
|
|
156
169
|
<div className="flex items-center gap-2" key={idx}>
|
|
@@ -214,14 +227,14 @@ const ArgFormData = ({ endpointKey, arg, value, onChange }: ArgFormDataProps) =>
|
|
|
214
227
|
Arg.FormData = ArgFormData;
|
|
215
228
|
|
|
216
229
|
interface ArgIDProps {
|
|
217
|
-
value: string;
|
|
230
|
+
value: string | null;
|
|
218
231
|
onChange: (value: string) => void;
|
|
219
232
|
}
|
|
220
233
|
const ArgID = ({ value, onChange }: ArgIDProps) => {
|
|
221
234
|
return (
|
|
222
235
|
<Input
|
|
223
236
|
inputClassName="w-full font-mono"
|
|
224
|
-
value={value}
|
|
237
|
+
value={value ?? ""}
|
|
225
238
|
onChange={(value) => {
|
|
226
239
|
onChange(value);
|
|
227
240
|
}}
|
|
@@ -268,14 +281,14 @@ const ArgFloat = ({ value, onChange }: ArgFloatProps) => {
|
|
|
268
281
|
Arg.Float = ArgFloat;
|
|
269
282
|
|
|
270
283
|
interface ArgStringProps {
|
|
271
|
-
value: string;
|
|
284
|
+
value: string | null;
|
|
272
285
|
onChange: (value: string) => void;
|
|
273
286
|
}
|
|
274
287
|
const ArgString = ({ value, onChange }: ArgStringProps) => {
|
|
275
288
|
return (
|
|
276
289
|
<Input
|
|
277
290
|
inputClassName="w-full font-mono"
|
|
278
|
-
value={value}
|
|
291
|
+
value={value ?? ""}
|
|
279
292
|
onChange={(value) => {
|
|
280
293
|
onChange(value);
|
|
281
294
|
}}
|
package/ui/Signal/makeExample.ts
CHANGED
|
@@ -44,7 +44,7 @@ const getRequestExample = (modelRef: Cls) => {
|
|
|
44
44
|
else
|
|
45
45
|
example[key] = (
|
|
46
46
|
(field.example ?? field.enum)
|
|
47
|
-
? arraiedModel(field.example ??
|
|
47
|
+
? arraiedModel(field.example ?? field.enum?.values[0], field.optArrDepth)
|
|
48
48
|
: arraiedModel(getRequestExample(field.modelRef), field.arrDepth)
|
|
49
49
|
) as unknown;
|
|
50
50
|
});
|