akanjs 2.4.2-rc.0 → 2.4.2-rc.1

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.
Files changed (38) hide show
  1. package/constant/fieldInfo.ts +4 -3
  2. package/constant/index.ts +2 -0
  3. package/constant/textFieldPathSet.ts +8 -0
  4. package/constant/textFieldPaths.ts +59 -0
  5. package/constant/types.ts +0 -4
  6. package/constant/via.ts +13 -28
  7. package/dictionary/dictInfo.ts +4 -0
  8. package/document/documentQuery.ts +17 -1
  9. package/document/documentSchema.ts +1 -15
  10. package/document/filterMeta.ts +4 -2
  11. package/local/apps/serverLifecycle/serverLifecycle-local.db +0 -0
  12. package/local/apps/serverLifecycle/serverLifecycle-local.db-shm +0 -0
  13. package/local/apps/serverLifecycle/serverLifecycle-local.db-wal +0 -0
  14. package/package.json +1 -1
  15. package/server/devtools/types.ts +2 -2
  16. package/server/resolver/database.resolver.ts +5 -6
  17. package/service/predefinedAdaptor/database.adaptor.ts +212 -88
  18. package/service/predefinedAdaptor/index.ts +1 -0
  19. package/service/predefinedAdaptor/searchIndex.ts +517 -0
  20. package/service/predefinedAdaptor/sqlDescriptor.ts +25 -0
  21. package/types/constant/fieldInfo.d.ts +4 -3
  22. package/types/constant/index.d.ts +2 -0
  23. package/types/constant/textFieldPathSet.d.ts +8 -0
  24. package/types/constant/textFieldPaths.d.ts +10 -0
  25. package/types/constant/types.d.ts +0 -3
  26. package/types/constant/via.d.ts +4 -24
  27. package/types/dictionary/base.dictionary.d.ts +1 -1
  28. package/types/dictionary/dictionary.d.ts +8 -8
  29. package/types/document/documentQuery.d.ts +13 -1
  30. package/types/document/documentSchema.d.ts +0 -3
  31. package/types/document/filterMeta.d.ts +2 -1
  32. package/types/server/devtools/types.d.ts +2 -2
  33. package/types/service/predefinedAdaptor/database.adaptor.d.ts +46 -13
  34. package/types/service/predefinedAdaptor/index.d.ts +1 -0
  35. package/types/service/predefinedAdaptor/searchIndex.d.ts +73 -0
  36. package/types/service/predefinedAdaptor/sqlDescriptor.d.ts +5 -0
  37. package/types/ui/Constant/schemaDoc.d.ts +2 -2
  38. package/ui/Constant/schemaDoc.ts +8 -2
@@ -17,6 +17,7 @@ import {
17
17
  type UnCls,
18
18
  } from "akanjs/base";
19
19
  import { ConstantRegistry } from "./constantRegistry";
20
+ import type { TextFieldRole } from "./textFieldPaths";
20
21
  import type { BaseObject } from "./types";
21
22
  import type { ConstantModelRef } from "./via";
22
23
 
@@ -102,7 +103,7 @@ export interface ConstantFieldProps<
102
103
  example?: FieldValue;
103
104
  of?: MapValue;
104
105
  validate?: (value: FieldValue, model: any) => boolean;
105
- text?: "search" | "filter";
106
+ text?: TextFieldRole;
106
107
  meta?: Metadata;
107
108
  }
108
109
  export const fieldPresets = ["email", "password", "url"] as const;
@@ -192,7 +193,7 @@ interface ConstantFieldBuildProps<
192
193
  example?: FieldValue;
193
194
  of?: MapValue;
194
195
  validate?: (value: FieldValue, model: any) => boolean;
195
- text?: "search" | "filter";
196
+ text?: TextFieldRole;
196
197
  modelRef: ConstantModelRef;
197
198
  arrDepth: number;
198
199
  optArrDepth: number;
@@ -315,7 +316,7 @@ export class ConstantField<
315
316
  readonly example?: FieldValue;
316
317
  readonly of?: MapValue;
317
318
  readonly validate?: (value: FieldValue, model: any) => boolean;
318
- readonly text?: "search" | "filter";
319
+ readonly text?: TextFieldRole;
319
320
  readonly modelRef: ConstantModelRef;
320
321
  readonly arrDepth: number;
321
322
  readonly optArrDepth: number;
package/constant/index.ts CHANGED
@@ -6,5 +6,7 @@ export * from "./getDefault";
6
6
  export * from "./immerify";
7
7
  export * from "./purify";
8
8
  export * from "./serialize";
9
+ export * from "./textFieldPathSet";
10
+ export * from "./textFieldPaths";
9
11
  export * from "./types";
10
12
  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
@@ -26,6 +26,7 @@ import {
26
26
  resolve,
27
27
  } from "./fieldInfo";
