akanjs 2.4.2-rc.1 → 2.4.2-rc.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/common/fileUpload.ts +1 -1
- package/common/index.ts +1 -1
- package/common/subRoute.ts +61 -0
- package/constant/cascadePaths.ts +32 -0
- package/constant/fieldInfo.ts +7 -0
- package/constant/index.ts +1 -0
- package/constant/purify.ts +9 -10
- package/constant/via.ts +6 -0
- package/fetch/client/fetchClient.ts +1 -1
- package/fetch/client/httpClient.ts +9 -5
- package/fetch/fetchType/sliceFetch.type.ts +1 -1
- package/package.json +1 -1
- package/server/di/diLifecycle.ts +7 -1
- package/server/proxy/hostBasePathWebProxy.ts +16 -5
- package/server/resolver/service.resolver.ts +27 -5
- package/server/webRouter.ts +27 -11
- package/signal/endpointInfo.ts +2 -1
- package/signal/slice.ts +1 -1
- package/signal/sliceInfo.ts +2 -1
- package/store/action.ts +18 -2
- package/store/state.ts +9 -1
- package/store/storeInstance.ts +2 -0
- package/test/sampleOf.ts +3 -3
- package/types/common/fileUpload.d.ts +1 -1
- package/types/common/index.d.ts +1 -1
- package/types/common/subRoute.d.ts +19 -0
- package/types/constant/cascadePaths.d.ts +11 -0
- package/types/constant/fieldInfo.d.ts +4 -0
- package/types/constant/index.d.ts +1 -0
- package/types/constant/purify.d.ts +3 -2
- package/types/constant/via.d.ts +4 -0
- package/types/fetch/fetchType/sliceFetch.type.d.ts +1 -1
- package/types/server/resolver/service.resolver.d.ts +3 -3
- package/types/signal/endpointInfo.d.ts +2 -2
- package/types/signal/slice.d.ts +1 -1
- package/types/signal/sliceInfo.d.ts +2 -2
- package/types/store/action.d.ts +1 -1
- package/types/store/state.d.ts +4 -2
- package/types/test/sampleOf.d.ts +2 -2
- package/types/ui/Load/Units.d.ts +3 -1
- package/ui/Load/Units.tsx +17 -0
- package/ui/Signal/Arg.tsx +1 -1
package/common/fileUpload.ts
CHANGED
|
@@ -10,7 +10,7 @@ interface FileUploadSerializedSignal {
|
|
|
10
10
|
/** Framework-owned file-upload contract shared by client-safe packages. */
|
|
11
11
|
export const fileUploadContract = {
|
|
12
12
|
fields: { files: "files", metas: "metas", type: "type", parentId: "parentId" },
|
|
13
|
-
buildMetas: (fileList: FileList) =>
|
|
13
|
+
buildMetas: (fileList: FileList | File[]) =>
|
|
14
14
|
Array.from(fileList).map((f) => ({ lastModifiedAt: new Date(f.lastModified).toISOString(), size: f.size })),
|
|
15
15
|
} as const;
|
|
16
16
|
|
package/common/index.ts
CHANGED
|
@@ -52,7 +52,7 @@ export {
|
|
|
52
52
|
} from "./routeConvention";
|
|
53
53
|
export { sleep } from "./sleep";
|
|
54
54
|
export { splitVersion } from "./splitVersion";
|
|
55
|
-
export { getBasePathFromPathname, parseBasePaths } from "./subRoute";
|
|
55
|
+
export { getBasePathFromPathname, parseBasePaths, parseSubRouteHosts, resolveSubRouteHosts } from "./subRoute";
|
|
56
56
|
export type * from "./types";
|
|
57
57
|
export {
|
|
58
58
|
type WebsocketAuthAckData,
|
package/common/subRoute.ts
CHANGED
|
@@ -12,6 +12,67 @@ export const parseBasePaths = (value: string | string[] | Set<string> | undefine
|
|
|
12
12
|
return [...new Set(items.map((basePath) => basePath.trim()).filter(Boolean))];
|
|
13
13
|
};
|
|
14
14
|
|
|
15
|
+
const normalizeSubRouteHost = (host: string): string => host.trim().toLowerCase().replace(/:\d+$/, "");
|
|
16
|
+
|
|
17
|
+
/** A hostname never contains these; a fragment carrying one is a malformed entry, not a host we failed to match. */
|
|
18
|
+
const isSubRouteHost = (host: string): boolean => host.length > 0 && !/[\s/=]/.test(host);
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* `"soft=a.com,b.com;office=c.com"` -> `{ soft: ["a.com", "b.com"], office: ["c.com"] }`. A deployment platform
|
|
22
|
+
* renders this value, so a malformed entry is skipped rather than thrown: one bad character must not CrashLoop
|
|
23
|
+
* every pod that received it.
|
|
24
|
+
*/
|
|
25
|
+
export const parseSubRouteHosts = (value: string | undefined | null): Record<string, string[]> => {
|
|
26
|
+
const hostsByBasePath: Record<string, string[]> = {};
|
|
27
|
+
for (const group of (value ?? "").split(";")) {
|
|
28
|
+
const separatorIdx = group.indexOf("=");
|
|
29
|
+
if (separatorIdx < 0) continue;
|
|
30
|
+
const basePath = group
|
|
31
|
+
.slice(0, separatorIdx)
|
|
32
|
+
.trim()
|
|
33
|
+
.replace(/^\/+|\/+$/g, "");
|
|
34
|
+
if (!basePath) continue;
|
|
35
|
+
const hosts = group
|
|
36
|
+
.slice(separatorIdx + 1)
|
|
37
|
+
.split(",")
|
|
38
|
+
.map(normalizeSubRouteHost)
|
|
39
|
+
.filter(isSubRouteHost);
|
|
40
|
+
if (!hosts.length) continue;
|
|
41
|
+
hostsByBasePath[basePath] = [...new Set([...(hostsByBasePath[basePath] ?? []), ...hosts])];
|
|
42
|
+
}
|
|
43
|
+
return hostsByBasePath;
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Unions the env mapping onto the one baked into the build artifact, never replacing it — dropping the env is the
|
|
48
|
+
* rollback path. A basePath the build does not serve is reported back instead of honoured: the route tree is a
|
|
49
|
+
* build output, so accepting one would answer every request under it with a 404 and nothing to explain why.
|
|
50
|
+
*/
|
|
51
|
+
export const resolveSubRouteHosts = ({
|
|
52
|
+
subRoutes,
|
|
53
|
+
basePaths,
|
|
54
|
+
env,
|
|
55
|
+
}: {
|
|
56
|
+
subRoutes: Record<string, string[]>;
|
|
57
|
+
basePaths: Iterable<string>;
|
|
58
|
+
env?: string | null;
|
|
59
|
+
}): { subRoutes: Record<string, string[]>; ignoredBasePaths: string[] } => {
|
|
60
|
+
const parsed = Object.entries(parseSubRouteHosts(env));
|
|
61
|
+
if (!parsed.length) return { subRoutes, ignoredBasePaths: [] };
|
|
62
|
+
|
|
63
|
+
const configuredBasePaths = new Set(parseBasePaths([...basePaths]));
|
|
64
|
+
const merged: Record<string, string[]> = { ...subRoutes };
|
|
65
|
+
const ignoredBasePaths: string[] = [];
|
|
66
|
+
for (const [basePath, hosts] of parsed) {
|
|
67
|
+
if (!configuredBasePaths.has(basePath)) {
|
|
68
|
+
ignoredBasePaths.push(basePath);
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
merged[basePath] = [...new Set([...(merged[basePath] ?? []).map(normalizeSubRouteHost), ...hosts])];
|
|
72
|
+
}
|
|
73
|
+
return { subRoutes: merged, ignoredBasePaths };
|
|
74
|
+
};
|
|
75
|
+
|
|
15
76
|
export const getBasePathFromPathname = (
|
|
16
77
|
pathname: string,
|
|
17
78
|
{
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type { ConstantField, FieldObject } from "./fieldInfo";
|
|
2
|
+
import type { ConstantModelRef } from "./via";
|
|
3
|
+
|
|
4
|
+
/** What happens to the documents a relation field points at when the owner is removed. */
|
|
5
|
+
export const cascadeActions = ["remove"] as const;
|
|
6
|
+
export type CascadeAction = (typeof cascadeActions)[number];
|
|
7
|
+
|
|
8
|
+
export class CascadePaths {
|
|
9
|
+
/** Field key → the model its ids point at. Resolved to a refName later: the target may not be registered yet. */
|
|
10
|
+
readonly remove = new Map<string, ConstantModelRef>();
|
|
11
|
+
|
|
12
|
+
collect(fieldMap: FieldObject) {
|
|
13
|
+
for (const [key, field] of Object.entries(fieldMap)) {
|
|
14
|
+
if (!field.cascade) continue;
|
|
15
|
+
this.#assertCascadable(key, field.cascade, field);
|
|
16
|
+
this.remove.set(key, field.modelRef);
|
|
17
|
+
}
|
|
18
|
+
return this;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
#assertCascadable(key: string, action: CascadeAction, field: ConstantField) {
|
|
22
|
+
|
|
23
|
+
if (!cascadeActions.includes(action)) {
|
|
24
|
+
throw new Error(`Cascade field "${key}" declares cascade: "${action}", which is not one of ${cascadeActions}`);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
if (!field.isClass || field.isScalar) {
|
|
28
|
+
throw new Error(`Cascade field "${key}" is not a model reference and has no document to remove`);
|
|
29
|
+
}
|
|
30
|
+
if (field.arrDepth > 1) throw new Error(`Cascade field "${key}" is a nested array and cannot cascade`);
|
|
31
|
+
}
|
|
32
|
+
}
|
package/constant/fieldInfo.ts
CHANGED
|
@@ -16,6 +16,7 @@ import {
|
|
|
16
16
|
type SingleValue,
|
|
17
17
|
type UnCls,
|
|
18
18
|
} from "akanjs/base";
|
|
19
|
+
import type { CascadeAction } from "./cascadePaths";
|
|
19
20
|
import { ConstantRegistry } from "./constantRegistry";
|
|
20
21
|
import type { TextFieldRole } from "./textFieldPaths";
|
|
21
22
|
import type { BaseObject } from "./types";
|
|
@@ -104,6 +105,7 @@ export interface ConstantFieldProps<
|
|
|
104
105
|
of?: MapValue;
|
|
105
106
|
validate?: (value: FieldValue, model: any) => boolean;
|
|
106
107
|
text?: TextFieldRole;
|
|
108
|
+
cascade?: CascadeAction;
|
|
107
109
|
meta?: Metadata;
|
|
108
110
|
}
|
|
109
111
|
export const fieldPresets = ["email", "password", "url"] as const;
|
|
@@ -194,6 +196,7 @@ interface ConstantFieldBuildProps<
|
|
|
194
196
|
of?: MapValue;
|
|
195
197
|
validate?: (value: FieldValue, model: any) => boolean;
|
|
196
198
|
text?: TextFieldRole;
|
|
199
|
+
cascade?: CascadeAction;
|
|
197
200
|
modelRef: ConstantModelRef;
|
|
198
201
|
arrDepth: number;
|
|
199
202
|
optArrDepth: number;
|
|
@@ -317,6 +320,7 @@ export class ConstantField<
|
|
|
317
320
|
readonly of?: MapValue;
|
|
318
321
|
readonly validate?: (value: FieldValue, model: any) => boolean;
|
|
319
322
|
readonly text?: TextFieldRole;
|
|
323
|
+
readonly cascade?: CascadeAction;
|
|
320
324
|
readonly modelRef: ConstantModelRef;
|
|
321
325
|
readonly arrDepth: number;
|
|
322
326
|
readonly optArrDepth: number;
|
|
@@ -348,6 +352,7 @@ export class ConstantField<
|
|
|
348
352
|
this.of = props.of;
|
|
349
353
|
this.validate = props.validate;
|
|
350
354
|
this.text = props.text;
|
|
355
|
+
this.cascade = props.cascade;
|
|
351
356
|
this.modelRef = props.modelRef;
|
|
352
357
|
this.arrDepth = props.arrDepth;
|
|
353
358
|
this.optArrDepth = props.optArrDepth;
|
|
@@ -426,6 +431,7 @@ export class ConstantField<
|
|
|
426
431
|
of: option.of,
|
|
427
432
|
validate: option.validate,
|
|
428
433
|
text: option.text,
|
|
434
|
+
cascade: option.cascade,
|
|
429
435
|
modelRef,
|
|
430
436
|
arrDepth: arrDepth,
|
|
431
437
|
optArrDepth: optArrDepth,
|
|
@@ -465,6 +471,7 @@ export class ConstantField<
|
|
|
465
471
|
of: this.of,
|
|
466
472
|
validate: this.validate,
|
|
467
473
|
text: this.text,
|
|
474
|
+
cascade: this.cascade,
|
|
468
475
|
modelRef: this.modelRef,
|
|
469
476
|
arrDepth: this.arrDepth,
|
|
470
477
|
optArrDepth: this.optArrDepth,
|
package/constant/index.ts
CHANGED
package/constant/purify.ts
CHANGED
|
@@ -10,7 +10,6 @@ import {
|
|
|
10
10
|
ID,
|
|
11
11
|
PrimitiveRegistry,
|
|
12
12
|
type PrimitiveScalar,
|
|
13
|
-
type Upload,
|
|
14
13
|
} from "akanjs/base";
|
|
15
14
|
import { Logger } from "akanjs/common";
|
|
16
15
|
|
|
@@ -37,15 +36,15 @@ type PurifiedWithObjectToId<T, StateKeys extends keyof GetStateObject<T> = keyof
|
|
|
37
36
|
} & {
|
|
38
37
|
[K in StateKeys as null extends T[K] ? K : never]?: Purified<T[K]> | undefined;
|
|
39
38
|
};
|
|
40
|
-
export type PurifiedModel<T> = T extends
|
|
41
|
-
?
|
|
42
|
-
: T extends
|
|
43
|
-
?
|
|
44
|
-
: T extends
|
|
45
|
-
?
|
|
46
|
-
: T
|
|
47
|
-
|
|
48
|
-
|
|
39
|
+
export type PurifiedModel<T> = T extends (infer S)[]
|
|
40
|
+
? PurifiedModel<S>[]
|
|
41
|
+
: T extends string | number | boolean | Dayjs | File
|
|
42
|
+
? T
|
|
43
|
+
: T extends Map<infer K, infer V>
|
|
44
|
+
? Map<K, PurifiedModel<V>>
|
|
45
|
+
: PurifiedWithObjectToId<T>;
|
|
46
|
+
|
|
47
|
+
export type UploadableClientArg<T> = [T] extends [File[]] ? File[] | FileList : T;
|
|
49
48
|
|
|
50
49
|
export type PurifyFunc<Input, _DefaultInput = DefaultOf<Input>, _PurifiedInput = PurifiedModel<Input>> = (
|
|
51
50
|
self: _DefaultInput,
|
package/constant/via.ts
CHANGED
|
@@ -13,6 +13,7 @@ import { applyMixins } from "akanjs/common";
|
|
|
13
13
|
import { immerable } from "immer";
|
|
14
14
|
|
|
15
15
|
import { crystalize, getDefault } from ".";
|
|
16
|
+
import { CascadePaths } from "./cascadePaths";
|
|
16
17
|
import { ConstantRegistry } from "./constantRegistry";
|
|
17
18
|
import {
|
|
18
19
|
ConstantField,
|
|
@@ -193,6 +194,7 @@ const getBaseConstantClass = (field: FieldObject, modelType: ConstantType = "sca
|
|
|
193
194
|
static readonly [FIELD_META]: FieldObject = field;
|
|
194
195
|
static modelType: ConstantType = modelType;
|
|
195
196
|
static text: TextFieldPaths = new TextFieldPaths();
|
|
197
|
+
static cascade: CascadePaths = new CascadePaths();
|
|
196
198
|
static children: Set<ConstantModelRef> = new Set();
|
|
197
199
|
static relations: Set<ConstantModelRef> = new Set();
|
|
198
200
|
static enums: Set<EnumInstance> = new Set();
|
|
@@ -269,6 +271,7 @@ export interface ConstantStatics<
|
|
|
269
271
|
relations: Set<ConstantModelRef>;
|
|
270
272
|
enums: Set<EnumInstance>;
|
|
271
273
|
text: TextFieldPaths;
|
|
274
|
+
cascade: CascadePaths;
|
|
272
275
|
_OptionalKey: OptionalKey;
|
|
273
276
|
_RelationKey: RelationKey;
|
|
274
277
|
_PrimitiveKey: PrimitiveKey;
|
|
@@ -296,6 +299,7 @@ export interface DatabaseConstantStatics<Schema = any, FieldObj extends FieldObj
|
|
|
296
299
|
relations: Set<ConstantModelRef>;
|
|
297
300
|
enums: Set<EnumInstance>;
|
|
298
301
|
text: TextFieldPaths;
|
|
302
|
+
cascade: CascadePaths;
|
|
299
303
|
_DatabaseSchema: {
|
|
300
304
|
[K in keyof Schema]: K extends keyof FieldObj
|
|
301
305
|
? FieldObj[K]["fieldType"] extends "hidden"
|
|
@@ -318,6 +322,7 @@ export type ConstantModelRef<
|
|
|
318
322
|
relations: Set<ConstantModelRef>;
|
|
319
323
|
enums: Set<EnumInstance>;
|
|
320
324
|
text: TextFieldPaths;
|
|
325
|
+
cascade: CascadePaths;
|
|
321
326
|
}
|
|
322
327
|
>;
|
|
323
328
|
|
|
@@ -440,6 +445,7 @@ const applyConstantStatics = <Model>(model: ConstantCls<Model>, fieldMap: FieldO
|
|
|
440
445
|
for (const relation of field.modelRef.relations) model.relations.add(relation);
|
|
441
446
|
});
|
|
442
447
|
model.text.collect(fieldMap);
|
|
448
|
+
model.cascade.collect(fieldMap);
|
|
443
449
|
return model as unknown as ConstantCls<Model>;
|
|
444
450
|
};
|
|
445
451
|
|
|
@@ -491,7 +491,7 @@ export class FetchClient {
|
|
|
491
491
|
this.#setHandlerFactory(
|
|
492
492
|
names.addModelFiles,
|
|
493
493
|
() =>
|
|
494
|
-
(async (fileList: FileList, parentId?: string, option?: FetchPolicy) => {
|
|
494
|
+
(async (fileList: FileList | File[], parentId?: string, option?: FetchPolicy) => {
|
|
495
495
|
const cap = resolveFileUploadCapability(this.serializedSignal);
|
|
496
496
|
const endpoint = cap ? this.serializedSignal[cap.refName]?.endpoint[cap.endpointKey] : undefined;
|
|
497
497
|
if (!cap || !endpoint)
|
|
@@ -125,6 +125,12 @@ export class HttpClient {
|
|
|
125
125
|
});
|
|
126
126
|
return `${paramedPath}${searchPath}`;
|
|
127
127
|
}
|
|
128
|
+
|
|
129
|
+
static #toUploadValues(argValue: unknown): (Blob | string)[] {
|
|
130
|
+
if (Array.isArray(argValue)) return argValue as (Blob | string)[];
|
|
131
|
+
if (typeof FileList !== "undefined" && argValue instanceof FileList) return Array.from(argValue);
|
|
132
|
+
return [argValue as Blob | string];
|
|
133
|
+
}
|
|
128
134
|
static makeBody(bodyArgs: SerializedArg[], uploadArgs: SerializedArg[], argMap: Map<string, unknown>) {
|
|
129
135
|
if (uploadArgs.length > 0) {
|
|
130
136
|
const formData = new FormData();
|
|
@@ -133,11 +139,9 @@ export class HttpClient {
|
|
|
133
139
|
if (arg.nullable && (argValue === null || argValue === undefined)) return;
|
|
134
140
|
if (!arg.nullable && (argValue === null || argValue === undefined))
|
|
135
141
|
throw new Error(`Argument ${arg.name} is required`);
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
});
|
|
140
|
-
} else formData.append(arg.name, argValue as Blob | string);
|
|
142
|
+
HttpClient.#toUploadValues(argValue).forEach((value) => {
|
|
143
|
+
formData.append(arg.name, value);
|
|
144
|
+
});
|
|
141
145
|
});
|
|
142
146
|
bodyArgs.forEach((arg) => {
|
|
143
147
|
const argValue = argMap.get(arg.name);
|
|
@@ -152,7 +152,7 @@ type AppliedBaseSliceFetchType<
|
|
|
152
152
|
[K in `get${_CapitalizedRefName}Edit`]: (id: string, option?: FetchPolicy) => Promise<ServerEdit<RefName, Full>>;
|
|
153
153
|
} & {
|
|
154
154
|
[K in `add${_CapitalizedRefName}Files`]: (
|
|
155
|
-
fileList: FileList,
|
|
155
|
+
fileList: FileList | File[],
|
|
156
156
|
parentId?: string,
|
|
157
157
|
option?: FetchPolicy,
|
|
158
158
|
) => Promise<ProtoFile[]>;
|
package/package.json
CHANGED
package/server/di/diLifecycle.ts
CHANGED
|
@@ -457,7 +457,13 @@ export class DiLifecycle {
|
|
|
457
457
|
if (serviceCls.type === "database") {
|
|
458
458
|
const databaseModule = this.#database.get(serviceCls.refName);
|
|
459
459
|
if (!databaseModule) throw new Error(`Database "${serviceCls.refName}" is not registered`);
|
|
460
|
-
ServiceResolver.resolveDatabaseService(
|
|
460
|
+
ServiceResolver.resolveDatabaseService(
|
|
461
|
+
databaseModule.constant,
|
|
462
|
+
databaseModule.database,
|
|
463
|
+
serviceCls,
|
|
464
|
+
|
|
465
|
+
(refName) => this.getService(refName),
|
|
466
|
+
);
|
|
461
467
|
}
|
|
462
468
|
const service = new serviceCls();
|
|
463
469
|
await InjectInfo.resolveInjection(service, serviceCls, this.registry, this.#env);
|
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { getEnv } from "akanjs/base";
|
|
4
|
+
import { Logger, resolveSubRouteHosts } from "akanjs/common";
|
|
4
5
|
import type { BaseBuildArtifact } from "../types";
|
|
5
6
|
import { AkanResponse } from "./akanResponse";
|
|
6
7
|
import type { WebProxy } from "./types";
|
|
7
8
|
|
|
8
9
|
export class HostBasePathWebProxy implements WebProxy {
|
|
9
10
|
static readonly refName = "HostBasePathWebProxy";
|
|
11
|
+
#logger = new Logger("HostBasePathWebProxy");
|
|
10
12
|
#domainMap: Map<string, string> | null = null;
|
|
11
13
|
|
|
12
14
|
use(request: Bun.BunRequest) {
|
|
@@ -49,7 +51,16 @@ export class HostBasePathWebProxy implements WebProxy {
|
|
|
49
51
|
|
|
50
52
|
#getDomainMap(): Map<string, string> {
|
|
51
53
|
if (this.#domainMap) return this.#domainMap;
|
|
52
|
-
const
|
|
54
|
+
const metadata = loadWebRouteMetadata();
|
|
55
|
+
const { subRoutes, ignoredBasePaths } = resolveSubRouteHosts({
|
|
56
|
+
subRoutes: metadata.subRoutes,
|
|
57
|
+
basePaths: metadata.basePaths,
|
|
58
|
+
env: process.env.AKAN_SUB_ROUTE_HOSTS,
|
|
59
|
+
});
|
|
60
|
+
if (ignoredBasePaths.length)
|
|
61
|
+
this.#logger.warn(
|
|
62
|
+
`AKAN_SUB_ROUTE_HOSTS names basePaths this build does not serve, ignoring: ${ignoredBasePaths.join(", ")}`,
|
|
63
|
+
);
|
|
53
64
|
const map = new Map<string, string>();
|
|
54
65
|
for (const [basePath, domains] of Object.entries(subRoutes)) {
|
|
55
66
|
for (const domain of domains) map.set(normalizeHost(domain), basePath);
|
|
@@ -59,14 +70,14 @@ export class HostBasePathWebProxy implements WebProxy {
|
|
|
59
70
|
}
|
|
60
71
|
}
|
|
61
72
|
|
|
62
|
-
function loadWebRouteMetadata() {
|
|
73
|
+
function loadWebRouteMetadata(): Pick<BaseBuildArtifact, "subRoutes" | "basePaths"> {
|
|
63
74
|
const artifactPath = path.join(resolveArtifactDir(), "base-artifact.json");
|
|
64
|
-
if (!fs.existsSync(artifactPath)) return {
|
|
75
|
+
if (!fs.existsSync(artifactPath)) return { subRoutes: {}, basePaths: [] };
|
|
65
76
|
try {
|
|
66
77
|
const parsed = JSON.parse(fs.readFileSync(artifactPath, "utf8")) as Partial<BaseBuildArtifact>;
|
|
67
|
-
return parsed.subRoutes ?? {};
|
|
78
|
+
return { subRoutes: parsed.subRoutes ?? {}, basePaths: parsed.basePaths ?? [] };
|
|
68
79
|
} catch {
|
|
69
|
-
return {
|
|
80
|
+
return { subRoutes: {}, basePaths: [] };
|
|
70
81
|
}
|
|
71
82
|
}
|
|
72
83
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { PromiseOrObject } from "akanjs/base";
|
|
2
2
|
import { capitalize } from "akanjs/common";
|
|
3
|
-
import type
|
|
3
|
+
import { type ConstantModel, ConstantRegistry, type QueryOf } from "akanjs/constant";
|
|
4
4
|
import {
|
|
5
5
|
type CRUDEventType,
|
|
6
6
|
type DatabaseModel,
|
|
@@ -17,7 +17,11 @@ import {
|
|
|
17
17
|
import type { DatabaseService, ServiceCls } from "akanjs/service";
|
|
18
18
|
|
|
19
19
|
export class ServiceResolver {
|
|
20
|
-
static #getDefaultDbServiceMethods(
|
|
20
|
+
static #getDefaultDbServiceMethods(
|
|
21
|
+
className: string,
|
|
22
|
+
cascades: [string, string][],
|
|
23
|
+
getService: (refName: string) => DatabaseService,
|
|
24
|
+
) {
|
|
21
25
|
const dbServiceMethods = {
|
|
22
26
|
async __get(this: DatabaseService, id: string) {
|
|
23
27
|
return await this.__databaseModel.__get(id);
|
|
@@ -95,9 +99,18 @@ export class ServiceResolver {
|
|
|
95
99
|
return this.__update(id, data);
|
|
96
100
|
},
|
|
97
101
|
async __remove(this: DatabaseService, id: string): Promise<Doc> {
|
|
102
|
+
|
|
103
|
+
const targets = cascades.map(([key, refName]) => [key, getService(refName)] as const);
|
|
98
104
|
await this.__libsPreRemove(id);
|
|
99
105
|
const doc = await this.__databaseModel.__remove(id);
|
|
100
|
-
|
|
106
|
+
const removed = await this.__libsPostRemove(doc);
|
|
107
|
+
for (const [key, target] of targets) {
|
|
108
|
+
const value = (removed as Record<string, unknown>)[key];
|
|
109
|
+
const ids = (Array.isArray(value) ? value : [value]).filter((v): v is string => typeof v === "string");
|
|
110
|
+
|
|
111
|
+
for (const targetId of ids) await target.__remove(targetId);
|
|
112
|
+
}
|
|
113
|
+
return removed;
|
|
101
114
|
},
|
|
102
115
|
async [`remove${className}`](this: DatabaseService, id: string): Promise<Doc> {
|
|
103
116
|
return this.__remove(id);
|
|
@@ -105,9 +118,18 @@ export class ServiceResolver {
|
|
|
105
118
|
};
|
|
106
119
|
return dbServiceMethods;
|
|
107
120
|
}
|
|
108
|
-
static resolveDatabaseService(
|
|
121
|
+
static resolveDatabaseService(
|
|
122
|
+
constant: ConstantModel,
|
|
123
|
+
database: DatabaseModel,
|
|
124
|
+
srvRef: ServiceCls,
|
|
125
|
+
getService: (refName: string) => DatabaseService,
|
|
126
|
+
): ServiceCls {
|
|
109
127
|
const className = capitalize(database.refName);
|
|
110
|
-
|
|
128
|
+
|
|
129
|
+
const cascades = [...constant.full.cascade.remove].map(
|
|
130
|
+
([key, modelRef]) => [key, ConstantRegistry.getRefName(modelRef)] as [string, string],
|
|
131
|
+
);
|
|
132
|
+
Object.assign(srvRef.prototype, ServiceResolver.#getDefaultDbServiceMethods(className, cascades, getService));
|
|
111
133
|
const getQueryDataFromKey = (queryKey: string, args: any): { query: any; queryOption: any } => {
|
|
112
134
|
const lastArg = args.at(-1);
|
|
113
135
|
const hasQueryOption =
|
package/server/webRouter.ts
CHANGED
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
getBasePathFromPathname,
|
|
8
8
|
Logger,
|
|
9
9
|
parseAkanI18nEnv,
|
|
10
|
+
resolveSubRouteHosts,
|
|
10
11
|
} from "akanjs/common";
|
|
11
12
|
import { type AkanRequestStore, createRequestStore, parseCookieHeader } from "akanjs/fetch";
|
|
12
13
|
import type { AkanMetricsReport } from "akanjs/service";
|
|
@@ -267,6 +268,7 @@ export class WebRouter {
|
|
|
267
268
|
#logger = new Logger("WebRouter");
|
|
268
269
|
#artifactDir = WebRouter.#resolveArtifactDir();
|
|
269
270
|
#artifact: BaseBuildArtifact;
|
|
271
|
+
#subRoutes: Record<string, string[]>;
|
|
270
272
|
#rsc: RscWorker;
|
|
271
273
|
#hub: HmrWsHub | null = null;
|
|
272
274
|
#prodMode = process.env.NODE_ENV === "production";
|
|
@@ -293,6 +295,16 @@ export class WebRouter {
|
|
|
293
295
|
constructor({ artifact, cssBytesByUrl, rsc, seedIndex, upgradeHmrWs }: WebRouterOptions) {
|
|
294
296
|
this.#logger.verbose(`[SSR] loaded ${Object.keys(cssBytesByUrl).length} CSS assets`);
|
|
295
297
|
this.#artifact = artifact;
|
|
298
|
+
const { subRoutes, ignoredBasePaths } = resolveSubRouteHosts({
|
|
299
|
+
subRoutes: artifact.subRoutes,
|
|
300
|
+
basePaths: artifact.basePaths,
|
|
301
|
+
env: process.env.AKAN_SUB_ROUTE_HOSTS,
|
|
302
|
+
});
|
|
303
|
+
this.#subRoutes = subRoutes;
|
|
304
|
+
if (ignoredBasePaths.length)
|
|
305
|
+
this.#logger.warn(
|
|
306
|
+
`AKAN_SUB_ROUTE_HOSTS names basePaths this build does not serve, ignoring: ${ignoredBasePaths.join(", ")}`,
|
|
307
|
+
);
|
|
296
308
|
this.#rsc = rsc;
|
|
297
309
|
this.renderState = {
|
|
298
310
|
buildId: 0,
|
|
@@ -424,8 +436,7 @@ export class WebRouter {
|
|
|
424
436
|
const clientOrigin = WebRouter.#clientFacingOrigin(req);
|
|
425
437
|
const target = reqUrl.searchParams.get("url");
|
|
426
438
|
const rawTargetUrl = target ? new URL(target, clientOrigin) : reqUrl;
|
|
427
|
-
const requestBasePath =
|
|
428
|
-
req.headers.get("x-base-path") ?? WebRouter.#basePathForRequestHost(req, this.#artifact.subRoutes);
|
|
439
|
+
const requestBasePath = this.#requestBasePath(req);
|
|
429
440
|
const normalizedTarget = normalizeRscTargetUrlForHostBasePath(rawTargetUrl, {
|
|
430
441
|
basePath: requestBasePath,
|
|
431
442
|
basePaths: this.#artifact.basePaths,
|
|
@@ -517,11 +528,7 @@ export class WebRouter {
|
|
|
517
528
|
});
|
|
518
529
|
}
|
|
519
530
|
|
|
520
|
-
const sitemapBasePath = getSitemapBasePath(
|
|
521
|
-
url.pathname,
|
|
522
|
-
this.#artifact.basePaths,
|
|
523
|
-
req.headers.get("x-base-path") ?? WebRouter.#basePathForRequestHost(req, this.#artifact.subRoutes),
|
|
524
|
-
);
|
|
531
|
+
const sitemapBasePath = getSitemapBasePath(url.pathname, this.#artifact.basePaths, this.#requestBasePath(req));
|
|
525
532
|
if (sitemapBasePath !== undefined) {
|
|
526
533
|
return new Response(
|
|
527
534
|
createDefaultSitemapXml({
|
|
@@ -713,6 +720,17 @@ export class WebRouter {
|
|
|
713
720
|
return getClientFacingOrigin(req);
|
|
714
721
|
}
|
|
715
722
|
|
|
723
|
+
/**
|
|
724
|
+
* `x-base-path` is set by `HostBasePathWebProxy`, but it reaches here from the wire too, so it is checked against
|
|
725
|
+
* the basePaths this build serves — the same check `getBasePathFromPathname` already applies to it. An unknown
|
|
726
|
+
* value falls through to host matching instead of routing the request into a basePath that resolves to nothing.
|
|
727
|
+
*/
|
|
728
|
+
#requestBasePath(req: Request): string | null {
|
|
729
|
+
const headerBasePath = req.headers.get("x-base-path");
|
|
730
|
+
if (headerBasePath && this.#artifact.basePaths.includes(headerBasePath)) return headerBasePath;
|
|
731
|
+
return WebRouter.#basePathForRequestHost(req, this.#subRoutes);
|
|
732
|
+
}
|
|
733
|
+
|
|
716
734
|
static #basePathForRequestHost(req: Request, subRoutes: Record<string, string[]>): string | null {
|
|
717
735
|
const host = (req.headers.get("x-forwarded-host") ?? req.headers.get("host") ?? "")
|
|
718
736
|
.toLowerCase()
|
|
@@ -857,8 +875,7 @@ export class WebRouter {
|
|
|
857
875
|
pathname,
|
|
858
876
|
i18n: this.#artifact.i18n,
|
|
859
877
|
basePaths: this.#artifact.basePaths,
|
|
860
|
-
headerBasePath:
|
|
861
|
-
req.headers.get("x-base-path") ?? WebRouter.#basePathForRequestHost(req, this.#artifact.subRoutes),
|
|
878
|
+
headerBasePath: this.#requestBasePath(req),
|
|
862
879
|
});
|
|
863
880
|
}
|
|
864
881
|
|
|
@@ -866,8 +883,7 @@ export class WebRouter {
|
|
|
866
883
|
const basePath = getBasePathFromPathname(pathname, {
|
|
867
884
|
basePaths: Object.keys(this.renderState.cssAssets),
|
|
868
885
|
i18n: this.#artifact.i18n,
|
|
869
|
-
headerBasePath:
|
|
870
|
-
req.headers.get("x-base-path") ?? WebRouter.#basePathForRequestHost(req, this.#artifact.subRoutes),
|
|
886
|
+
headerBasePath: this.#requestBasePath(req),
|
|
871
887
|
});
|
|
872
888
|
return this.renderState.cssAssets[basePath ?? ""]?.cssUrl ?? null;
|
|
873
889
|
}
|
package/signal/endpointInfo.ts
CHANGED
|
@@ -15,6 +15,7 @@ import type {
|
|
|
15
15
|
ParamFieldType,
|
|
16
16
|
PlainTypeToFieldType,
|
|
17
17
|
PurifiedModel,
|
|
18
|
+
UploadableClientArg,
|
|
18
19
|
} from "akanjs/constant";
|
|
19
20
|
import type { ServiceModel } from "akanjs/service";
|
|
20
21
|
import type { InternalArgCls } from "./internalArg";
|
|
@@ -135,7 +136,7 @@ export class EndpointInfo<
|
|
|
135
136
|
Arg extends ConstantFieldTypeInput = PlainTypeToFieldType<ExplicitType>,
|
|
136
137
|
Optional extends boolean = false,
|
|
137
138
|
_ArgType = unknown extends ExplicitType ? FieldToValue<Arg> : ExplicitType,
|
|
138
|
-
_ClientArg = PurifiedModel<_ArgType
|
|
139
|
+
_ClientArg = UploadableClientArg<PurifiedModel<_ArgType>>,
|
|
139
140
|
_ServerArg = DocumentModel<_ArgType>,
|
|
140
141
|
>(name: ArgName, arg: Arg, option?: EndpointArgProps<Optional>) {
|
|
141
142
|
if (this.execFn) throw new Error("Query function is already set");
|
package/signal/slice.ts
CHANGED
package/signal/sliceInfo.ts
CHANGED
|
@@ -9,6 +9,7 @@ import type {
|
|
|
9
9
|
PlainTypeToFieldType,
|
|
10
10
|
PurifiedModel,
|
|
11
11
|
QueryOf,
|
|
12
|
+
UploadableClientArg,
|
|
12
13
|
} from "akanjs/constant";
|
|
13
14
|
import type { FilterCls, FilterInstance } from "akanjs/document";
|
|
14
15
|
import type { ServiceModel } from "akanjs/service";
|
|
@@ -94,7 +95,7 @@ export class SliceInfo<
|
|
|
94
95
|
Arg extends ConstantFieldTypeInput = PlainTypeToFieldType<ExplicitType>,
|
|
95
96
|
Optional extends boolean = false,
|
|
96
97
|
_ArgType = unknown extends ExplicitType ? FieldToValue<Arg> : ExplicitType,
|
|
97
|
-
_ClientArg = PurifiedModel<_ArgType
|
|
98
|
+
_ClientArg = UploadableClientArg<PurifiedModel<_ArgType>>,
|
|
98
99
|
_ServerArg = DocumentModel<_ArgType>,
|
|
99
100
|
>(name: ArgName, arg: Arg, option?: EndpointArgProps<Optional>) {
|
|
100
101
|
if (this.execFn) throw new Error("Query function is already set");
|
package/store/action.ts
CHANGED
|
@@ -224,7 +224,7 @@ type FormSetter<
|
|
|
224
224
|
? K extends string
|
|
225
225
|
? SetterKey<"upload", K, RefName, _CapitalizedRefName>
|
|
226
226
|
: never
|
|
227
|
-
: never]: (fileList: FileList, idx?: number) => Promise<void>;
|
|
227
|
+
: never]: (fileList: FileList | File[], idx?: number) => Promise<void>;
|
|
228
228
|
} & {
|
|
229
229
|
[K in `writeOn${_CapitalizedRefName}`]: (path: string | (string | number)[], value: any) => void;
|
|
230
230
|
};
|
|
@@ -376,7 +376,11 @@ export const makeFormSetter = (refName: string, fetch: FetchProxy<any>) => {
|
|
|
376
376
|
: {}),
|
|
377
377
|
...(field.isClass && !!fileUploadRefName && ConstantRegistry.getRefName(field.modelRef) === fileUploadRefName
|
|
378
378
|
? {
|
|
379
|
-
[namesOfField.uploadFieldOnModel]: async function (
|
|
379
|
+
[namesOfField.uploadFieldOnModel]: async function (
|
|
380
|
+
this: SetGet,
|
|
381
|
+
fileList: FileList | File[],
|
|
382
|
+
index?: number,
|
|
383
|
+
) {
|
|
380
384
|
const form = (this.get() as { [key: string]: any })[names.modelForm] as { [key: string]: any };
|
|
381
385
|
if (!fileList.length) return;
|
|
382
386
|
const files = await (fetch[names.addModelFiles] as (...args: any) => Promise<ProtoFile[]>)(
|
|
@@ -476,6 +480,7 @@ export const makeActions = (refName: string, slice: { [key: string]: SerializedS
|
|
|
476
480
|
initModel: `init${className}`,
|
|
477
481
|
modelInitList: `${fieldName}InitList`,
|
|
478
482
|
modelInitAt: `${fieldName}InitAt`,
|
|
483
|
+
modelStaleAt: `${fieldName}StaleAt`,
|
|
479
484
|
refreshModel: `refresh${className}`,
|
|
480
485
|
selectModel: `select${className}`,
|
|
481
486
|
setPageOfModel: `setPageOf${className}`,
|
|
@@ -489,6 +494,14 @@ export const makeActions = (refName: string, slice: { [key: string]: SerializedS
|
|
|
489
494
|
queryArgsOfModel: `queryArgsOf${className}`,
|
|
490
495
|
sortOfModel: `sortOf${className}`,
|
|
491
496
|
};
|
|
497
|
+
const staleAtOfOtherSlices = (createdSliceName: string) => {
|
|
498
|
+
const staleAt = new Date();
|
|
499
|
+
return Object.fromEntries(
|
|
500
|
+
slices
|
|
501
|
+
.filter(({ sliceName }) => sliceName !== createdSliceName)
|
|
502
|
+
.map(({ sliceName }) => [sliceName.replace(names.model, names.modelStaleAt), staleAt]),
|
|
503
|
+
);
|
|
504
|
+
};
|
|
492
505
|
const baseAction = {
|
|
493
506
|
[names.createModelInForm]: async function (
|
|
494
507
|
this: SetGet,
|
|
@@ -527,6 +540,7 @@ export const makeActions = (refName: string, slice: { [key: string]: SerializedS
|
|
|
527
540
|
[namesOfSlice.modelInsight]: newModelInsight,
|
|
528
541
|
[names.modelViewAt]: new Date(),
|
|
529
542
|
[names.modelModal]: modal ?? null,
|
|
543
|
+
...staleAtOfOtherSlices(sliceName),
|
|
530
544
|
...(typeof path === "string" && path ? { [path]: model } : {}),
|
|
531
545
|
});
|
|
532
546
|
await onSuccess?.(model);
|
|
@@ -609,6 +623,7 @@ export const makeActions = (refName: string, slice: { [key: string]: SerializedS
|
|
|
609
623
|
[namesOfSlice.modelInsight]: newModelInsight,
|
|
610
624
|
[names.modelViewAt]: new Date(),
|
|
611
625
|
[names.modelModal]: modal ?? null,
|
|
626
|
+
...staleAtOfOtherSlices(sliceName),
|
|
612
627
|
...(typeof path === "string" && path ? { [path]: model } : {}),
|
|
613
628
|
});
|
|
614
629
|
await onSuccess?.(model);
|
|
@@ -845,6 +860,7 @@ export const makeActions = (refName: string, slice: { [key: string]: SerializedS
|
|
|
845
860
|
initModel: SliceName.replace(names.Model, names.initModel),
|
|
846
861
|
modelInitList: SliceName.replace(names.Model, names.modelInitList),
|
|
847
862
|
modelInitAt: SliceName.replace(names.Model, names.modelInitAt),
|
|
863
|
+
modelStaleAt: SliceName.replace(names.Model, names.modelStaleAt),
|
|
848
864
|
refreshModel: SliceName.replace(names.Model, names.refreshModel),
|
|
849
865
|
selectModel: SliceName.replace(names.Model, names.selectModel),
|
|
850
866
|
setPageOfModel: SliceName.replace(names.Model, names.setPageOfModel),
|
package/store/state.ts
CHANGED
|
@@ -23,6 +23,7 @@ export type SliceStateKey =
|
|
|
23
23
|
| "modelListLoading"
|
|
24
24
|
| "modelInitList"
|
|
25
25
|
| "modelInitAt"
|
|
26
|
+
| "modelStaleAt"
|
|
26
27
|
| "modelSelection"
|
|
27
28
|
| "lastPageOfModel"
|
|
28
29
|
| "pageOfModel"
|
|
@@ -78,6 +79,8 @@ export type SliceState<
|
|
|
78
79
|
[K in `${RefName}InitList${_CapitalizedSuffix}`]: DataList<Light>;
|
|
79
80
|
} & {
|
|
80
81
|
[K in `${RefName}InitAt${_CapitalizedSuffix}`]: Date;
|
|
82
|
+
} & {
|
|
83
|
+
[K in `${RefName}StaleAt${_CapitalizedSuffix}`]: Date;
|
|
81
84
|
} & {
|
|
82
85
|
[K in `${RefName}Selection${_CapitalizedSuffix}`]: DataList<Light>;
|
|
83
86
|
} & {
|
|
@@ -112,7 +115,9 @@ type DefaultSliceStateFields<
|
|
|
112
115
|
| `${_RefName}InitList${StoreSliceSuffixCap<SlceCls, Suffix>}`
|
|
113
116
|
| `${_RefName}Selection${StoreSliceSuffixCap<SlceCls, Suffix>}`]: DataList<_Light>;
|
|
114
117
|
} & {
|
|
115
|
-
[Suffix in _Suffixes as
|
|
118
|
+
[Suffix in _Suffixes as
|
|
119
|
+
| `${_RefName}InitAt${StoreSliceSuffixCap<SlceCls, Suffix>}`
|
|
120
|
+
| `${_RefName}StaleAt${StoreSliceSuffixCap<SlceCls, Suffix>}`]: Date;
|
|
116
121
|
} & {
|
|
117
122
|
[Suffix in _Suffixes as `${_RefName}ListLoading${StoreSliceSuffixCap<SlceCls, Suffix>}`]: boolean;
|
|
118
123
|
} & {
|
|
@@ -182,6 +187,7 @@ export const createSliceState = (refName: string, slice: { [key: string]: Serial
|
|
|
182
187
|
modelListLoading: `${fieldName}ListLoading`,
|
|
183
188
|
modelInitList: `${fieldName}InitList`,
|
|
184
189
|
modelInitAt: `${fieldName}InitAt`,
|
|
190
|
+
modelStaleAt: `${fieldName}StaleAt`,
|
|
185
191
|
modelSelection: `${fieldName}Selection`,
|
|
186
192
|
modelInsight: `${fieldName}Insight`,
|
|
187
193
|
lastPageOfModel: `lastPageOf${className}`,
|
|
@@ -200,6 +206,7 @@ export const createSliceState = (refName: string, slice: { [key: string]: Serial
|
|
|
200
206
|
modelListLoading: sliceName.replace(names.model, names.modelListLoading),
|
|
201
207
|
modelInitList: sliceName.replace(names.model, names.modelInitList),
|
|
202
208
|
modelInitAt: sliceName.replace(names.model, names.modelInitAt),
|
|
209
|
+
modelStaleAt: sliceName.replace(names.model, names.modelStaleAt),
|
|
203
210
|
modelSelection: sliceName.replace(names.model, names.modelSelection),
|
|
204
211
|
modelInsight: sliceName.replace(names.model, names.modelInsight),
|
|
205
212
|
lastPageOfModel: SliceName.replace(names.Model, names.lastPageOfModel),
|
|
@@ -214,6 +221,7 @@ export const createSliceState = (refName: string, slice: { [key: string]: Serial
|
|
|
214
221
|
[namesOfSlice.modelListLoading]: true,
|
|
215
222
|
[namesOfSlice.modelInitList]: new DataList(),
|
|
216
223
|
[namesOfSlice.modelInitAt]: new Date(0),
|
|
224
|
+
[namesOfSlice.modelStaleAt]: new Date(0),
|
|
217
225
|
[namesOfSlice.modelSelection]: new DataList(),
|
|
218
226
|
[namesOfSlice.modelInsight]: new cnst.insight(),
|
|
219
227
|
[namesOfSlice.lastPageOfModel]: 1,
|
package/store/storeInstance.ts
CHANGED
|
@@ -263,6 +263,7 @@ export class StoreInstance {
|
|
|
263
263
|
modelListLoading: `${fieldName}ListLoading`,
|
|
264
264
|
modelInitList: `${fieldName}InitList`,
|
|
265
265
|
modelInitAt: `${fieldName}InitAt`,
|
|
266
|
+
modelStaleAt: `${fieldName}StaleAt`,
|
|
266
267
|
pageOfModel: `pageOf${className}`,
|
|
267
268
|
limitOfModel: `limitOf${className}`,
|
|
268
269
|
queryArgsOfModel: `queryArgsOf${className}`,
|
|
@@ -286,6 +287,7 @@ export class StoreInstance {
|
|
|
286
287
|
modelList: sliceName.replace(names.model, names.modelList),
|
|
287
288
|
modelListLoading: sliceName.replace(names.model, names.modelListLoading),
|
|
288
289
|
modelInitAt: SliceName.replace(names.Model, names.modelInitAt),
|
|
290
|
+
modelStaleAt: SliceName.replace(names.Model, names.modelStaleAt),
|
|
289
291
|
lastPageOfModel: SliceName.replace(names.Model, names.lastPageOfModel),
|
|
290
292
|
pageOfModel: SliceName.replace(names.Model, names.pageOfModel),
|
|
291
293
|
limitOfModel: SliceName.replace(names.Model, names.limitOfModel),
|
package/test/sampleOf.ts
CHANGED
|
@@ -10,7 +10,7 @@ import {
|
|
|
10
10
|
Upload,
|
|
11
11
|
} from "akanjs/base";
|
|
12
12
|
import { randomPick } from "akanjs/common";
|
|
13
|
-
import type { BaseObject, ConstantCls, ConstantField, DocumentModel, FieldPreset } from "akanjs/constant";
|
|
13
|
+
import type { BaseObject, ConstantCls, ConstantField, DocumentModel, FieldObject, FieldPreset } from "akanjs/constant";
|
|
14
14
|
|
|
15
15
|
import { sample } from "./sample";
|
|
16
16
|
|
|
@@ -57,8 +57,8 @@ const makeSample = (field: ConstantField): any => {
|
|
|
57
57
|
export type SampleOf<Model> = DocumentModel<{
|
|
58
58
|
[K in keyof Model as Model[K] extends BaseObject ? never : K]: NonNullable<Model[K]>;
|
|
59
59
|
}>;
|
|
60
|
-
export const sampleOf = <Model>(
|
|
61
|
-
modelRef: ConstantCls<Model>,
|
|
60
|
+
export const sampleOf = <Model, FieldObj extends FieldObject>(
|
|
61
|
+
modelRef: ConstantCls<Model, FieldObj>,
|
|
62
62
|
): DocumentModel<{ [K in keyof Model as Model[K] extends BaseObject ? never : K]: NonNullable<Model[K]> }> => {
|
|
63
63
|
|
|
64
64
|
return Object.fromEntries(
|
|
@@ -13,7 +13,7 @@ export declare const fileUploadContract: {
|
|
|
13
13
|
readonly type: "type";
|
|
14
14
|
readonly parentId: "parentId";
|
|
15
15
|
};
|
|
16
|
-
readonly buildMetas: (fileList: FileList) => {
|
|
16
|
+
readonly buildMetas: (fileList: FileList | File[]) => {
|
|
17
17
|
lastModifiedAt: string;
|
|
18
18
|
size: number;
|
|
19
19
|
}[];
|
package/types/common/index.d.ts
CHANGED
|
@@ -25,6 +25,6 @@ export { randomPicks } from "./randomPicks.d.ts";
|
|
|
25
25
|
export { assertUniqueRoutePatterns, compareRouteSpecificity, isRouteSourceFile, isSpecialRouteLeaf, matchRoutePattern, normalizeRoutePattern, type ParsedRouteModuleKey, parseRouteModuleKey, type RouteModuleKind, routeSegmentToPatternPart, routeSegmentToTreePath, tryParseRouteModuleKey, type ValidatePageSourceFileOptions, type ValidateSubRoutePageKeyOptions, validatePageSourceFile, validateSubRoutePageKey, } from "./routeConvention.d.ts";
|
|
26
26
|
export { sleep } from "./sleep.d.ts";
|
|
27
27
|
export { splitVersion } from "./splitVersion.d.ts";
|
|
28
|
-
export { getBasePathFromPathname, parseBasePaths } from "./subRoute.d.ts";
|
|
28
|
+
export { getBasePathFromPathname, parseBasePaths, parseSubRouteHosts, resolveSubRouteHosts } from "./subRoute.d.ts";
|
|
29
29
|
export type * from "./types.d.ts";
|
|
30
30
|
export { type WebsocketAuthAckData, type WebsocketAuthRequest, websocketAuthContract, } from "./websocketAuth.d.ts";
|
|
@@ -1,5 +1,24 @@
|
|
|
1
1
|
import type { AkanI18nConfig } from "./localeConfig.d.ts";
|
|
2
2
|
export declare const parseBasePaths: (value: string | string[] | Set<string> | undefined | null) => string[];
|
|
3
|
+
/**
|
|
4
|
+
* `"soft=a.com,b.com;office=c.com"` -> `{ soft: ["a.com", "b.com"], office: ["c.com"] }`. A deployment platform
|
|
5
|
+
* renders this value, so a malformed entry is skipped rather than thrown: one bad character must not CrashLoop
|
|
6
|
+
* every pod that received it.
|
|
7
|
+
*/
|
|
8
|
+
export declare const parseSubRouteHosts: (value: string | undefined | null) => Record<string, string[]>;
|
|
9
|
+
/**
|
|
10
|
+
* Unions the env mapping onto the one baked into the build artifact, never replacing it — dropping the env is the
|
|
11
|
+
* rollback path. A basePath the build does not serve is reported back instead of honoured: the route tree is a
|
|
12
|
+
* build output, so accepting one would answer every request under it with a 404 and nothing to explain why.
|
|
13
|
+
*/
|
|
14
|
+
export declare const resolveSubRouteHosts: ({ subRoutes, basePaths, env, }: {
|
|
15
|
+
subRoutes: Record<string, string[]>;
|
|
16
|
+
basePaths: Iterable<string>;
|
|
17
|
+
env?: string | null;
|
|
18
|
+
}) => {
|
|
19
|
+
subRoutes: Record<string, string[]>;
|
|
20
|
+
ignoredBasePaths: string[];
|
|
21
|
+
};
|
|
3
22
|
export declare const getBasePathFromPathname: (pathname: string, { basePaths, i18n, headerBasePath, }: {
|
|
4
23
|
basePaths: Iterable<string>;
|
|
5
24
|
i18n?: Pick<AkanI18nConfig, "locales" | "defaultLocale">;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { FieldObject } from "./fieldInfo.d.ts";
|
|
2
|
+
import type { ConstantModelRef } from "./via.d.ts";
|
|
3
|
+
/** What happens to the documents a relation field points at when the owner is removed. */
|
|
4
|
+
export declare const cascadeActions: readonly ["remove"];
|
|
5
|
+
export type CascadeAction = (typeof cascadeActions)[number];
|
|
6
|
+
export declare class CascadePaths {
|
|
7
|
+
#private;
|
|
8
|
+
/** Field key → the model its ids point at. Resolved to a refName later: the target may not be registered yet. */
|
|
9
|
+
readonly remove: Map<string, ConstantModelRef>;
|
|
10
|
+
collect(fieldMap: FieldObject): this;
|
|
11
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { type Any, CLIENT_VALUE, type Cls, type Dayjs, type EnumInstance, type Float, Int, type PrimitiveScalar, SERVER_VALUE, type SingleValue, type UnCls } from "akanjs/base";
|
|
2
|
+
import type { CascadeAction } from "./cascadePaths.d.ts";
|
|
2
3
|
import type { TextFieldRole } from "./textFieldPaths.d.ts";
|
|
3
4
|
import type { BaseObject } from "./types.d.ts";
|
|
4
5
|
import type { ConstantModelRef } from "./via.d.ts";
|
|
@@ -45,6 +46,7 @@ export interface ConstantFieldProps<FieldType extends ConstantFieldKind = Consta
|
|
|
45
46
|
of?: MapValue;
|
|
46
47
|
validate?: (value: FieldValue, model: any) => boolean;
|
|
47
48
|
text?: TextFieldRole;
|
|
49
|
+
cascade?: CascadeAction;
|
|
48
50
|
meta?: Metadata;
|
|
49
51
|
}
|
|
50
52
|
export declare const fieldPresets: readonly ["email", "password", "url"];
|
|
@@ -91,6 +93,7 @@ interface ConstantFieldBuildProps<FieldType extends ConstantFieldKind = any, Fie
|
|
|
91
93
|
of?: MapValue;
|
|
92
94
|
validate?: (value: FieldValue, model: any) => boolean;
|
|
93
95
|
text?: TextFieldRole;
|
|
96
|
+
cascade?: CascadeAction;
|
|
94
97
|
modelRef: ConstantModelRef;
|
|
95
98
|
arrDepth: number;
|
|
96
99
|
optArrDepth: number;
|
|
@@ -134,6 +137,7 @@ export declare class ConstantField<FieldType extends ConstantFieldKind = Constan
|
|
|
134
137
|
readonly of?: MapValue;
|
|
135
138
|
readonly validate?: (value: FieldValue, model: any) => boolean;
|
|
136
139
|
readonly text?: TextFieldRole;
|
|
140
|
+
readonly cascade?: CascadeAction;
|
|
137
141
|
readonly modelRef: ConstantModelRef;
|
|
138
142
|
readonly arrDepth: number;
|
|
139
143
|
readonly optArrDepth: number;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type Dayjs, type GetStateObject
|
|
1
|
+
import { type Dayjs, type GetStateObject } from "akanjs/base";
|
|
2
2
|
import { type BaseObject, type ConstantModelRef, type DefaultOf, type DefaultOfSchema } from ".";
|
|
3
3
|
type Purified<O> = O extends BaseObject ? string : O extends BaseObject[] ? string[] : O extends Dayjs ? Dayjs : O extends object ? PurifiedModel<O> : O;
|
|
4
4
|
type PurifiedWithObjectToId<T, StateKeys extends keyof GetStateObject<T> = keyof GetStateObject<T>> = {
|
|
@@ -6,7 +6,8 @@ type PurifiedWithObjectToId<T, StateKeys extends keyof GetStateObject<T> = keyof
|
|
|
6
6
|
} & {
|
|
7
7
|
[K in StateKeys as null extends T[K] ? K : never]?: Purified<T[K]> | undefined;
|
|
8
8
|
};
|
|
9
|
-
export type PurifiedModel<T> = T extends
|
|
9
|
+
export type PurifiedModel<T> = T extends (infer S)[] ? PurifiedModel<S>[] : T extends string | number | boolean | Dayjs | File ? T : T extends Map<infer K, infer V> ? Map<K, PurifiedModel<V>> : PurifiedWithObjectToId<T>;
|
|
10
|
+
export type UploadableClientArg<T> = [T] extends [File[]] ? File[] | FileList : T;
|
|
10
11
|
export type PurifyFunc<Input, _DefaultInput = DefaultOf<Input>, _PurifiedInput = PurifiedModel<Input>> = (self: _DefaultInput, isChild?: boolean) => _PurifiedInput | null;
|
|
11
12
|
export type PurifyFuncV2<Input, RelationKey extends string = never, _DefaultInput = DefaultOfSchema<Input, RelationKey>, _PurifiedInput = PurifiedModel<Input>> = (self: _DefaultInput, isChild?: boolean) => _PurifiedInput | null;
|
|
12
13
|
export declare const makePurify: <I>(modelRef: ConstantModelRef<I>) => PurifyFunc<I>;
|
package/types/constant/via.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { CLIENT_VALUE, type Cls, DEFAULT_VALUE, type EnumInstance, FIELD_META, type ObjectAssign, type ObjectAssignKeyOfObjects, PURIFIED_VALUE, SERVER_VALUE } from "akanjs/base";
|
|
2
|
+
import { CascadePaths } from "./cascadePaths.d.ts";
|
|
2
3
|
import { type ExtractFieldInfoObject, type FieldBuilder, type FieldInfoObject, type FieldInfoObjectToFieldObject, type FieldObject, type FieldResolver } from "./fieldInfo.d.ts";
|
|
3
4
|
import { type PurifiedModel, type PurifyFunc } from "./purify.d.ts";
|
|
4
5
|
import { TextFieldPaths } from "./textFieldPaths.d.ts";
|
|
@@ -34,6 +35,7 @@ export interface ConstantStatics<Schema = any, OwnSchema = Schema, OwnFieldObj e
|
|
|
34
35
|
relations: Set<ConstantModelRef>;
|
|
35
36
|
enums: Set<EnumInstance>;
|
|
36
37
|
text: TextFieldPaths;
|
|
38
|
+
cascade: CascadePaths;
|
|
37
39
|
_OptionalKey: OptionalKey;
|
|
38
40
|
_RelationKey: RelationKey;
|
|
39
41
|
_PrimitiveKey: PrimitiveKey;
|
|
@@ -60,6 +62,7 @@ export interface DatabaseConstantStatics<Schema = any, FieldObj extends FieldObj
|
|
|
60
62
|
relations: Set<ConstantModelRef>;
|
|
61
63
|
enums: Set<EnumInstance>;
|
|
62
64
|
text: TextFieldPaths;
|
|
65
|
+
cascade: CascadePaths;
|
|
63
66
|
_DatabaseSchema: {
|
|
64
67
|
[K in keyof Schema]: K extends keyof FieldObj ? FieldObj[K]["fieldType"] extends "hidden" ? NonNullable<Schema[K]> : Schema[K] : Schema[K];
|
|
65
68
|
};
|
|
@@ -71,6 +74,7 @@ export type ConstantModelRef<Schema = any, FieldObj extends FieldObject = FieldO
|
|
|
71
74
|
relations: Set<ConstantModelRef>;
|
|
72
75
|
enums: Set<EnumInstance>;
|
|
73
76
|
text: TextFieldPaths;
|
|
77
|
+
cascade: CascadePaths;
|
|
74
78
|
}>;
|
|
75
79
|
export type DocumentConstantModelRef<Schema = any, FieldObj extends FieldObject = FieldObject, ModelType extends ConstantType = ConstantType, DatabaseSchema = DatabaseSchemaOf<Schema, never>> = Cls<Schema, {
|
|
76
80
|
[FIELD_META]: FieldObj;
|
|
@@ -60,7 +60,7 @@ type AppliedBaseSliceFetchType<RefName extends string, Input, Full, _Capitalized
|
|
|
60
60
|
} & {
|
|
61
61
|
[K in `get${_CapitalizedRefName}Edit`]: (id: string, option?: FetchPolicy) => Promise<ServerEdit<RefName, Full>>;
|
|
62
62
|
} & {
|
|
63
|
-
[K in `add${_CapitalizedRefName}Files`]: (fileList: FileList, parentId?: string, option?: FetchPolicy) => Promise<ProtoFile[]>;
|
|
63
|
+
[K in `add${_CapitalizedRefName}Files`]: (fileList: FileList | File[], parentId?: string, option?: FetchPolicy) => Promise<ProtoFile[]>;
|
|
64
64
|
} & {
|
|
65
65
|
[K in `merge${_CapitalizedRefName}`]: (modelOrId: Full | string, data: Partial<_PurifiedInput>, option?: FetchPolicy) => Promise<Full>;
|
|
66
66
|
};
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { type ConstantModel } from "akanjs/constant";
|
|
2
2
|
import { type DatabaseModel } from "akanjs/document";
|
|
3
|
-
import type { ServiceCls } from "akanjs/service";
|
|
3
|
+
import type { DatabaseService, ServiceCls } from "akanjs/service";
|
|
4
4
|
export declare class ServiceResolver {
|
|
5
5
|
#private;
|
|
6
|
-
static resolveDatabaseService(constant: ConstantModel, database: DatabaseModel, srvRef: ServiceCls): ServiceCls;
|
|
6
|
+
static resolveDatabaseService(constant: ConstantModel, database: DatabaseModel, srvRef: ServiceCls, getService: (refName: string) => DatabaseService): ServiceCls;
|
|
7
7
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { type Dayjs, type EnumInstance, type PromiseOrObject } from "akanjs/base";
|
|
2
|
-
import type { ConstantFieldType, ConstantFieldTypeInput, DocumentModel, FieldToValue, ParamFieldType, PlainTypeToFieldType, PurifiedModel } from "akanjs/constant";
|
|
2
|
+
import type { ConstantFieldType, ConstantFieldTypeInput, DocumentModel, FieldToValue, ParamFieldType, PlainTypeToFieldType, PurifiedModel, UploadableClientArg } from "akanjs/constant";
|
|
3
3
|
import type { ServiceModel } from "akanjs/service";
|
|
4
4
|
import type { InternalArgCls } from "./internalArg.d.ts";
|
|
5
5
|
import type { ArgType, SignalOption, SrvMap } from "./types.d.ts";
|
|
@@ -45,7 +45,7 @@ export declare class EndpointInfo<ReqType extends EndpointType = EndpointType, S
|
|
|
45
45
|
static getReturnInfo<Returns extends ConstantFieldTypeInput = ConstantFieldTypeInput, Nullable extends boolean = false>(modelRef: Returns, signalOption?: SignalOption<Returns, Nullable>): ReturnInfo<Returns, Nullable>;
|
|
46
46
|
constructor(type: ReqType, returnRef: Returns, signalOption?: SignalOption<Returns, Nullable>);
|
|
47
47
|
param<ArgName extends string, Arg extends ParamFieldType, _ClientArg = FieldToValue<Arg>, _ServerArg = DocumentModel<_ClientArg>>(name: string, arg: Arg, option?: Omit<EndpointArgProps, "nullable">): EndpointInfo<ReqType, Srvs, [...ArgNames, ArgName], [...Args, arg: _ClientArg], InternalArgs, [...ServerArgs, arg: _ServerArg], Returns, ClientReturns, ServerReturns, Nullable>;
|
|
48
|
-
body<ArgName extends string, ExplicitType = unknown, Arg extends ConstantFieldTypeInput = PlainTypeToFieldType<ExplicitType>, Optional extends boolean = false, _ArgType = unknown extends ExplicitType ? FieldToValue<Arg> : ExplicitType, _ClientArg = PurifiedModel<_ArgType
|
|
48
|
+
body<ArgName extends string, ExplicitType = unknown, Arg extends ConstantFieldTypeInput = PlainTypeToFieldType<ExplicitType>, Optional extends boolean = false, _ArgType = unknown extends ExplicitType ? FieldToValue<Arg> : ExplicitType, _ClientArg = UploadableClientArg<PurifiedModel<_ArgType>>, _ServerArg = DocumentModel<_ArgType>>(name: ArgName, arg: Arg, option?: EndpointArgProps<Optional>): EndpointInfo<ReqType, Srvs, [...ArgNames, ArgName], Optional extends true ? [...Args, arg?: _ClientArg | null] : [...Args, arg: _ClientArg], InternalArgs, [...ServerArgs, arg: _ServerArg | (Optional extends true ? undefined : never)], Returns, ClientReturns, ServerReturns, Nullable>;
|
|
49
49
|
room<ArgName extends string, ExplicitType = unknown, Arg extends ConstantFieldTypeInput = PlainTypeToFieldType<ExplicitType>, _ArgType = unknown extends ExplicitType ? FieldToValue<Arg> : ExplicitType, _ClientArg = PurifiedModel<_ArgType>, _ServerArg = DocumentModel<_ArgType>>(name: string, arg: Arg, option?: Omit<EndpointArgProps, "nullable">): EndpointInfo<ReqType, Srvs, [...ArgNames, ArgName], [...Args, arg: _ClientArg], InternalArgs, [...ServerArgs, arg: _ServerArg], Returns, ClientReturns, ServerReturns, Nullable>;
|
|
50
50
|
msg<ArgName extends string, ExplicitType = unknown, Arg extends ConstantFieldTypeInput = PlainTypeToFieldType<ExplicitType>, Optional extends boolean = false, _ArgType = unknown extends ExplicitType ? FieldToValue<Arg> : ExplicitType, _ClientArg = PurifiedModel<_ArgType>, _ServerArg = DocumentModel<_ArgType>>(name: string, arg: Arg, option?: EndpointArgProps<Optional>): EndpointInfo<ReqType, Srvs, [...ArgNames, ArgName], Optional extends true ? [...Args, arg?: _ClientArg | null] : [...Args, arg: _ClientArg], InternalArgs, [...ServerArgs, arg: _ServerArg | (Optional extends true ? undefined : never)], Returns, ClientReturns, ServerReturns, Nullable>;
|
|
51
51
|
search<ArgName extends string, ExplicitType = unknown, Arg extends ConstantFieldTypeInput = PlainTypeToFieldType<ExplicitType>, _ArgType = unknown extends ExplicitType ? FieldToValue<Arg> : ExplicitType, _ClientArg = PurifiedModel<_ArgType>, _ServerArg = DocumentModel<_ArgType>>(name: string, arg: Arg, option?: Omit<EndpointArgProps, "nullable">): EndpointInfo<ReqType, Srvs, [...ArgNames, ArgName], [...Args, arg?: _ClientArg | null], InternalArgs, [...ServerArgs, arg: _ServerArg | undefined], Returns, ClientReturns, ServerReturns, Nullable>;
|
package/types/signal/slice.d.ts
CHANGED
|
@@ -51,7 +51,7 @@ export declare function slice<SrvModule extends ServiceModel, BuildSlice extends
|
|
|
51
51
|
[""]: SliceInfo<SrvRefName<SrvModule>, _Input, _Full, _Light, _Insight, _Filter, SrvMap<SrvModule>, [
|
|
52
52
|
"query"
|
|
53
53
|
], [
|
|
54
|
-
_Query
|
|
54
|
+
query?: _Query | null
|
|
55
55
|
], [
|
|
56
56
|
], [
|
|
57
57
|
_Query
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { Cls, PromiseOrObject } from "akanjs/base";
|
|
2
|
-
import type { BaseInsight, BaseObject, ConstantFieldTypeInput, DocumentModel, FieldToValue, ParamFieldType, PlainTypeToFieldType, PurifiedModel, QueryOf } from "akanjs/constant";
|
|
2
|
+
import type { BaseInsight, BaseObject, ConstantFieldTypeInput, DocumentModel, FieldToValue, ParamFieldType, PlainTypeToFieldType, PurifiedModel, QueryOf, UploadableClientArg } from "akanjs/constant";
|
|
3
3
|
import type { FilterCls, FilterInstance } from "akanjs/document";
|
|
4
4
|
import type { ServiceModel } from "akanjs/service";
|
|
5
5
|
import { type ArgInfo, type EndpointArgProps, type InternalArgInfo, type InternalArgProps } from "./endpointInfo.d.ts";
|
|
@@ -23,7 +23,7 @@ export declare class SliceInfo<RefName extends string = string, Input = any, Ful
|
|
|
23
23
|
execFn: ((...args: [...ServerArgs, ...InternalArgs]) => QueryOf<DocumentModel<Full>>) | null;
|
|
24
24
|
constructor(refName: RefName, input: Cls<Input>, full: Cls<Full>, light: Cls<Light>, insight: Cls<Insight>, filter: FilterCls<Filter>, signalOption?: SignalOption);
|
|
25
25
|
param<ArgName extends string, Arg extends ParamFieldType, _ClientArg = FieldToValue<Arg>, _ServerArg = DocumentModel<_ClientArg>>(name: ArgName, arg: Arg, option?: Omit<EndpointArgProps, "nullable">): SliceInfo<RefName, Input, Full, Light, Insight, Filter, Srvs, [...ArgNames, ArgName], [...Args, arg: _ClientArg], InternalArgs, [...ServerArgs, arg: _ServerArg]>;
|
|
26
|
-
body<ArgName extends string, ExplicitType = unknown, Arg extends ConstantFieldTypeInput = PlainTypeToFieldType<ExplicitType>, Optional extends boolean = false, _ArgType = unknown extends ExplicitType ? FieldToValue<Arg> : ExplicitType, _ClientArg = PurifiedModel<_ArgType
|
|
26
|
+
body<ArgName extends string, ExplicitType = unknown, Arg extends ConstantFieldTypeInput = PlainTypeToFieldType<ExplicitType>, Optional extends boolean = false, _ArgType = unknown extends ExplicitType ? FieldToValue<Arg> : ExplicitType, _ClientArg = UploadableClientArg<PurifiedModel<_ArgType>>, _ServerArg = DocumentModel<_ArgType>>(name: ArgName, arg: Arg, option?: EndpointArgProps<Optional>): SliceInfo<RefName, Input, Full, Light, Insight, Filter, Srvs, [...ArgNames, ArgName], Optional extends true ? [...Args, arg?: _ClientArg | null] : [...Args, arg: _ClientArg], InternalArgs, [...ServerArgs, arg: _ServerArg | (Optional extends true ? undefined : never)]>;
|
|
27
27
|
search<ArgName extends string, ExplicitType = unknown, Arg extends ConstantFieldTypeInput = PlainTypeToFieldType<ExplicitType>, _ArgType = unknown extends ExplicitType ? FieldToValue<Arg> : ExplicitType, _ClientArg = PurifiedModel<_ArgType>, _ServerArg = DocumentModel<_ArgType>>(name: ArgName, arg: Arg, option?: Omit<EndpointArgProps, "nullable">): SliceInfo<RefName, Input, Full, Light, Insight, Filter, Srvs, [...ArgNames, ArgName], [...Args, arg?: _ClientArg | null], InternalArgs, [...ServerArgs, arg: _ServerArg | undefined]>;
|
|
28
28
|
with<ArgType, Optional extends boolean = false>(argRef: InternalArgCls<ArgType>, option?: InternalArgProps<Optional>): SliceInfo<RefName, Input, Full, Light, Insight, Filter, Srvs, ArgNames, Args, [...InternalArgs, arg: NonNullable<ArgType> | (Optional extends true ? null : never)], ServerArgs>;
|
|
29
29
|
exec(query: (this: {
|
package/types/store/action.d.ts
CHANGED
|
@@ -111,7 +111,7 @@ type ArrayFieldAddOrSubSetters<RefName extends string, _CapRef extends string, _
|
|
|
111
111
|
}) => void;
|
|
112
112
|
};
|
|
113
113
|
type FormSetter<Full, RefName extends string, _CapitalizedRefName extends string = Capitalize<RefName>, _DefaultState = DefaultOf<Full>> = FieldFormSetter<_DefaultState, RefName, _CapitalizedRefName> & ArrayFieldAddSetters<RefName, _CapitalizedRefName, _DefaultState> & ArrayFieldSubSetters<RefName, _CapitalizedRefName, _DefaultState> & ArrayFieldAddOrSubSetters<RefName, _CapitalizedRefName, _DefaultState> & {
|
|
114
|
-
[K in keyof _DefaultState as _DefaultState[K] extends (ProtoFile | null) | ProtoFile[] ? K extends string ? SetterKey<"upload", K, RefName, _CapitalizedRefName> : never : never]: (fileList: FileList, idx?: number) => Promise<void>;
|
|
114
|
+
[K in keyof _DefaultState as _DefaultState[K] extends (ProtoFile | null) | ProtoFile[] ? K extends string ? SetterKey<"upload", K, RefName, _CapitalizedRefName> : never : never]: (fileList: FileList | File[], idx?: number) => Promise<void>;
|
|
115
115
|
} & {
|
|
116
116
|
[K in `writeOn${_CapitalizedRefName}`]: (path: string | (string | number)[], value: any) => void;
|
|
117
117
|
};
|
package/types/store/state.d.ts
CHANGED
|
@@ -3,7 +3,7 @@ import { type DefaultOf } from "akanjs/constant";
|
|
|
3
3
|
import type { ExtractSort, FilterInstance } from "akanjs/document";
|
|
4
4
|
import type { SerializedSlice, SlceCnstCapitalizedRefName, SlceCnstDefault, SlceCnstFull, SlceCnstInsight, SlceCnstLight, SlceCnstRefName, SlceDbFilter, SlceDbSort, SliceCls } from "akanjs/signal";
|
|
5
5
|
import type { StoreSliceArgs, StoreSliceMap, StoreSliceSuffixCap, Submit } from "./types.d.ts";
|
|
6
|
-
export type SliceStateKey = "defaultModel" | "modelInsight" | "modelList" | "modelListLoading" | "modelInitList" | "modelInitAt" | "modelSelection" | "lastPageOfModel" | "pageOfModel" | "limitOfModel" | "queryArgsOfModel" | "sortOfModel";
|
|
6
|
+
export type SliceStateKey = "defaultModel" | "modelInsight" | "modelList" | "modelListLoading" | "modelInitList" | "modelInitAt" | "modelStaleAt" | "modelSelection" | "lastPageOfModel" | "pageOfModel" | "limitOfModel" | "queryArgsOfModel" | "sortOfModel";
|
|
7
7
|
type _SliceMap<S extends SliceCls> = StoreSliceMap<S>;
|
|
8
8
|
type _StateRefName<S extends SliceCls> = SlceCnstRefName<S>;
|
|
9
9
|
type _StateCap<S extends SliceCls> = SlceCnstCapitalizedRefName<S>;
|
|
@@ -42,6 +42,8 @@ export type SliceState<RefName extends string, Suffix extends string, Full, Ligh
|
|
|
42
42
|
[K in `${RefName}InitList${_CapitalizedSuffix}`]: DataList<Light>;
|
|
43
43
|
} & {
|
|
44
44
|
[K in `${RefName}InitAt${_CapitalizedSuffix}`]: Date;
|
|
45
|
+
} & {
|
|
46
|
+
[K in `${RefName}StaleAt${_CapitalizedSuffix}`]: Date;
|
|
45
47
|
} & {
|
|
46
48
|
[K in `${RefName}Selection${_CapitalizedSuffix}`]: DataList<Light>;
|
|
47
49
|
} & {
|
|
@@ -64,7 +66,7 @@ type DefaultSliceStateFields<SlceCls extends SliceCls, _RefName extends string,
|
|
|
64
66
|
} & {
|
|
65
67
|
[Suffix in _Suffixes as `${_RefName}List${StoreSliceSuffixCap<SlceCls, Suffix>}` | `${_RefName}InitList${StoreSliceSuffixCap<SlceCls, Suffix>}` | `${_RefName}Selection${StoreSliceSuffixCap<SlceCls, Suffix>}`]: DataList<_Light>;
|
|
66
68
|
} & {
|
|
67
|
-
[Suffix in _Suffixes as `${_RefName}InitAt${StoreSliceSuffixCap<SlceCls, Suffix>}`]: Date;
|
|
69
|
+
[Suffix in _Suffixes as `${_RefName}InitAt${StoreSliceSuffixCap<SlceCls, Suffix>}` | `${_RefName}StaleAt${StoreSliceSuffixCap<SlceCls, Suffix>}`]: Date;
|
|
68
70
|
} & {
|
|
69
71
|
[Suffix in _Suffixes as `${_RefName}ListLoading${StoreSliceSuffixCap<SlceCls, Suffix>}`]: boolean;
|
|
70
72
|
} & {
|
package/types/test/sampleOf.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import type { BaseObject, ConstantCls, DocumentModel } from "akanjs/constant";
|
|
1
|
+
import type { BaseObject, ConstantCls, DocumentModel, FieldObject } from "akanjs/constant";
|
|
2
2
|
export type SampleOf<Model> = DocumentModel<{
|
|
3
3
|
[K in keyof Model as Model[K] extends BaseObject ? never : K]: NonNullable<Model[K]>;
|
|
4
4
|
}>;
|
|
5
|
-
export declare const sampleOf: <Model>(modelRef: ConstantCls<Model>) => DocumentModel<{ [K in keyof Model as Model[K] extends BaseObject ? never : K]: NonNullable<Model[K]>; }>;
|
|
5
|
+
export declare const sampleOf: <Model, FieldObj extends FieldObject>(modelRef: ConstantCls<Model, FieldObj>) => DocumentModel<{ [K in keyof Model as Model[K] extends BaseObject ? never : K]: NonNullable<Model[K]>; }>;
|
package/types/ui/Load/Units.d.ts
CHANGED
|
@@ -18,6 +18,8 @@ interface DefaultProps<L extends {
|
|
|
18
18
|
renderList?: (list: DataList<L>) => ReactNode;
|
|
19
19
|
reverse?: boolean;
|
|
20
20
|
pagination?: boolean;
|
|
21
|
+
/** Max age in ms of the cached slice data before the client refetches on mount; `0` always refetches. */
|
|
22
|
+
staleTime?: number;
|
|
21
23
|
}
|
|
22
24
|
interface UnitsProps<RefName extends string, Light extends {
|
|
23
25
|
id: string;
|
|
@@ -26,5 +28,5 @@ interface UnitsProps<RefName extends string, Light extends {
|
|
|
26
28
|
}
|
|
27
29
|
export default function Units<RefName extends string, Light extends {
|
|
28
30
|
id: string;
|
|
29
|
-
}>({ containerRef, className, init, noDiv, from, to, loading, renderItem, renderList, renderEmpty, filter, sort, reverse, style, pagination, }: UnitsProps<RefName, Light>): import("react/jsx-runtime").JSX.Element;
|
|
31
|
+
}>({ containerRef, className, init, noDiv, from, to, loading, renderItem, renderList, renderEmpty, filter, sort, reverse, style, pagination, staleTime, }: UnitsProps<RefName, Light>): import("react/jsx-runtime").JSX.Element;
|
|
30
32
|
export {};
|
package/ui/Load/Units.tsx
CHANGED
|
@@ -28,6 +28,8 @@ interface DefaultProps<L extends { id: string }> {
|
|
|
28
28
|
renderList?: (list: DataList<L>) => ReactNode;
|
|
29
29
|
reverse?: boolean;
|
|
30
30
|
pagination?: boolean;
|
|
31
|
+
/** Max age in ms of the cached slice data before the client refetches on mount; `0` always refetches. */
|
|
32
|
+
staleTime?: number;
|
|
31
33
|
}
|
|
32
34
|
|
|
33
35
|
interface UnitsProps<RefName extends string, Light extends { id: string }> extends DefaultProps<Light> {
|
|
@@ -60,6 +62,7 @@ function Render<RefName extends string, Light extends { id: string }>({
|
|
|
60
62
|
sort = (a, b) => 1,
|
|
61
63
|
reverse,
|
|
62
64
|
pagination,
|
|
65
|
+
staleTime,
|
|
63
66
|
}: RenderProps<RefName, Light>) {
|
|
64
67
|
const loaded = useRef(false);
|
|
65
68
|
const storeUse = st.use as { [key: string]: () => unknown };
|
|
@@ -75,6 +78,7 @@ function Render<RefName extends string, Light extends { id: string }>({
|
|
|
75
78
|
modelInsight: `${modelName}Insight`,
|
|
76
79
|
modelInitList: `${modelName}InitList`,
|
|
77
80
|
modelInitAt: `${modelName}InitAt`,
|
|
81
|
+
modelStaleAt: `${modelName}StaleAt`,
|
|
78
82
|
modelObjList: `${modelName}ObjList`,
|
|
79
83
|
modelObjInsight: `${modelName}ObjInsight`,
|
|
80
84
|
pageOfModel: `pageOf${ModelName}`,
|
|
@@ -84,12 +88,14 @@ function Render<RefName extends string, Light extends { id: string }>({
|
|
|
84
88
|
sortOfModel: `sortOf${ModelName}`,
|
|
85
89
|
setPageOfModel: `setPageOf${ModelName}`,
|
|
86
90
|
addPageOfModel: `addPageOf${ModelName}`,
|
|
91
|
+
refreshModel: `refresh${ModelName}`,
|
|
87
92
|
};
|
|
88
93
|
const namesOfSlice = {
|
|
89
94
|
modelList: sliceName.replace(names.model, names.modelList),
|
|
90
95
|
modelListLoading: sliceName.replace(names.model, names.modelListLoading),
|
|
91
96
|
modelInitList: sliceName.replace(names.model, names.modelInitList),
|
|
92
97
|
modelInitAt: sliceName.replace(names.model, names.modelInitAt),
|
|
98
|
+
modelStaleAt: sliceName.replace(names.model, names.modelStaleAt),
|
|
93
99
|
modelInsight: sliceName.replace(names.model, names.modelInsight),
|
|
94
100
|
pageOfModel: sliceName.replace(names.model, names.pageOfModel),
|
|
95
101
|
lastPageOfModel: sliceName.replace(names.model, names.lastPageOfModel),
|
|
@@ -98,6 +104,7 @@ function Render<RefName extends string, Light extends { id: string }>({
|
|
|
98
104
|
sortOfModel: sliceName.replace(names.model, names.sortOfModel),
|
|
99
105
|
setPageOfModel: sliceName.replace(names.model, names.setPageOfModel),
|
|
100
106
|
addPageOfModel: sliceName.replace(names.model, names.addPageOfModel),
|
|
107
|
+
refreshModel: sliceName.replace(names.model, names.refreshModel),
|
|
101
108
|
};
|
|
102
109
|
const modelList = storeUse[namesOfSlice.modelList]() as DataList<Light>;
|
|
103
110
|
const modelListLoading = storeUse[namesOfSlice.modelListLoading]() as string | boolean;
|
|
@@ -106,6 +113,7 @@ function Render<RefName extends string, Light extends { id: string }>({
|
|
|
106
113
|
const initModelObjInsight = (init as any)[names.modelObjInsight] as BaseInsight;
|
|
107
114
|
const initLimitOfModel = (init as any)[names.limitOfModel] as number;
|
|
108
115
|
const initPageOfModel = (init as any)[names.pageOfModel] as number;
|
|
116
|
+
const modelStaleAt = storeUse[namesOfSlice.modelStaleAt]() as Date;
|
|
109
117
|
|
|
110
118
|
const useCache =
|
|
111
119
|
!modelListLoading &&
|
|
@@ -143,6 +151,13 @@ function Render<RefName extends string, Light extends { id: string }>({
|
|
|
143
151
|
loaded.current = true;
|
|
144
152
|
}, []);
|
|
145
153
|
|
|
154
|
+
useEffect(() => {
|
|
155
|
+
const staleThreshold = Math.max(modelStaleAt.getTime(), staleTime === undefined ? 0 : Date.now() - staleTime);
|
|
156
|
+
if (storeGet<Date>()[namesOfSlice.modelInitAt].getTime() >= staleThreshold) return;
|
|
157
|
+
if (storeGet<boolean>()[namesOfSlice.modelListLoading]) return;
|
|
158
|
+
void storeDo[namesOfSlice.refreshModel]({ invalidate: true });
|
|
159
|
+
}, [modelStaleAt]);
|
|
160
|
+
|
|
146
161
|
const modelInsight = storeUse[namesOfSlice.modelInsight]() as BaseInsight;
|
|
147
162
|
const limitOfModel = storeUse[namesOfSlice.limitOfModel]() as number;
|
|
148
163
|
const pageOfModel = storeUse[namesOfSlice.pageOfModel]() as number;
|
|
@@ -237,6 +252,7 @@ export default function Units<RefName extends string, Light extends { id: string
|
|
|
237
252
|
reverse,
|
|
238
253
|
style,
|
|
239
254
|
pagination = true,
|
|
255
|
+
staleTime,
|
|
240
256
|
}: UnitsProps<RefName, Light>) {
|
|
241
257
|
const props: UnitsProps<RefName, Light> = {
|
|
242
258
|
containerRef,
|
|
@@ -254,6 +270,7 @@ export default function Units<RefName extends string, Light extends { id: string
|
|
|
254
270
|
sort,
|
|
255
271
|
reverse,
|
|
256
272
|
pagination,
|
|
273
|
+
staleTime,
|
|
257
274
|
};
|
|
258
275
|
|
|
259
276
|
const { fulfilled, value: promiseInit } = useFetch(init);
|
package/ui/Signal/Arg.tsx
CHANGED
|
@@ -351,7 +351,7 @@ const ArgUpload = ({ value, onChange }: ArgUploadProps) => {
|
|
|
351
351
|
multiple
|
|
352
352
|
className="file-input file-input-bordered w-full max-w-xs"
|
|
353
353
|
onChange={(e: ChangeEvent<HTMLInputElement>) => {
|
|
354
|
-
onChange(
|
|
354
|
+
onChange(e.target.files);
|
|
355
355
|
}}
|
|
356
356
|
/>
|
|
357
357
|
);
|