akanjs 2.4.2-rc.0 → 2.4.2-rc.2
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/index.ts +1 -1
- package/common/subRoute.ts +61 -0
- package/constant/cascadePaths.ts +32 -0
- package/constant/fieldInfo.ts +11 -3
- package/constant/index.ts +3 -0
- package/constant/textFieldPathSet.ts +8 -0
- package/constant/textFieldPaths.ts +59 -0
- package/constant/types.ts +0 -4
- package/constant/via.ts +19 -28
- package/dictionary/dictInfo.ts +4 -0
- package/document/documentQuery.ts +17 -1
- package/document/documentSchema.ts +1 -15
- package/document/filterMeta.ts +4 -2
- package/local/apps/serverLifecycle/serverLifecycle-local.db +0 -0
- package/local/apps/serverLifecycle/serverLifecycle-local.db-shm +0 -0
- package/local/apps/serverLifecycle/serverLifecycle-local.db-wal +0 -0
- package/package.json +1 -1
- package/server/devtools/types.ts +2 -2
- package/server/di/diLifecycle.ts +7 -1
- package/server/proxy/hostBasePathWebProxy.ts +16 -5
- package/server/resolver/database.resolver.ts +5 -6
- package/server/resolver/service.resolver.ts +27 -5
- package/server/webRouter.ts +27 -11
- package/service/predefinedAdaptor/database.adaptor.ts +212 -88
- package/service/predefinedAdaptor/index.ts +1 -0
- package/service/predefinedAdaptor/searchIndex.ts +517 -0
- package/service/predefinedAdaptor/sqlDescriptor.ts +25 -0
- 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 +8 -3
- package/types/constant/index.d.ts +3 -0
- package/types/constant/textFieldPathSet.d.ts +8 -0
- package/types/constant/textFieldPaths.d.ts +10 -0
- package/types/constant/types.d.ts +0 -3
- package/types/constant/via.d.ts +8 -24
- package/types/dictionary/base.dictionary.d.ts +1 -1
- package/types/dictionary/dictionary.d.ts +8 -8
- package/types/document/documentQuery.d.ts +13 -1
- package/types/document/documentSchema.d.ts +0 -3
- package/types/document/filterMeta.d.ts +2 -1
- package/types/server/devtools/types.d.ts +2 -2
- package/types/server/resolver/service.resolver.d.ts +3 -3
- package/types/service/predefinedAdaptor/database.adaptor.d.ts +46 -13
- package/types/service/predefinedAdaptor/index.d.ts +1 -0
- package/types/service/predefinedAdaptor/searchIndex.d.ts +73 -0
- package/types/service/predefinedAdaptor/sqlDescriptor.d.ts +5 -0
- package/types/ui/Constant/schemaDoc.d.ts +2 -2
- package/ui/Constant/schemaDoc.ts +8 -2
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,7 +16,9 @@ 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";
|
|
21
|
+
import type { TextFieldRole } from "./textFieldPaths";
|
|
20
22
|
import type { BaseObject } from "./types";
|
|
21
23
|
import type { ConstantModelRef } from "./via";
|
|
22
24
|
|
|
@@ -102,7 +104,8 @@ export interface ConstantFieldProps<
|
|
|
102
104
|
example?: FieldValue;
|
|
103
105
|
of?: MapValue;
|
|
104
106
|
validate?: (value: FieldValue, model: any) => boolean;
|
|
105
|
-
text?:
|
|
107
|
+
text?: TextFieldRole;
|
|
108
|
+
cascade?: CascadeAction;
|
|
106
109
|
meta?: Metadata;
|
|
107
110
|
}
|
|
108
111
|
export const fieldPresets = ["email", "password", "url"] as const;
|
|
@@ -192,7 +195,8 @@ interface ConstantFieldBuildProps<
|
|
|
192
195
|
example?: FieldValue;
|
|
193
196
|
of?: MapValue;
|
|
194
197
|
validate?: (value: FieldValue, model: any) => boolean;
|
|
195
|
-
text?:
|
|
198
|
+
text?: TextFieldRole;
|
|
199
|
+
cascade?: CascadeAction;
|
|
196
200
|
modelRef: ConstantModelRef;
|
|
197
201
|
arrDepth: number;
|
|
198
202
|
optArrDepth: number;
|
|
@@ -315,7 +319,8 @@ export class ConstantField<
|
|
|
315
319
|
readonly example?: FieldValue;
|
|
316
320
|
readonly of?: MapValue;
|
|
317
321
|
readonly validate?: (value: FieldValue, model: any) => boolean;
|
|
318
|
-
readonly text?:
|
|
322
|
+
readonly text?: TextFieldRole;
|
|
323
|
+
readonly cascade?: CascadeAction;
|
|
319
324
|
readonly modelRef: ConstantModelRef;
|
|
320
325
|
readonly arrDepth: number;
|
|
321
326
|
readonly optArrDepth: number;
|
|
@@ -347,6 +352,7 @@ export class ConstantField<
|
|
|
347
352
|
this.of = props.of;
|
|
348
353
|
this.validate = props.validate;
|
|
349
354
|
this.text = props.text;
|
|
355
|
+
this.cascade = props.cascade;
|
|
350
356
|
this.modelRef = props.modelRef;
|
|
351
357
|
this.arrDepth = props.arrDepth;
|
|
352
358
|
this.optArrDepth = props.optArrDepth;
|
|
@@ -425,6 +431,7 @@ export class ConstantField<
|
|
|
425
431
|
of: option.of,
|
|
426
432
|
validate: option.validate,
|
|
427
433
|
text: option.text,
|
|
434
|
+
cascade: option.cascade,
|
|
428
435
|
modelRef,
|
|
429
436
|
arrDepth: arrDepth,
|
|
430
437
|
optArrDepth: optArrDepth,
|
|
@@ -464,6 +471,7 @@ export class ConstantField<
|
|
|
464
471
|
of: this.of,
|
|
465
472
|
validate: this.validate,
|
|
466
473
|
text: this.text,
|
|
474
|
+
cascade: this.cascade,
|
|
467
475
|
modelRef: this.modelRef,
|
|
468
476
|
arrDepth: this.arrDepth,
|
|
469
477
|
optArrDepth: this.optArrDepth,
|
package/constant/index.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
export * from "./cascadePaths";
|
|
1
2
|
export * from "./constantRegistry";
|
|
2
3
|
export * from "./crystalize";
|
|
3
4
|
export * from "./deserialize";
|
|
@@ -6,5 +7,7 @@ export * from "./getDefault";
|
|
|
6
7
|
export * from "./immerify";
|
|
7
8
|
export * from "./purify";
|
|
8
9
|
export * from "./serialize";
|
|
10
|
+
export * from "./textFieldPathSet";
|
|
11
|
+
export * from "./textFieldPaths";
|
|
9
12
|
export * from "./types";
|
|
10
13
|
export * from "./via";
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/** The `search_doc` columns a model feeds, each holding the document paths that write it. */
|
|
2
|
+
export class TextFieldPathSet {
|
|
3
|
+
readonly title = new Set<string>();
|
|
4
|
+
readonly desc = new Set<string>();
|
|
5
|
+
readonly tag = new Set<string>();
|
|
6
|
+
readonly thumb = new Set<string>();
|
|
7
|
+
readonly filter = new Set<string>();
|
|
8
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { type Cls, PrimitiveRegistry, type PrimitiveScalar } from "akanjs/base";
|
|
2
|
+
import type { ConstantField, FieldObject } from "./fieldInfo";
|
|
3
|
+
import { TextFieldPathSet } from "./textFieldPathSet";
|
|
4
|
+
|
|
5
|
+
/** Column a `text` field feeds in the `search_doc` mirror. */
|
|
6
|
+
export const textFieldRoles = ["title", "desc", "tag", "thumb", "filter"] as const;
|
|
7
|
+
export type TextFieldRole = (typeof textFieldRoles)[number];
|
|
8
|
+
|
|
9
|
+
const refRoles = new Set<TextFieldRole>(["thumb", "filter"]);
|
|
10
|
+
|
|
11
|
+
export class TextFieldPaths extends TextFieldPathSet {
|
|
12
|
+
readonly children = new TextFieldPathSet();
|
|
13
|
+
|
|
14
|
+
collect(fieldMap: FieldObject) {
|
|
15
|
+
for (const [key, field] of Object.entries(fieldMap)) {
|
|
16
|
+
if (field.text) {
|
|
17
|
+
this.#assertIndexable(key, field.text, field);
|
|
18
|
+
this[field.text].add(key);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
if (field.isClass && field.isScalar) this.#mergeChild(key, field);
|
|
22
|
+
}
|
|
23
|
+
return this;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
#mergeChild(key: string, parent: ConstantField) {
|
|
27
|
+
const child = parent.modelRef.text as TextFieldPaths;
|
|
28
|
+
for (const role of textFieldRoles) {
|
|
29
|
+
for (const path of [...child[role], ...child.children[role]]) {
|
|
30
|
+
this.#assertReachable(`${key}.${path}`, parent);
|
|
31
|
+
this.children[role].add(`${key}.${path}`);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
#assertReachable(path: string, parent: ConstantField) {
|
|
37
|
+
if (parent.fieldType === "secret") throw new Error(`Text field "${path}" is under a secret field`);
|
|
38
|
+
if (parent.fieldType === "hidden") throw new Error(`Text field "${path}" is under a hidden field`);
|
|
39
|
+
if (parent.fieldType === "resolve") throw new Error(`Text field "${path}" is under a resolved field`);
|
|
40
|
+
if (parent.arrDepth > 1) throw new Error(`Text field "${path}" is under a nested array and cannot be indexed`);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
#assertIndexable(key: string, role: TextFieldRole, field: ConstantField) {
|
|
44
|
+
|
|
45
|
+
if (field.fieldType === "secret") throw new Error(`Text field "${key}" is secret and must not be indexed`);
|
|
46
|
+
if (field.fieldType === "hidden") throw new Error(`Text field "${key}" is hidden and must not be indexed`);
|
|
47
|
+
if (field.fieldType === "resolve") throw new Error(`Text field "${key}" is resolved and is absent from _doc`);
|
|
48
|
+
if (field.isMap) throw new Error(`Text field "${key}" is a Map and cannot be indexed`);
|
|
49
|
+
if (field.arrDepth > 1) throw new Error(`Text field "${key}" is a nested array and cannot be indexed`);
|
|
50
|
+
const modelRef = field.modelRef as unknown as Cls;
|
|
51
|
+
const refName = PrimitiveRegistry.has(modelRef)
|
|
52
|
+
? PrimitiveRegistry.getName(modelRef as unknown as typeof PrimitiveScalar)
|
|
53
|
+
: null;
|
|
54
|
+
if (refName === "String") return;
|
|
55
|
+
if (refRoles.has(role) && (refName === "ID" || (field.isClass && !field.isScalar))) return;
|
|
56
|
+
const accepted = refRoles.has(role) ? "String, ID, or a model reference" : "String";
|
|
57
|
+
throw new Error(`Text field "${key}" declares text: "${role}", which accepts ${accepted}`);
|
|
58
|
+
}
|
|
59
|
+
}
|
package/constant/types.ts
CHANGED
|
@@ -98,10 +98,6 @@ export interface ProtoPatch {
|
|
|
98
98
|
}
|
|
99
99
|
|
|
100
100
|
export const DEFAULT_PAGE_SIZE = 20;
|
|
101
|
-
export interface TextDoc {
|
|
102
|
-
[key: string]: string | TextDoc;
|
|
103
|
-
}
|
|
104
|
-
|
|
105
101
|
export type NonFunctionalKeys<T> = {
|
|
106
102
|
[K in keyof T]: T[K] extends (...args: never[]) => unknown ? never : K;
|
|
107
103
|
}[keyof T];
|
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,
|
|
@@ -26,6 +27,7 @@ import {
|
|
|
26
27
|
resolve,
|
|
27
28
|
} from "./fieldInfo";
|
|
28
29
|
import { makePurify, type PurifiedModel, type PurifyFunc } from "./purify";
|
|
30
|
+
import { TextFieldPaths } from "./textFieldPaths";
|
|
29
31
|
import type { BaseInsight, BaseObject, ConstantType, DefaultOf, DefaultOfSchema, NonFunctionalKeys } from "./types";
|
|
30
32
|
|
|
31
33
|
type BaseFields = "id" | "createdAt" | "updatedAt" | "removedAt";
|
|
@@ -191,8 +193,8 @@ const getBaseConstantClass = (field: FieldObject, modelType: ConstantType = "sca
|
|
|
191
193
|
class BaseConstant {
|
|
192
194
|
static readonly [FIELD_META]: FieldObject = field;
|
|
193
195
|
static modelType: ConstantType = modelType;
|
|
194
|
-
static text:
|
|
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();
|
|
@@ -268,7 +270,8 @@ export interface ConstantStatics<
|
|
|
268
270
|
children: Set<ConstantModelRef>;
|
|
269
271
|
relations: Set<ConstantModelRef>;
|
|
270
272
|
enums: Set<EnumInstance>;
|
|
271
|
-
text:
|
|
273
|
+
text: TextFieldPaths;
|
|
274
|
+
cascade: CascadePaths;
|
|
272
275
|
_OptionalKey: OptionalKey;
|
|
273
276
|
_RelationKey: RelationKey;
|
|
274
277
|
_PrimitiveKey: PrimitiveKey;
|
|
@@ -295,7 +298,8 @@ export interface DatabaseConstantStatics<Schema = any, FieldObj extends FieldObj
|
|
|
295
298
|
children: Set<ConstantModelRef>;
|
|
296
299
|
relations: Set<ConstantModelRef>;
|
|
297
300
|
enums: Set<EnumInstance>;
|
|
298
|
-
text:
|
|
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"
|
|
@@ -317,7 +321,8 @@ export type ConstantModelRef<
|
|
|
317
321
|
children: Set<ConstantModelRef>;
|
|
318
322
|
relations: Set<ConstantModelRef>;
|
|
319
323
|
enums: Set<EnumInstance>;
|
|
320
|
-
text:
|
|
324
|
+
text: TextFieldPaths;
|
|
325
|
+
cascade: CascadePaths;
|
|
321
326
|
}
|
|
322
327
|
>;
|
|
323
328
|
|
|
@@ -430,31 +435,17 @@ const applyConstantStatics = <Model>(model: ConstantCls<Model>, fieldMap: FieldO
|
|
|
430
435
|
purify: makePurify(model),
|
|
431
436
|
getDefault: () => ({ ...defaultValue }),
|
|
432
437
|
});
|
|
433
|
-
Object.entries(fieldMap).forEach(([
|
|
438
|
+
Object.entries(fieldMap).forEach(([, field]) => {
|
|
434
439
|
if (field.enum) model.enums.add(field.enum);
|
|
435
|
-
if (field.
|
|
436
|
-
|
|
437
|
-
else
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
for (const childEnum of field.modelRef.enums) model.enums.add(childEnum);
|
|
442
|
-
for (const relation of field.modelRef.relations) model.relations.add(relation);
|
|
443
|
-
for (const relationEnum of field.modelRef.enums) model.enums.add(relationEnum);
|
|
444
|
-
field.modelRef.text.search.forEach((subKey) => {
|
|
445
|
-
model.text.children.search.add(`${key}.${subKey}`);
|
|
446
|
-
});
|
|
447
|
-
field.modelRef.text.filter.forEach((subKey) => {
|
|
448
|
-
model.text.children.filter.add(`${key}.${subKey}`);
|
|
449
|
-
});
|
|
450
|
-
field.modelRef.text.children.search.forEach((subKey) => {
|
|
451
|
-
model.text.children.search.add(`${key}.${subKey}`);
|
|
452
|
-
});
|
|
453
|
-
field.modelRef.text.children.filter.forEach((subKey) => {
|
|
454
|
-
model.text.children.filter.add(`${key}.${subKey}`);
|
|
455
|
-
});
|
|
456
|
-
}
|
|
440
|
+
if (!field.isClass) return;
|
|
441
|
+
if (field.isScalar) model.children.add(field.modelRef);
|
|
442
|
+
else model.relations.add(field.modelRef);
|
|
443
|
+
for (const child of field.modelRef.children) model.children.add(child);
|
|
444
|
+
for (const childEnum of field.modelRef.enums) model.enums.add(childEnum);
|
|
445
|
+
for (const relation of field.modelRef.relations) model.relations.add(relation);
|
|
457
446
|
});
|
|
447
|
+
model.text.collect(fieldMap);
|
|
448
|
+
model.cascade.collect(fieldMap);
|
|
458
449
|
return model as unknown as ConstantCls<Model>;
|
|
459
450
|
};
|
|
460
451
|
|
package/dictionary/dictInfo.ts
CHANGED
|
@@ -169,6 +169,10 @@ export class ModelDictInfo<
|
|
|
169
169
|
} = {
|
|
170
170
|
latest: FieldTranslation.translate(["Latest", "최신순"]).desc(["Latest", "최신순"]),
|
|
171
171
|
oldest: FieldTranslation.translate(["Oldest", "오래된순"]).desc(["Oldest", "오래된순"]),
|
|
172
|
+
relevance: FieldTranslation.translate(["Relevance", "관련도순"]).desc([
|
|
173
|
+
"Best text-search match first",
|
|
174
|
+
"검색어와 가장 관련있는 순",
|
|
175
|
+
]),
|
|
172
176
|
};
|
|
173
177
|
static getBaseSignalDictionary<T extends string>(refName: T): BaseModelCrudGetSignalTranslation<T, [string, string]> {
|
|
174
178
|
const capRefName = capitalize(refName);
|
|
@@ -23,12 +23,22 @@ export const isDocumentId = (value: unknown): value is DocumentId =>
|
|
|
23
23
|
export type DocumentPrimitive = string | number | boolean | null | Dayjs | Date;
|
|
24
24
|
export type DocumentPath<T = any> = Extract<keyof T, string> | (string & {});
|
|
25
25
|
|
|
26
|
+
export const searchColumns = ["title", "desc", "tag", "filter"] as const;
|
|
27
|
+
export type SearchColumn = (typeof searchColumns)[number];
|
|
28
|
+
|
|
29
|
+
export interface DocumentSearchOptions {
|
|
30
|
+
columns?: SearchColumn[];
|
|
31
|
+
prefix?: boolean;
|
|
32
|
+
weights?: number[];
|
|
33
|
+
}
|
|
34
|
+
|
|
26
35
|
export type DocumentQueryNode =
|
|
27
36
|
| { kind: "all"; queries: DocumentQuery[] }
|
|
28
37
|
| { kind: "any"; queries: DocumentQuery[] }
|
|
29
38
|
| { kind: "not"; query: DocumentQuery }
|
|
30
39
|
| { kind: "op"; op: DocumentQueryOperator; value?: unknown }
|
|
31
|
-
| { kind: "raw"; sql: string; params: unknown[] }
|
|
40
|
+
| { kind: "raw"; sql: string; params: unknown[] }
|
|
41
|
+
| ({ kind: "search"; text: string } & DocumentSearchOptions);
|
|
32
42
|
|
|
33
43
|
export type DocumentQueryOperator =
|
|
34
44
|
| "eq"
|
|
@@ -152,6 +162,12 @@ export const createDocumentQueryHelper = () => ({
|
|
|
152
162
|
has: (value: unknown) => op("has", value),
|
|
153
163
|
contains: (value: unknown) => op("contains", value),
|
|
154
164
|
raw: (sql: string, params: unknown[] = []): DocumentQueryNode => ({ kind: "raw", sql, params }),
|
|
165
|
+
|
|
166
|
+
search: (text: string, options: DocumentSearchOptions = {}): DocumentQueryNode => ({
|
|
167
|
+
kind: "search",
|
|
168
|
+
text,
|
|
169
|
+
...options,
|
|
170
|
+
}),
|
|
155
171
|
when: (condition: unknown, query: DocumentQuery): DocumentQuery => (condition ? query : {}),
|
|
156
172
|
});
|
|
157
173
|
|
|
@@ -7,15 +7,14 @@ export type DocumentHookName = `before${Capitalize<SaveEventType>}` | `after${Ca
|
|
|
7
7
|
|
|
8
8
|
export interface DocumentIndexDescriptor {
|
|
9
9
|
name?: string;
|
|
10
|
+
|
|
10
11
|
fields: Record<string, 1 | -1 | "text" | boolean>;
|
|
11
12
|
unique?: boolean;
|
|
12
|
-
text?: boolean;
|
|
13
13
|
where?: DocumentQuery;
|
|
14
14
|
}
|
|
15
15
|
|
|
16
16
|
export interface DocumentIndexBuilder<Schema> {
|
|
17
17
|
path(path: string, order?: 1 | -1): DocumentIndexBuilder<Schema>;
|
|
18
|
-
text(path: string): DocumentIndexBuilder<Schema>;
|
|
19
18
|
unique(): DocumentIndexBuilder<Schema>;
|
|
20
19
|
where(where: DocumentQuery | ((q: DocumentQueryHelper) => DocumentQuery)): DocumentIndexBuilder<Schema>;
|
|
21
20
|
done(): Schema;
|
|
@@ -58,14 +57,6 @@ export class DocumentSchema<Doc = unknown> {
|
|
|
58
57
|
return this;
|
|
59
58
|
}
|
|
60
59
|
|
|
61
|
-
text(...fields: string[]) {
|
|
62
|
-
this.indexes.push({
|
|
63
|
-
text: true,
|
|
64
|
-
fields: Object.fromEntries(fields.map((field) => [field, "text" as const])),
|
|
65
|
-
});
|
|
66
|
-
return this;
|
|
67
|
-
}
|
|
68
|
-
|
|
69
60
|
createIndex(name: string): DocumentIndexBuilder<this> {
|
|
70
61
|
const schema = this;
|
|
71
62
|
const descriptor: DocumentIndexDescriptor = { name, fields: {} };
|
|
@@ -74,11 +65,6 @@ export class DocumentSchema<Doc = unknown> {
|
|
|
74
65
|
descriptor.fields[path] = order;
|
|
75
66
|
return api;
|
|
76
67
|
},
|
|
77
|
-
text(path: string) {
|
|
78
|
-
descriptor.fields[path] = "text";
|
|
79
|
-
descriptor.text = true;
|
|
80
|
-
return api;
|
|
81
|
-
},
|
|
82
68
|
unique() {
|
|
83
69
|
descriptor.unique = true;
|
|
84
70
|
return api;
|
package/document/filterMeta.ts
CHANGED
|
@@ -98,7 +98,7 @@ export const fillMissingFilterArgs = (filterInfo: FilterInfo, args: unknown[]) =
|
|
|
98
98
|
return [...args, ...Array(filterInfo.args.length - args.length).fill(undefined)];
|
|
99
99
|
};
|
|
100
100
|
|
|
101
|
-
export type BaseFilterSortKey = "latest" | "oldest";
|
|
101
|
+
export type BaseFilterSortKey = "latest" | "oldest" | "relevance";
|
|
102
102
|
export type BaseFilterQueryKey = "any";
|
|
103
103
|
export type BaseFilterKey = BaseFilterSortKey | BaseFilterQueryKey;
|
|
104
104
|
|
|
@@ -130,6 +130,8 @@ interface BaseQuery<Model> {
|
|
|
130
130
|
interface BaseSort {
|
|
131
131
|
latest: { createdAt: -1 };
|
|
132
132
|
oldest: { createdAt: 1 };
|
|
133
|
+
|
|
134
|
+
relevance: Record<string, never>;
|
|
133
135
|
}
|
|
134
136
|
type LibFilterQuery<LibFilters extends FilterCls[]> = MergeAllDoubleKeyOfObjects<
|
|
135
137
|
LibFilters,
|
|
@@ -189,7 +191,7 @@ export const from = <
|
|
|
189
191
|
any: filter().query((_q) => ({ removedAt: { empty: true } })),
|
|
190
192
|
...querySort.query,
|
|
191
193
|
},
|
|
192
|
-
sort: Object.assign({ latest: { createdAt: -1 }, oldest: { createdAt: 1 } }, querySort.sort),
|
|
194
|
+
sort: Object.assign({ latest: { createdAt: -1 }, oldest: { createdAt: 1 }, relevance: {} }, querySort.sort),
|
|
193
195
|
},
|
|
194
196
|
...libFilterRefs.map((libFilterRef) => getFilterMeta(libFilterRef)),
|
|
195
197
|
);
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
package/package.json
CHANGED
package/server/devtools/types.ts
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* GET /_akan/deps -> DevtoolsEnvelope<"deps", DepsData>
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
|
-
import type { ConstantType } from "akanjs/constant";
|
|
11
|
+
import type { ConstantType, TextFieldRole } from "akanjs/constant";
|
|
12
12
|
import type { RootDictionary } from "akanjs/dictionary";
|
|
13
13
|
import type { ArgType, SerializedArg, SerializedReturns } from "akanjs/signal";
|
|
14
14
|
|
|
@@ -73,7 +73,7 @@ export interface ConstantFieldNode {
|
|
|
73
73
|
maxlength?: number;
|
|
74
74
|
/** `ConstantFieldProps["type"]`: "email" | "password" | "url". */
|
|
75
75
|
preset?: string;
|
|
76
|
-
text?:
|
|
76
|
+
text?: TextFieldRole;
|
|
77
77
|
accumulate?: unknown;
|
|
78
78
|
example?: unknown;
|
|
79
79
|
meta?: Record<string, unknown>;
|
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
|
|
|
@@ -49,11 +49,12 @@ const timedQuery = async <T>(fn: () => Promise<T>): Promise<T> => {
|
|
|
49
49
|
export class DatabaseResolver {
|
|
50
50
|
static resolveDatabase(constant: ConstantModel, database: DatabaseModel): AdaptorCls<DatabaseInstance> {
|
|
51
51
|
const [modelName, className]: [string, string] = [database.refName, capitalize(database.refName)];
|
|
52
|
+
|
|
53
|
+
const resolveSort = (sortKey?: string | null) =>
|
|
54
|
+
sortKey ? (getFilterSortByKey(database.filter, sortKey) as { [key: string]: 1 | -1 }) : null;
|
|
52
55
|
const getListQuery = (query?: QueryOf<any>, queryOption?: ListQueryOption) => {
|
|
53
56
|
const find = query ?? {};
|
|
54
|
-
const sort =
|
|
55
|
-
[key: string]: 1 | -1;
|
|
56
|
-
};
|
|
57
|
+
const sort = resolveSort(queryOption?.sort);
|
|
57
58
|
const skip = Number(queryOption?.skip ?? 0);
|
|
58
59
|
const limit = queryOption?.limit === null ? DEFAULT_PAGE_SIZE : Number(queryOption?.limit ?? 0);
|
|
59
60
|
const select = queryOption?.select;
|
|
@@ -62,9 +63,7 @@ export class DatabaseResolver {
|
|
|
62
63
|
};
|
|
63
64
|
const getFindQuery = (query?: QueryOf<any>, queryOption?: FindQueryOption) => {
|
|
64
65
|
const find = query ?? {};
|
|
65
|
-
const sort =
|
|
66
|
-
[key: string]: 1 | -1;
|
|
67
|
-
};
|
|
66
|
+
const sort = resolveSort(queryOption?.sort);
|
|
68
67
|
const skip = Number(queryOption?.skip ?? 0);
|
|
69
68
|
const select = queryOption?.select;
|
|
70
69
|
const sample = queryOption?.sample ?? false;
|