28
28
  import { makePurify, type PurifiedModel, type PurifyFunc } from "./purify";
29
+ import { TextFieldPaths } from "./textFieldPaths";
29
30
  import type { BaseInsight, BaseObject, ConstantType, DefaultOf, DefaultOfSchema, NonFunctionalKeys } from "./types";
30
31
 
31
32
  type BaseFields = "id" | "createdAt" | "updatedAt" | "removedAt";
@@ -191,8 +192,7 @@ const getBaseConstantClass = (field: FieldObject, modelType: ConstantType = "sca
191
192
  class BaseConstant {
192
193
  static readonly [FIELD_META]: FieldObject = field;
193
194
  static modelType: ConstantType = modelType;
194
- static text: { search: Set<string>; filter: Set<string>; children: { search: Set<string>; filter: Set<string> } } =
195
- { search: new Set(), filter: new Set(), children: { search: new Set(), filter: new Set() } };
195
+ static text: TextFieldPaths = new TextFieldPaths();
196
196
  static children: Set<ConstantModelRef> = new Set();
197
197
  static relations: Set<ConstantModelRef> = new Set();
198
198
  static enums: Set<EnumInstance> = new Set();
@@ -268,7 +268,7 @@ export interface ConstantStatics<
268
268
  children: Set<ConstantModelRef>;
269
269
  relations: Set<ConstantModelRef>;
270
270
  enums: Set<EnumInstance>;
271
- text: { search: Set<string>; filter: Set<string>; children: { search: Set<string>; filter: Set<string> } };
271
+ text: TextFieldPaths;
272
272
  _OptionalKey: OptionalKey;
273
273
  _RelationKey: RelationKey;
274
274
  _PrimitiveKey: PrimitiveKey;
@@ -295,7 +295,7 @@ export interface DatabaseConstantStatics<Schema = any, FieldObj extends FieldObj
295
295
  children: Set<ConstantModelRef>;
296
296
  relations: Set<ConstantModelRef>;
297
297
  enums: Set<EnumInstance>;
298
- text: { search: Set<string>; filter: Set<string>; children: { search: Set<string>; filter: Set<string> } };
298
+ text: TextFieldPaths;
299
299
  _DatabaseSchema: {
300
300
  [K in keyof Schema]: K extends keyof FieldObj
301
301
  ? FieldObj[K]["fieldType"] extends "hidden"
@@ -317,7 +317,7 @@ export type ConstantModelRef<
317
317
  children: Set<ConstantModelRef>;
318
318
  relations: Set<ConstantModelRef>;
319
319
  enums: Set<EnumInstance>;
320
- text: { search: Set<string>; filter: Set<string>; children: { search: Set<string>; filter: Set<string> } };
320
+ text: TextFieldPaths;
321
321
  }
322
322
  >;
323
323
 
@@ -430,31 +430,16 @@ const applyConstantStatics = <Model>(model: ConstantCls<Model>, fieldMap: FieldO
430
430
  purify: makePurify(model),
431
431
  getDefault: () => ({ ...defaultValue }),
432
432
  });
433
- Object.entries(fieldMap).forEach(([key, field]) => {
433
+ Object.entries(fieldMap).forEach(([, field]) => {
434
434
  if (field.enum) model.enums.add(field.enum);
435
- if (field.text === "search") model.text.search.add(key);
436
- else if (field.text === "filter") model.text.filter.add(key);
437
- else if (field.isClass) {
438
- if (field.isScalar) model.children.add(field.modelRef);
439
- else model.relations.add(field.modelRef);
440
- for (const child of field.modelRef.children) model.children.add(child);
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
- }
435
+ if (!field.isClass) return;
436
+ if (field.isScalar) model.children.add(field.modelRef);
437
+ else model.relations.add(field.modelRef);
438
+ for (const child of field.modelRef.children) model.children.add(child);
439
+ for (const childEnum of field.modelRef.enums) model.enums.add(childEnum);
440
+ for (const relation of field.modelRef.relations) model.relations.add(relation);
457
441
  });
442
+ model.text.collect(fieldMap);
458
443
  return model as unknown as ConstantCls<Model>;
459
444
  };
460
445
 
@@ -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;
@@ -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
  );
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "akanjs",
3
- "version": "2.4.2-rc.0",
3
+ "version": "2.4.2-rc.1",
4
4
  "sourceType": "module",
5
5
  "type": "module",
6
6
  "publishConfig": {
@@ -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?: "search" | "filter";
76
+ text?: TextFieldRole;
77
77
  accumulate?: unknown;
78
78
  example?: unknown;
79
79
  meta?: Record<string, unknown>;
@@ -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 = getFilterSortByKey(database.filter, queryOption?.sort ?? "latest") as {
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 = getFilterSortByKey(database.filter, queryOption?.sort ?? "latest") as {
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;