akanjs 3.0.0-alpha.1 → 3.0.0-alpha.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/base/symbols.ts +1 -0
- package/constant/cascadePaths.ts +79 -8
- package/document/database.ts +23 -1
- package/document/databaseRegistry.ts +2 -2
- package/document/filterMeta.ts +7 -0
- package/document/into.ts +15 -7
- package/package.json +1 -1
- package/server/di/diLifecycle.ts +8 -10
- package/server/resolver/CascadeRunner.ts +201 -0
- package/server/resolver/database.resolver.ts +57 -4
- package/server/resolver/index.ts +1 -0
- package/server/resolver/service.resolver.ts +38 -25
- package/service/predefinedAdaptor/database.adaptor.ts +11 -3
- package/service/serve.ts +5 -1
- package/service/serviceModule.ts +17 -0
- package/service/types.ts +6 -0
- package/types/base/symbols.d.ts +1 -0
- package/types/constant/cascadePaths.d.ts +22 -4
- package/types/document/database.d.ts +22 -1
- package/types/document/databaseRegistry.d.ts +2 -2
- package/types/document/filterMeta.d.ts +1 -0
- package/types/document/into.d.ts +10 -6
- package/types/server/resolver/CascadeRunner.d.ts +13 -0
- package/types/server/resolver/database.resolver.d.ts +6 -2
- package/types/server/resolver/index.d.ts +1 -0
- package/types/server/resolver/service.resolver.d.ts +3 -3
- package/types/service/predefinedAdaptor/database.adaptor.d.ts +14 -2
- package/types/service/types.d.ts +5 -1
package/base/symbols.ts
CHANGED
|
@@ -3,6 +3,7 @@ export const SLICE_META = Symbol.for("akan.slice");
|
|
|
3
3
|
export const FILTER_META = Symbol.for("akan.filter");
|
|
4
4
|
export const LOADER_META = Symbol.for("akan.loader");
|
|
5
5
|
export const INJECT_META = Symbol.for("akan.inject");
|
|
6
|
+
export const LIBS_REMOVE_HOOK = Symbol.for("akan.service.libsRemoveHook");
|
|
6
7
|
export const ENDPOINT_META = Symbol.for("akan.endpoint");
|
|
7
8
|
export const ENDPOINT_DICT_SHAPE: unique symbol = Symbol.for("akan.endpoint.dictShape") as never;
|
|
8
9
|
export const SLICE_DICT_SHAPE: unique symbol = Symbol.for("akan.slice.dictShape") as never;
|
package/constant/cascadePaths.ts
CHANGED
|
@@ -1,32 +1,103 @@
|
|
|
1
|
+
import { type Cls, PrimitiveRegistry, type PrimitiveScalar } from "akanjs/base";
|
|
1
2
|
import type { ConstantField, FieldObject } from "./fieldInfo";
|
|
2
3
|
import type { ConstantModelRef } from "./via";
|
|
3
4
|
|
|
4
|
-
/**
|
|
5
|
-
|
|
5
|
+
/**
|
|
6
|
+
* Which end of a relation goes away with the other. `removeRef` removes what the field points at when this
|
|
7
|
+
* document is removed; `removeWith` removes this document when what the field points at is removed. The two
|
|
8
|
+
* read identically on a relation field, so the value has to name the direction — a mistake here is a data loss.
|
|
9
|
+
*/
|
|
10
|
+
export const cascadeActions = ["removeRef", "removeWith"] as const;
|
|
6
11
|
export type CascadeAction = (typeof cascadeActions)[number];
|
|
7
12
|
|
|
13
|
+
/** How a `removeWith` field names the owner whose removal takes this document with it. */
|
|
14
|
+
export interface CascadeWithPath {
|
|
15
|
+
readonly key: string;
|
|
16
|
+
/** Set when the field is a relation. Resolved to a refName later: the owner may not be registered yet. */
|
|
17
|
+
readonly modelRef: ConstantModelRef | null;
|
|
18
|
+
/** Set when the field declares `ref`, which names the owner at declaration. */
|
|
19
|
+
readonly refName: string | null;
|
|
20
|
+
/** Set when the field declares `refPath`: the sibling field holding the owner's refName. */
|
|
21
|
+
readonly typeKey: string | null;
|
|
22
|
+
/** The refNames `typeKey` may hold. Empty unless the field is polymorphic. */
|
|
23
|
+
readonly typeValues: readonly string[];
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const idNames = new Set(["ID", "String"]);
|
|
27
|
+
|
|
8
28
|
export class CascadePaths {
|
|
9
|
-
/** Field key → the model its ids point at
|
|
10
|
-
readonly
|
|
29
|
+
/** Field key → the model its ids point at, removed when this document is. */
|
|
30
|
+
readonly removeRef = new Map<string, ConstantModelRef>();
|
|
31
|
+
/** Field key → the owner whose removal removes this document. */
|
|
32
|
+
readonly removeWith = new Map<string, CascadeWithPath>();
|
|
11
33
|
|
|
12
34
|
collect(fieldMap: FieldObject) {
|
|
13
35
|
for (const [key, field] of Object.entries(fieldMap)) {
|
|
14
36
|
if (!field.cascade) continue;
|
|
15
|
-
this.#
|
|
16
|
-
this.
|
|
37
|
+
this.#assertKnownAction(key, field.cascade);
|
|
38
|
+
if (field.cascade === "removeRef") this.removeRef.set(key, this.#readOwnedRelation(key, field));
|
|
39
|
+
else this.removeWith.set(key, this.#readOwnerPath(key, field, fieldMap));
|
|
17
40
|
}
|
|
18
41
|
return this;
|
|
19
42
|
}
|
|
20
43
|
|
|
21
|
-
#
|
|
22
|
-
|
|
44
|
+
#assertKnownAction(key: string, action: CascadeAction) {
|
|
45
|
+
|
|
23
46
|
if (!cascadeActions.includes(action)) {
|
|
24
47
|
throw new Error(`Cascade field "${key}" declares cascade: "${action}", which is not one of ${cascadeActions}`);
|
|
25
48
|
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
#readOwnedRelation(key: string, field: ConstantField) {
|
|
26
52
|
|
|
27
53
|
if (!field.isClass || field.isScalar) {
|
|
28
54
|
throw new Error(`Cascade field "${key}" is not a model reference and has no document to remove`);
|
|
29
55
|
}
|
|
30
56
|
if (field.arrDepth > 1) throw new Error(`Cascade field "${key}" is a nested array and cannot cascade`);
|
|
57
|
+
return field.modelRef;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
#readOwnerPath(key: string, field: ConstantField, fieldMap: FieldObject): CascadeWithPath {
|
|
61
|
+
|
|
62
|
+
if (field.arrDepth > 0) throw new Error(`Cascade field "${key}" is an array and names more than one owner`);
|
|
63
|
+
if (field.isMap) throw new Error(`Cascade field "${key}" is a Map and names no owner`);
|
|
64
|
+
if (field.refPath) return this.#readPolymorphicOwner(key, field, fieldMap);
|
|
65
|
+
if (field.ref) {
|
|
66
|
+
this.#assertHoldsId(key, field);
|
|
67
|
+
return { key, modelRef: null, refName: field.ref, typeKey: null, typeValues: [] };
|
|
68
|
+
}
|
|
69
|
+
if (field.isClass && !field.isScalar) {
|
|
70
|
+
return { key, modelRef: field.modelRef, refName: null, typeKey: null, typeValues: [] };
|
|
71
|
+
}
|
|
72
|
+
throw new Error(
|
|
73
|
+
`Cascade field "${key}" declares cascade: "removeWith" but names no owner; make it a model reference, ` +
|
|
74
|
+
`or add ref: "<model>" / refPath: "<typeField>"`,
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
#readPolymorphicOwner(key: string, field: ConstantField, fieldMap: FieldObject): CascadeWithPath {
|
|
79
|
+
if (field.ref) throw new Error(`Cascade field "${key}" declares both ref and refPath; keep one`);
|
|
80
|
+
this.#assertHoldsId(key, field);
|
|
81
|
+
const typeKey = field.refPath as string;
|
|
82
|
+
const typeField = fieldMap[typeKey];
|
|
83
|
+
if (!typeField) throw new Error(`Cascade field "${key}" declares refPath: "${typeKey}", which is not a field`);
|
|
84
|
+
|
|
85
|
+
if (!typeField.enum) {
|
|
86
|
+
throw new Error(
|
|
87
|
+
`Cascade field "${key}" declares refPath: "${typeKey}", which must be an enumOf(...) naming the owner ` +
|
|
88
|
+
`refNames it may hold`,
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
const typeValues = typeField.enum.values.map((value) => String(value));
|
|
92
|
+
return { key, modelRef: null, refName: null, typeKey, typeValues };
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
#assertHoldsId(key: string, field: ConstantField) {
|
|
96
|
+
const modelRef = field.modelRef as unknown as Cls;
|
|
97
|
+
const refName = PrimitiveRegistry.has(modelRef)
|
|
98
|
+
? PrimitiveRegistry.getName(modelRef as unknown as typeof PrimitiveScalar)
|
|
99
|
+
: null;
|
|
100
|
+
if (refName && idNames.has(refName)) return;
|
|
101
|
+
throw new Error(`Cascade field "${key}" declares ref or refPath and must hold an ID`);
|
|
31
102
|
}
|
|
32
103
|
}
|
package/document/database.ts
CHANGED
|
@@ -3,8 +3,9 @@ import { Logger } from "akanjs/common";
|
|
|
3
3
|
import type { DocumentModel, QueryOf } from "akanjs/constant";
|
|
4
4
|
import type { CacheAdaptor, CacheSetOptions } from "akanjs/service";
|
|
5
5
|
import type { DataLoader } from "./dataLoader";
|
|
6
|
+
import type { DocumentUpdateInput } from "./documentQuery";
|
|
6
7
|
import type { ExtractQuery, ExtractSort, FilterInstance } from "./filterMeta";
|
|
7
|
-
import type { CRUDEventType, Mdl, SaveEventType } from "./into";
|
|
8
|
+
import type { CRUDEventType, Mdl, SaveEventType, UpdateResult } from "./into";
|
|
8
9
|
import type { DataInputOf, FindQueryOption, ListQueryOption } from "./types";
|
|
9
10
|
|
|
10
11
|
export class CacheDatabase<T = unknown> {
|
|
@@ -25,6 +26,15 @@ export class CacheDatabase<T = unknown> {
|
|
|
25
26
|
await this.cache.delete(this.refName, `${topic}:${key}`);
|
|
26
27
|
}
|
|
27
28
|
}
|
|
29
|
+
/**
|
|
30
|
+
* What `update<Filter>` returns. The patch cannot be a trailing parameter — a filter's own args may be optional,
|
|
31
|
+
* and no tuple type puts a required element after those — and leading it reads backwards. So it lands here, on a
|
|
32
|
+
* terminal `.set()` that mirrors the `UPDATE … SET …` it compiles to.
|
|
33
|
+
*/
|
|
34
|
+
export interface UpdateChain<Doc = any> {
|
|
35
|
+
set(update: DocumentUpdateInput<Doc>): Promise<UpdateResult>;
|
|
36
|
+
}
|
|
37
|
+
|
|
28
38
|
type QueryMethodOfKey<
|
|
29
39
|
CapitalizedK extends string,
|
|
30
40
|
Doc,
|
|
@@ -53,6 +63,14 @@ type QueryMethodOfKey<
|
|
|
53
63
|
[K in `insight${CapitalizedK}`]: (...args: _Args) => Promise<Insight>;
|
|
54
64
|
} & {
|
|
55
65
|
[K in `query${CapitalizedK}`]: (...args: _Args) => _QueryOfDoc;
|
|
66
|
+
} & {
|
|
67
|
+
[K in `remove${CapitalizedK}`]: (...args: _Args) => Promise<UpdateResult>;
|
|
68
|
+
} & {
|
|
69
|
+
[K in `removeOne${CapitalizedK}`]: (...args: _Args) => Promise<UpdateResult>;
|
|
70
|
+
} & {
|
|
71
|
+
[K in `update${CapitalizedK}`]: (...args: _Args) => UpdateChain<Doc>;
|
|
72
|
+
} & {
|
|
73
|
+
[K in `updateOne${CapitalizedK}`]: (...args: _Args) => UpdateChain<Doc>;
|
|
56
74
|
};
|
|
57
75
|
type QueryMethodMap<Query, Doc, Insight, _FindQueryOption, _ListQueryOption, _QueryOfDoc> = {
|
|
58
76
|
[K in keyof Query]: K extends string
|
|
@@ -105,6 +123,10 @@ type DatabaseModelWithQuerySort<
|
|
|
105
123
|
__create: (data: _DataInput) => Promise<Doc>;
|
|
106
124
|
__update: (id: string, data: Partial<Doc>) => Promise<Doc>;
|
|
107
125
|
__remove: (id: string) => Promise<Doc>;
|
|
126
|
+
__removeMany: (query: _QueryOfDoc) => Promise<UpdateResult>;
|
|
127
|
+
__removeOne: (query: _QueryOfDoc) => Promise<UpdateResult>;
|
|
128
|
+
__updateMany: (query: _QueryOfDoc, update: DocumentUpdateInput<Doc>) => Promise<UpdateResult>;
|
|
129
|
+
__updateOne: (query: _QueryOfDoc, update: DocumentUpdateInput<Doc>) => Promise<UpdateResult>;
|
|
108
130
|
__list(query: _QueryOfDoc, queryOption?: _ListQueryOption): Promise<Doc[]>;
|
|
109
131
|
__listIds(query: _QueryOfDoc, queryOption?: _ListQueryOption): Promise<string[]>;
|
|
110
132
|
__find(query: _QueryOfDoc, queryOption?: _FindQueryOption): Promise<Doc | null>;
|
|
@@ -19,7 +19,7 @@ export interface DatabaseModel<
|
|
|
19
19
|
model: ModelCls<Model>;
|
|
20
20
|
filter: FilterCls<Filter>;
|
|
21
21
|
obj: ConstantCls<Obj>;
|
|
22
|
-
insight:
|
|
22
|
+
insight: DatabaseCls<Insight>;
|
|
23
23
|
_Input: Input;
|
|
24
24
|
_Doc: Doc;
|
|
25
25
|
_Model: Model;
|
|
@@ -92,7 +92,7 @@ export class DatabaseRegistry {
|
|
|
92
92
|
doc: DatabaseCls<Doc>,
|
|
93
93
|
model: ModelCls<Model>,
|
|
94
94
|
obj: ConstantCls<Obj>,
|
|
95
|
-
insight:
|
|
95
|
+
insight: DatabaseCls<Insight>,
|
|
96
96
|
filter: FilterCls<Filter>,
|
|
97
97
|
): DatabaseModel<T, Input, Doc, Model, Obj, Insight, Filter, _Query, _Sort> {
|
|
98
98
|
const dbInfo = {
|
package/document/filterMeta.ts
CHANGED
|
@@ -98,6 +98,13 @@ 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 const assertFilterFitsCrud = (refName: string, queryKey: string, className: string) => {
|
|
102
|
+
if (queryKey.toLowerCase() !== refName.toLowerCase()) return;
|
|
103
|
+
throw new Error(
|
|
104
|
+
`Filter "${queryKey}" on "${refName}" generates remove${className}/update${className}, which are the generated CRUD methods; rename the filter`,
|
|
105
|
+
);
|
|
106
|
+
};
|
|
107
|
+
|
|
101
108
|
export type BaseFilterSortKey = "latest" | "oldest" | "relevance";
|
|
102
109
|
export type BaseFilterQueryKey = "any";
|
|
103
110
|
export type BaseFilterKey = BaseFilterSortKey | BaseFilterQueryKey;
|
package/document/into.ts
CHANGED
|
@@ -77,22 +77,27 @@ export type Mdl<
|
|
|
77
77
|
find(query: _RawQuery, projection?: _Projection): FindManyChain<Doc>;
|
|
78
78
|
findOne(query: _RawQuery, projection?: _Projection): FindOneChain<Doc>;
|
|
79
79
|
findById(id: string | undefined, projection?: _Projection): Promise<Doc | null>;
|
|
80
|
-
|
|
80
|
+
count(query: _RawQuery): Promise<number>;
|
|
81
81
|
exists(query: _RawQuery): Promise<string | null>;
|
|
82
|
+
|
|
82
83
|
updateOne(
|
|
83
84
|
query: _RawQuery,
|
|
84
85
|
update: DocumentUpdateInput<_RawDoc>,
|
|
85
86
|
options?: DocumentUpdateOptions,
|
|
86
87
|
): Promise<UpdateResult>;
|
|
87
88
|
updateMany(query: _RawQuery, update: DocumentUpdateInput<_RawDoc>): Promise<UpdateResult>;
|
|
88
|
-
|
|
89
|
+
removeOne(query: _RawQuery): Promise<UpdateResult>;
|
|
90
|
+
removeMany(query: _RawQuery): Promise<UpdateResult>;
|
|
89
91
|
bulkWrite(operations: BulkWriteOperation<Raw, _RawDoc, _RawQuery>[]): Promise<UpdateResult>;
|
|
92
|
+
/** @deprecated Renamed to `count`. */
|
|
93
|
+
countDocuments(query: _RawQuery): Promise<number>;
|
|
90
94
|
};
|
|
91
95
|
|
|
92
|
-
interface IntoConstantModel<T extends string, _CapitalizedRefName extends string, Raw> {
|
|
96
|
+
interface IntoConstantModel<T extends string, _CapitalizedRefName extends string, Raw, Insight> {
|
|
93
97
|
refName: T;
|
|
94
98
|
_CapitalizedRefName: _CapitalizedRefName;
|
|
95
99
|
_Full: Raw;
|
|
100
|
+
_Insight: Insight;
|
|
96
101
|
}
|
|
97
102
|
type NoInferType<T> = [T][T extends unknown ? 0 : never];
|
|
98
103
|
type IntoModelActions<
|
|
@@ -100,6 +105,8 @@ type IntoModelActions<
|
|
|
100
105
|
_CapitalizedRefName extends string,
|
|
101
106
|
Doc,
|
|
102
107
|
Raw,
|
|
108
|
+
|
|
109
|
+
Insight,
|
|
103
110
|
_Query,
|
|
104
111
|
_Sort,
|
|
105
112
|
_QueryOfDoc = QueryOf<Doc>,
|
|
@@ -121,13 +128,14 @@ type IntoModelActions<
|
|
|
121
128
|
[K in `update${_CapitalizedRefName}`]: (id: string, data: Partial<Doc>) => Promise<Doc>;
|
|
122
129
|
} & {
|
|
123
130
|
[K in `remove${_CapitalizedRefName}`]: (id: string) => Promise<Doc>;
|
|
124
|
-
} & QueryMethodPart<_Query, _Sort, Raw, Doc,
|
|
131
|
+
} & QueryMethodPart<_Query, _Sort, Raw, Doc, DocumentModel<Insight>, unknown, unknown, _QueryOfDoc>;
|
|
125
132
|
|
|
126
133
|
export const into = <
|
|
127
134
|
Doc,
|
|
128
135
|
FilterRef extends FilterCls,
|
|
129
136
|
T extends string,
|
|
130
137
|
Raw,
|
|
138
|
+
Insight,
|
|
131
139
|
AddDbModels extends ModelCls[],
|
|
132
140
|
_CapitalizedRefName extends string,
|
|
133
141
|
_QueryOfDoc = QueryOf<Doc>,
|
|
@@ -137,11 +145,11 @@ export const into = <
|
|
|
137
145
|
>(
|
|
138
146
|
docRef: Cls<Doc>,
|
|
139
147
|
filterRef: FilterRef,
|
|
140
|
-
cnst: IntoConstantModel<T, _CapitalizedRefName, Raw>,
|
|
148
|
+
cnst: IntoConstantModel<T, _CapitalizedRefName, Raw, Insight>,
|
|
141
149
|
loaderBuilder: _LoaderBuilder,
|
|
142
150
|
...addMdls: [...AddDbModels]
|
|
143
151
|
): ModelCls<
|
|
144
|
-
IntoModelActions<T, _CapitalizedRefName, Doc, Raw, _Query, _Sort, _QueryOfDoc>,
|
|
152
|
+
IntoModelActions<T, _CapitalizedRefName, Doc, Raw, Insight, _Query, _Sort, _QueryOfDoc>,
|
|
145
153
|
ReturnType<_LoaderBuilder>
|
|
146
154
|
> => {
|
|
147
155
|
const loaderInfoMap = loaderBuilder(makeLoaderBuilder<Doc>());
|
|
@@ -162,7 +170,7 @@ export const into = <
|
|
|
162
170
|
});
|
|
163
171
|
});
|
|
164
172
|
return DefaultModel as unknown as ModelCls<
|
|
165
|
-
IntoModelActions<T, _CapitalizedRefName, Doc, Raw, _Query, _Sort, _QueryOfDoc>,
|
|
173
|
+
IntoModelActions<T, _CapitalizedRefName, Doc, Raw, Insight, _Query, _Sort, _QueryOfDoc>,
|
|
166
174
|
ReturnType<_LoaderBuilder>
|
|
167
175
|
>;
|
|
168
176
|
};
|
package/package.json
CHANGED
package/server/di/diLifecycle.ts
CHANGED
|
@@ -21,7 +21,7 @@ import { SignalRegistry } from "../../signal/signalRegistry";
|
|
|
21
21
|
import type { AkanLib, DatabaseModule, ScalarModule, ServiceModule } from "../akanLib";
|
|
22
22
|
import { createDefaultAkanOption } from "../akanOption";
|
|
23
23
|
import type { WebProxyRegistration } from "../proxy";
|
|
24
|
-
import { DatabaseResolver, ServiceResolver, SignalResolver } from "../resolver";
|
|
24
|
+
import { CascadeRunner, DatabaseResolver, ServiceResolver, SignalResolver } from "../resolver";
|
|
25
25
|
import type { SignalRoutes, WebsocketRoutes } from "../types";
|
|
26
26
|
import { getPredefinedAdaptor, predefinedAdaptorRole } from "./predefinedAdaptor";
|
|
27
27
|
import { collectAdaptors, resolveAdaptorHierarchy } from "./resolveAdaptorHierarchy";
|
|
@@ -63,6 +63,7 @@ export class DiLifecycle {
|
|
|
63
63
|
readonly disabledModules = new Map<string, string>();
|
|
64
64
|
readonly #predefinedAdaptor;
|
|
65
65
|
readonly #predefinedAdaptorRole = predefinedAdaptorRole;
|
|
66
|
+
readonly #cascade = new CascadeRunner();
|
|
66
67
|
|
|
67
68
|
/** Read-only view of the resolved module maps, for tooling that needs to describe the container. */
|
|
68
69
|
get modules(): {
|
|
@@ -122,8 +123,9 @@ export class DiLifecycle {
|
|
|
122
123
|
this.#service.set(refName, module as ServiceModule);
|
|
123
124
|
});
|
|
124
125
|
this.#database.forEach((mod) => {
|
|
125
|
-
const
|
|
126
|
-
this.#adaptor.set(
|
|
126
|
+
const { adaptor, schema } = DatabaseResolver.resolveDatabase(mod.constant, mod.database);
|
|
127
|
+
this.#adaptor.set(adaptor.refName, adaptor);
|
|
128
|
+
this.#cascade.register(mod.constant, schema, mod.service.srv);
|
|
127
129
|
});
|
|
128
130
|
const services = [
|
|
129
131
|
...[...this.#service.values()].map((mod) => mod.service.srv),
|
|
@@ -457,13 +459,7 @@ export class DiLifecycle {
|
|
|
457
459
|
if (serviceCls.type === "database") {
|
|
458
460
|
const databaseModule = this.#database.get(serviceCls.refName);
|
|
459
461
|
if (!databaseModule) throw new Error(`Database "${serviceCls.refName}" is not registered`);
|
|
460
|
-
ServiceResolver.resolveDatabaseService(
|
|
461
|
-
databaseModule.constant,
|
|
462
|
-
databaseModule.database,
|
|
463
|
-
serviceCls,
|
|
464
|
-
|
|
465
|
-
(refName) => this.getService(refName),
|
|
466
|
-
);
|
|
462
|
+
ServiceResolver.resolveDatabaseService(databaseModule.database, serviceCls, this.#cascade);
|
|
467
463
|
}
|
|
468
464
|
const service = new serviceCls();
|
|
469
465
|
await InjectInfo.resolveInjection(service, serviceCls, this.registry, this.#env);
|
|
@@ -476,6 +472,8 @@ export class DiLifecycle {
|
|
|
476
472
|
})),
|
|
477
473
|
);
|
|
478
474
|
}
|
|
475
|
+
|
|
476
|
+
this.#cascade.seal((refName) => this.getService(refName));
|
|
479
477
|
}
|
|
480
478
|
|
|
481
479
|
async #initializeInternal() {
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
import { AsyncLocalStorage } from "node:async_hooks";
|
|
2
|
+
import { LIBS_REMOVE_HOOK } from "akanjs/base";
|
|
3
|
+
import { Logger } from "akanjs/common";
|
|
4
|
+
import { type CascadeWithPath, type ConstantModel, ConstantRegistry } from "akanjs/constant";
|
|
5
|
+
import { type DocumentSchema, documentQueryHelper } from "akanjs/document";
|
|
6
|
+
import type { DatabaseService, ServiceCls } from "akanjs/service";
|
|
7
|
+
|
|
8
|
+
/** A relation the removed document owns: the ids it holds name documents to remove. */
|
|
9
|
+
interface RefEdge {
|
|
10
|
+
readonly key: string;
|
|
11
|
+
readonly refName: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/** A model that declared itself removable with this one: its rows name the removed document as their owner. */
|
|
15
|
+
interface WithEdge {
|
|
16
|
+
readonly refName: string;
|
|
17
|
+
readonly key: string;
|
|
18
|
+
readonly typeKey: string | null;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
interface CascadeModule {
|
|
22
|
+
readonly constant: ConstantModel;
|
|
23
|
+
readonly schema: DocumentSchema;
|
|
24
|
+
readonly srvRef: ServiceCls;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
interface CascadePlan {
|
|
28
|
+
readonly refEdges: RefEdge[];
|
|
29
|
+
readonly withEdges: WithEdge[];
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** One cascade in flight. Shared down the chain so a cycle is caught wherever it closes. */
|
|
33
|
+
interface CascadeContext {
|
|
34
|
+
readonly seen: Set<string>;
|
|
35
|
+
readonly depth: number;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** How deep one removal may cascade before the chain is treated as runaway and abandoned. */
|
|
39
|
+
const maxDepth = 16;
|
|
40
|
+
/** Ids taken per query while draining children one document at a time. */
|
|
41
|
+
const drainSize = 200;
|
|
42
|
+
|
|
43
|
+
export class CascadeRunner {
|
|
44
|
+
readonly #modules = new Map<string, CascadeModule>();
|
|
45
|
+
readonly #plans = new Map<string, CascadePlan>();
|
|
46
|
+
readonly #bulk = new Set<string>();
|
|
47
|
+
readonly #context = new AsyncLocalStorage<CascadeContext>();
|
|
48
|
+
readonly #logger = new Logger("Cascade");
|
|
49
|
+
#getService: ((refName: string) => DatabaseService) | null = null;
|
|
50
|
+
|
|
51
|
+
register(constant: ConstantModel, schema: DocumentSchema, srvRef: ServiceCls) {
|
|
52
|
+
this.#modules.set(constant.refName, { constant, schema, srvRef });
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Called once every service is live. A `_postRemove` and a `listenPost("remove")` registered during boot both
|
|
57
|
+
* count against bulk removal, so the strategy cannot be decided before the last service has initialized.
|
|
58
|
+
*/
|
|
59
|
+
seal(getService: (refName: string) => DatabaseService) {
|
|
60
|
+
this.#getService = getService;
|
|
61
|
+
for (const [refName, mod] of this.#modules) {
|
|
62
|
+
this.#plans.set(refName, { refEdges: this.#collectRefEdges(refName, mod), withEdges: [] });
|
|
63
|
+
}
|
|
64
|
+
for (const [refName, mod] of this.#modules) this.#collectWithEdges(refName, mod);
|
|
65
|
+
for (const refName of this.#modules.keys()) {
|
|
66
|
+
if (this.#hasRemoveSideEffect(refName)) continue;
|
|
67
|
+
this.#bulk.add(refName);
|
|
68
|
+
}
|
|
69
|
+
this.#report();
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
async run(refName: string, doc: Record<string, unknown>) {
|
|
73
|
+
const plan = this.#plans.get(refName);
|
|
74
|
+
if (!plan?.refEdges.length && !plan?.withEdges.length) return;
|
|
75
|
+
const parent = this.#context.getStore();
|
|
76
|
+
const seen = parent?.seen ?? new Set<string>();
|
|
77
|
+
const depth = (parent?.depth ?? 0) + 1;
|
|
78
|
+
const id = typeof doc.id === "string" ? doc.id : null;
|
|
79
|
+
if (id) seen.add(`${refName}:${id}`);
|
|
80
|
+
if (depth > maxDepth) {
|
|
81
|
+
this.#logger.error(`Cascade from ${refName} exceeded depth ${maxDepth} and was abandoned`);
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
await this.#context.run({ seen, depth }, async () => {
|
|
85
|
+
for (const edge of plan.refEdges) await this.#removeRef(edge, doc, seen);
|
|
86
|
+
if (id) for (const edge of plan.withEdges) await this.#removeWith(edge, refName, id, seen);
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
async #removeRef(edge: RefEdge, doc: Record<string, unknown>, seen: Set<string>) {
|
|
91
|
+
const value = doc[edge.key];
|
|
92
|
+
const ids = (Array.isArray(value) ? value : [value]).filter(
|
|
93
|
+
(item): item is string => typeof item === "string" && !seen.has(`${edge.refName}:${item}`),
|
|
94
|
+
);
|
|
95
|
+
if (!ids.length) return;
|
|
96
|
+
const service = this.#service(edge.refName);
|
|
97
|
+
if (this.#bulk.has(edge.refName)) {
|
|
98
|
+
await service.__removeMany({ id: documentQueryHelper.oneOf(ids) });
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
for (const id of ids) await service.__remove(id);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
async #removeWith(edge: WithEdge, ownerRef: string, ownerId: string, seen: Set<string>) {
|
|
106
|
+
const service = this.#service(edge.refName);
|
|
107
|
+
const query = edge.typeKey ? { [edge.key]: ownerId, [edge.typeKey]: ownerRef } : { [edge.key]: ownerId };
|
|
108
|
+
if (this.#bulk.has(edge.refName)) {
|
|
109
|
+
await service.__removeMany(query);
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
for (;;) {
|
|
114
|
+
const ids = await service.__listIds(query, { limit: drainSize });
|
|
115
|
+
if (!ids.length) return;
|
|
116
|
+
let removed = 0;
|
|
117
|
+
for (const id of ids) {
|
|
118
|
+
if (seen.has(`${edge.refName}:${id}`)) continue;
|
|
119
|
+
await service.__remove(id);
|
|
120
|
+
removed += 1;
|
|
121
|
+
}
|
|
122
|
+
if (!removed) return;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
#collectRefEdges(refName: string, mod: CascadeModule) {
|
|
127
|
+
return [...mod.constant.full.cascade.removeRef].map(([key, modelRef]) => {
|
|
128
|
+
const target = ConstantRegistry.getRefName(modelRef);
|
|
129
|
+
|
|
130
|
+
if (!this.#modules.has(target)) {
|
|
131
|
+
throw new Error(`Cascade field "${refName}.${key}" removes "${target}", which this app does not mount`);
|
|
132
|
+
}
|
|
133
|
+
return { key, refName: target };
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
#collectWithEdges(childRef: string, mod: CascadeModule) {
|
|
138
|
+
for (const [key, path] of mod.constant.full.cascade.removeWith) {
|
|
139
|
+
for (const owner of this.#resolveOwners(childRef, key, path)) {
|
|
140
|
+
this.#plans.get(owner)?.withEdges.push({ refName: childRef, key, typeKey: path.typeKey });
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
#resolveOwners(childRef: string, key: string, path: CascadeWithPath) {
|
|
146
|
+
if (path.typeValues.length) {
|
|
147
|
+
|
|
148
|
+
const mounted = path.typeValues.filter((owner) => this.#modules.has(owner));
|
|
149
|
+
for (const owner of path.typeValues) {
|
|
150
|
+
if (mounted.includes(owner)) continue;
|
|
151
|
+
this.#logger.warn(`Cascade field "${childRef}.${key}" names owner "${owner}", which this app does not mount`);
|
|
152
|
+
}
|
|
153
|
+
return mounted;
|
|
154
|
+
}
|
|
155
|
+
const owner = path.refName ?? ConstantRegistry.getRefName(path.modelRef as never);
|
|
156
|
+
if (!this.#modules.has(owner)) {
|
|
157
|
+
throw new Error(`Cascade field "${childRef}.${key}" is owned by "${owner}", which this app does not mount`);
|
|
158
|
+
}
|
|
159
|
+
return [owner];
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/** Everything a bulk `removeMany` would skip. All of it absent means the two paths leave the same rows behind. */
|
|
163
|
+
#hasRemoveSideEffect(refName: string) {
|
|
164
|
+
const mod = this.#modules.get(refName);
|
|
165
|
+
if (!mod) return true;
|
|
166
|
+
if (mod.schema.preHooks.get("remove")?.length || mod.schema.postHooks.get("remove")?.length) return true;
|
|
167
|
+
if ((mod.srvRef as unknown as { [LIBS_REMOVE_HOOK]?: boolean })[LIBS_REMOVE_HOOK]) return true;
|
|
168
|
+
const proto = mod.srvRef.prototype as { _preRemove?: unknown; _postRemove?: unknown };
|
|
169
|
+
if (typeof proto._preRemove === "function" || typeof proto._postRemove === "function") return true;
|
|
170
|
+
const plan = this.#plans.get(refName);
|
|
171
|
+
return !!plan?.refEdges.length || !!plan?.withEdges.length;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/** Neither the strategy nor the edge list is visible from the source, and adding a `_postRemove` to a target
|
|
175
|
+
* silently flips it from one query back to one per document. A quiet cascade is the one nobody can explain. */
|
|
176
|
+
#report() {
|
|
177
|
+
const lines: string[] = [];
|
|
178
|
+
for (const [refName, plan] of this.#plans) {
|
|
179
|
+
for (const edge of plan.refEdges) {
|
|
180
|
+
lines.push(`${refName}.${edge.key} removeRef ${edge.refName} (${this.#strategy(edge.refName)})`);
|
|
181
|
+
}
|
|
182
|
+
for (const edge of plan.withEdges) {
|
|
183
|
+
const path = edge.typeKey ? `${edge.key}+${edge.typeKey}` : edge.key;
|
|
184
|
+
lines.push(`${refName} removeWith ${edge.refName}.${path} (${this.#strategy(edge.refName)})`);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
if (!lines.length) return;
|
|
188
|
+
const bulk = lines.filter((line) => line.endsWith("(bulk)")).length;
|
|
189
|
+
this.#logger.info(`${lines.length} cascade edge(s), ${bulk} in one query`);
|
|
190
|
+
for (const line of lines) this.#logger.verbose(line);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
#strategy(refName: string) {
|
|
194
|
+
return this.#bulk.has(refName) ? "bulk" : "per document";
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
#service(refName: string) {
|
|
198
|
+
if (!this.#getService) throw new Error(`Cascade ran before the plan was sealed: ${refName}`);
|
|
199
|
+
return this.#getService(refName);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
@@ -2,6 +2,7 @@ import type { PromiseOrObject } from "akanjs/base";
|
|
|
2
2
|
import { applyMixins, capitalize } from "akanjs/common";
|
|
3
3
|
import { type ConstantModel, DEFAULT_PAGE_SIZE, type QueryOf } from "akanjs/constant";
|
|
4
4
|
import {
|
|
5
|
+
assertFilterFitsCrud,
|
|
5
6
|
CacheDatabase,
|
|
6
7
|
type CRUDEventType,
|
|
7
8
|
type DatabaseInstance,
|
|
@@ -20,6 +21,7 @@ import {
|
|
|
20
21
|
type ListQueryOption,
|
|
21
22
|
type Mdl,
|
|
22
23
|
type SaveEventType,
|
|
24
|
+
type UpdateChain,
|
|
23
25
|
} from "akanjs/document";
|
|
24
26
|
import {
|
|
25
27
|
type AdaptorCls,
|
|
@@ -47,7 +49,11 @@ const timedQuery = async <T>(fn: () => Promise<T>): Promise<T> => {
|
|
|
47
49
|
};
|
|
48
50
|
|
|
49
51
|
export class DatabaseResolver {
|
|
50
|
-
|
|
52
|
+
/** Returns the schema alongside the adaptor: the cascade planner reads its `remove` hooks to pick a strategy. */
|
|
53
|
+
static resolveDatabase(
|
|
54
|
+
constant: ConstantModel,
|
|
55
|
+
database: DatabaseModel,
|
|
56
|
+
): { adaptor: AdaptorCls<DatabaseInstance>; schema: DocumentSchema } {
|
|
51
57
|
const [modelName, className]: [string, string] = [database.refName, capitalize(database.refName)];
|
|
52
58
|
|
|
53
59
|
const resolveSort = (sortKey?: string | null) =>
|
|
@@ -85,6 +91,16 @@ export class DatabaseResolver {
|
|
|
85
91
|
schema.index(fields);
|
|
86
92
|
}
|
|
87
93
|
|
|
94
|
+
for (const path of constant.full.cascade.removeWith.values()) {
|
|
95
|
+
const fields = path.typeKey
|
|
96
|
+
? { removedAt: 1, [path.typeKey]: 1, [path.key]: 1 }
|
|
97
|
+
: { removedAt: 1, [path.key]: 1 };
|
|
98
|
+
const key = Object.keys(fields).join(",");
|
|
99
|
+
if (indexedSortFieldKeys.has(key)) continue;
|
|
100
|
+
indexedSortFieldKeys.add(key);
|
|
101
|
+
schema.index(fields as { [key: string]: 1 | -1 });
|
|
102
|
+
}
|
|
103
|
+
|
|
88
104
|
class DatabaseModelInstance extends adapt(`${modelName}Model`, ({ plug }) => ({
|
|
89
105
|
__database: plug(DatabaseAdaptorRole, (database) => database),
|
|
90
106
|
__cache: plug(CacheAdaptorRole, (cache) => new CacheDatabase(modelName, cache)),
|
|
@@ -218,11 +234,14 @@ export class DatabaseResolver {
|
|
|
218
234
|
find: (query: QueryOf<any>) => createFindManyChain(query),
|
|
219
235
|
findOne: (query: QueryOf<any>) => createFindOneChain(query),
|
|
220
236
|
findById: (id: string | undefined) => (id ? store.findOne({ id }) : Promise.resolve(null)),
|
|
221
|
-
|
|
237
|
+
count: (query: QueryOf<any>) => store.count(query),
|
|
222
238
|
updateOne: (query: QueryOf<any>, update: DocumentUpdateInput, options?: { upsert?: boolean }) =>
|
|
223
239
|
store.updateOneByQuery(query, update, options),
|
|
224
240
|
updateMany: (query: QueryOf<any>, update: DocumentUpdateInput) => store.updateManyByQuery(query, update),
|
|
225
|
-
|
|
241
|
+
removeOne: (query: QueryOf<any>) => store.removeOneByQuery(query),
|
|
242
|
+
removeMany: (query: QueryOf<any>) => store.removeManyByQuery(query),
|
|
243
|
+
|
|
244
|
+
countDocuments: (query: QueryOf<any>) => store.count(query),
|
|
226
245
|
bulkWrite: (
|
|
227
246
|
operations: { updateOne: { filter: QueryOf<any>; update: DocumentUpdateInput; upsert?: boolean } }[],
|
|
228
247
|
) => store.bulkWrite(operations),
|
|
@@ -322,6 +341,18 @@ export class DatabaseResolver {
|
|
|
322
341
|
async __remove(id: string) {
|
|
323
342
|
return await this.__store.remove(id);
|
|
324
343
|
}
|
|
344
|
+
async __removeMany(query: QueryOf<any>) {
|
|
345
|
+
return await timedQuery(() => this.__store.removeManyByQuery(query));
|
|
346
|
+
}
|
|
347
|
+
async __removeOne(query: QueryOf<any>) {
|
|
348
|
+
return await timedQuery(() => this.__store.removeOneByQuery(query));
|
|
349
|
+
}
|
|
350
|
+
async __updateMany(query: QueryOf<any>, update: DocumentUpdateInput) {
|
|
351
|
+
return await timedQuery(() => this.__store.updateManyByQuery(query, update));
|
|
352
|
+
}
|
|
353
|
+
async __updateOne(query: QueryOf<any>, update: DocumentUpdateInput) {
|
|
354
|
+
return await timedQuery(() => this.__store.updateOneByQuery(query, update));
|
|
355
|
+
}
|
|
325
356
|
async [`remove${className}`](id: string) {
|
|
326
357
|
return this.__remove(id);
|
|
327
358
|
}
|
|
@@ -347,6 +378,7 @@ export class DatabaseResolver {
|
|
|
347
378
|
Object.entries(filterMeta.query).forEach(([queryKey, filterInfo]) => {
|
|
348
379
|
const queryFn = filterInfo.queryFn;
|
|
349
380
|
if (!queryFn) throw new Error(`No query function for key: ${queryKey}`);
|
|
381
|
+
assertFilterFitsCrud(modelName, queryKey, className);
|
|
350
382
|
Object.assign(DatabaseModelInstance.prototype, {
|
|
351
383
|
[`list${capitalize(queryKey)}`]: async function (...args: any) {
|
|
352
384
|
const { query, queryOption } = getQueryDataFromKey(queryKey, args);
|
|
@@ -386,9 +418,30 @@ export class DatabaseResolver {
|
|
|
386
418
|
},
|
|
387
419
|
[`query${capitalize(queryKey)}`]: (...args: any) =>
|
|
388
420
|
queryFn(...fillMissingFilterArgs(filterInfo, args), documentQueryHelper),
|
|
421
|
+
[`remove${capitalize(queryKey)}`]: async function (...args: any) {
|
|
422
|
+
const query = queryFn(...fillMissingFilterArgs(filterInfo, args), documentQueryHelper);
|
|
423
|
+
return (this as unknown as DatabaseInstance).__removeMany(query);
|
|
424
|
+
},
|
|
425
|
+
[`removeOne${capitalize(queryKey)}`]: async function (...args: any) {
|
|
426
|
+
const query = queryFn(...fillMissingFilterArgs(filterInfo, args), documentQueryHelper);
|
|
427
|
+
return (this as unknown as DatabaseInstance).__removeOne(query);
|
|
428
|
+
},
|
|
429
|
+
[`update${capitalize(queryKey)}`]: function (...args: any): UpdateChain {
|
|
430
|
+
const instance = this as unknown as DatabaseInstance;
|
|
431
|
+
const query = queryFn(...fillMissingFilterArgs(filterInfo, args), documentQueryHelper);
|
|
432
|
+
return { set: (update) => instance.__updateMany(query, update) };
|
|
433
|
+
},
|
|
434
|
+
[`updateOne${capitalize(queryKey)}`]: function (...args: any): UpdateChain {
|
|
435
|
+
const instance = this as unknown as DatabaseInstance;
|
|
436
|
+
const query = queryFn(...fillMissingFilterArgs(filterInfo, args), documentQueryHelper);
|
|
437
|
+
return { set: (update) => instance.__updateOne(query, update) };
|
|
438
|
+
},
|
|
389
439
|
});
|
|
390
440
|
});
|
|
391
441
|
applyMixins(DatabaseModelInstance, [database.model]);
|
|
392
|
-
return
|
|
442
|
+
return {
|
|
443
|
+
adaptor: DatabaseModelInstance as unknown as AdaptorCls<DatabaseInstance<any, any, any, any, any, any>>,
|
|
444
|
+
schema,
|
|
445
|
+
};
|
|
393
446
|
}
|
|
394
447
|
}
|
package/server/resolver/index.ts
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
import type { PromiseOrObject } from "akanjs/base";
|
|
2
2
|
import { capitalize } from "akanjs/common";
|
|
3
|
-
import
|
|
3
|
+
import type { QueryOf } from "akanjs/constant";
|
|
4
4
|
import {
|
|
5
|
+
assertFilterFitsCrud,
|
|
5
6
|
type CRUDEventType,
|
|
6
7
|
type DatabaseModel,
|
|
7
8
|
type DataInputOf,
|
|
8
9
|
type Doc,
|
|
10
|
+
type DocumentUpdateInput,
|
|
9
11
|
documentQueryHelper,
|
|
10
12
|
type FindQueryOption,
|
|
11
13
|
fillMissingFilterArgs,
|
|
@@ -13,15 +15,13 @@ import {
|
|
|
13
15
|
getFilterMeta,
|
|
14
16
|
type ListQueryOption,
|
|
15
17
|
type SaveEventType,
|
|
18
|
+
type UpdateChain,
|
|
16
19
|
} from "akanjs/document";
|
|
17
20
|
import type { DatabaseService, ServiceCls } from "akanjs/service";
|
|
21
|
+
import type { CascadeRunner } from "./CascadeRunner";
|
|
18
22
|
|
|
19
23
|
export class ServiceResolver {
|
|
20
|
-
static #getDefaultDbServiceMethods(
|
|
21
|
-
className: string,
|
|
22
|
-
cascades: [string, string][],
|
|
23
|
-
getService: (refName: string) => DatabaseService,
|
|
24
|
-
) {
|
|
24
|
+
static #getDefaultDbServiceMethods(refName: string, className: string, cascade: CascadeRunner) {
|
|
25
25
|
const dbServiceMethods = {
|
|
26
26
|
async __get(this: DatabaseService, id: string) {
|
|
27
27
|
return await this.__databaseModel.__get(id);
|
|
@@ -99,37 +99,33 @@ export class ServiceResolver {
|
|
|
99
99
|
return this.__update(id, data);
|
|
100
100
|
},
|
|
101
101
|
async __remove(this: DatabaseService, id: string): Promise<Doc> {
|
|
102
|
-
|
|
103
|
-
const targets = cascades.map(([key, refName]) => [key, getService(refName)] as const);
|
|
104
102
|
await this.__libsPreRemove(id);
|
|
105
103
|
const doc = await this.__databaseModel.__remove(id);
|
|
106
104
|
const removed = await this.__libsPostRemove(doc);
|
|
107
|
-
|
|
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
|
-
}
|
|
105
|
+
await cascade.run(refName, removed as Record<string, unknown>);
|
|
113
106
|
return removed;
|
|
114
107
|
},
|
|
115
108
|
async [`remove${className}`](this: DatabaseService, id: string): Promise<Doc> {
|
|
116
109
|
return this.__remove(id);
|
|
117
110
|
},
|
|
111
|
+
async __removeMany(this: DatabaseService, query: QueryOf<any>) {
|
|
112
|
+
return await this.__databaseModel.__removeMany(query);
|
|
113
|
+
},
|
|
114
|
+
async __removeOne(this: DatabaseService, query: QueryOf<any>) {
|
|
115
|
+
return await this.__databaseModel.__removeOne(query);
|
|
116
|
+
},
|
|
117
|
+
async __updateMany(this: DatabaseService, query: QueryOf<any>, update: DocumentUpdateInput) {
|
|
118
|
+
return await this.__databaseModel.__updateMany(query, update);
|
|
119
|
+
},
|
|
120
|
+
async __updateOne(this: DatabaseService, query: QueryOf<any>, update: DocumentUpdateInput) {
|
|
121
|
+
return await this.__databaseModel.__updateOne(query, update);
|
|
122
|
+
},
|
|
118
123
|
};
|
|
119
124
|
return dbServiceMethods;
|
|
120
125
|
}
|
|
121
|
-
static resolveDatabaseService(
|
|
122
|
-
constant: ConstantModel,
|
|
123
|
-
database: DatabaseModel,
|
|
124
|
-
srvRef: ServiceCls,
|
|
125
|
-
getService: (refName: string) => DatabaseService,
|
|
126
|
-
): ServiceCls {
|
|
126
|
+
static resolveDatabaseService(database: DatabaseModel, srvRef: ServiceCls, cascade: CascadeRunner): ServiceCls {
|
|
127
127
|
const className = capitalize(database.refName);
|
|
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));
|
|
128
|
+
Object.assign(srvRef.prototype, ServiceResolver.#getDefaultDbServiceMethods(database.refName, className, cascade));
|
|
133
129
|
const getQueryDataFromKey = (queryKey: string, args: any): { query: any; queryOption: any } => {
|
|
134
130
|
const lastArg = args.at(-1);
|
|
135
131
|
const hasQueryOption =
|
|
@@ -154,6 +150,7 @@ export class ServiceResolver {
|
|
|
154
150
|
const queryFn = filterInfo.queryFn;
|
|
155
151
|
if (!queryFn) throw new Error(`No query function for key: ${queryKey}`);
|
|
156
152
|
const capitalizedQueryKey = capitalize(queryKey);
|
|
153
|
+
assertFilterFitsCrud(database.refName, queryKey, className);
|
|
157
154
|
Object.assign(srvRef.prototype, {
|
|
158
155
|
[`list${capitalizedQueryKey}`]: async function (this: DatabaseService, ...args: any) {
|
|
159
156
|
const { query, queryOption } = getQueryDataFromKey(queryKey, args);
|
|
@@ -194,6 +191,22 @@ export class ServiceResolver {
|
|
|
194
191
|
[`query${capitalize(queryKey)}`]: function (this: DatabaseService, ...args: any) {
|
|
195
192
|
return queryFn(...fillMissingFilterArgs(filterInfo, args), documentQueryHelper);
|
|
196
193
|
},
|
|
194
|
+
[`remove${capitalizedQueryKey}`]: async function (this: DatabaseService, ...args: any) {
|
|
195
|
+
const { query } = getQueryDataFromKey(queryKey, args);
|
|
196
|
+
return this.__removeMany(query);
|
|
197
|
+
},
|
|
198
|
+
[`removeOne${capitalizedQueryKey}`]: async function (this: DatabaseService, ...args: any) {
|
|
199
|
+
const { query } = getQueryDataFromKey(queryKey, args);
|
|
200
|
+
return this.__removeOne(query);
|
|
201
|
+
},
|
|
202
|
+
[`update${capitalizedQueryKey}`]: function (this: DatabaseService, ...args: any): UpdateChain {
|
|
203
|
+
const { query } = getQueryDataFromKey(queryKey, args);
|
|
204
|
+
return { set: (update) => this.__updateMany(query, update) };
|
|
205
|
+
},
|
|
206
|
+
[`updateOne${capitalizedQueryKey}`]: function (this: DatabaseService, ...args: any): UpdateChain {
|
|
207
|
+
const { query } = getQueryDataFromKey(queryKey, args);
|
|
208
|
+
return { set: (update) => this.__updateOne(query, update) };
|
|
209
|
+
},
|
|
197
210
|
});
|
|
198
211
|
});
|
|
199
212
|
return srvRef;
|
|
@@ -95,9 +95,12 @@ export interface DocumentStore {
|
|
|
95
95
|
query: DocumentQuery,
|
|
96
96
|
update: DocumentUpdateInput,
|
|
97
97
|
): Promise<{ acknowledged: boolean; matchedCount: number; modifiedCount: number }>;
|
|
98
|
-
|
|
98
|
+
removeManyByQuery(
|
|
99
99
|
query: DocumentQuery,
|
|
100
100
|
): Promise<{ acknowledged: boolean; matchedCount: number; modifiedCount: number }>;
|
|
101
|
+
removeOneByQuery(
|
|
102
|
+
query: DocumentQuery,
|
|
103
|
+
): Promise<{ acknowledged: boolean; matchedCount: number; modifiedCount: number; upsertedId: string | null }>;
|
|
101
104
|
bulkWrite(
|
|
102
105
|
operations: { updateOne: { filter: DocumentQuery; update: DocumentUpdateInput; upsert?: boolean } }[],
|
|
103
106
|
): Promise<{ acknowledged: boolean; matchedCount: number; modifiedCount: number; upsertedId: string | null }>;
|
|
@@ -1081,11 +1084,16 @@ export class SqlDocumentStore {
|
|
|
1081
1084
|
return { acknowledged: true, matchedCount: changes, modifiedCount: changes };
|
|
1082
1085
|
}
|
|
1083
1086
|
|
|
1084
|
-
async
|
|
1085
|
-
|
|
1087
|
+
async removeManyByQuery(query: DocumentQuery) {
|
|
1088
|
+
|
|
1086
1089
|
return this.updateManyByQuery(query, { removedAt: dayjs() });
|
|
1087
1090
|
}
|
|
1088
1091
|
|
|
1092
|
+
async removeOneByQuery(query: DocumentQuery) {
|
|
1093
|
+
|
|
1094
|
+
return this.updateOneByQuery(query, { removedAt: dayjs() });
|
|
1095
|
+
}
|
|
1096
|
+
|
|
1089
1097
|
private compiledUpdate(update: DocumentUpdate) {
|
|
1090
1098
|
const compiled = this.updateCompiler.compile(update);
|
|
1091
1099
|
return {
|
package/service/serve.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type Cls, INJECT_META } from "akanjs/base";
|
|
1
|
+
import { type Cls, INJECT_META, LIBS_REMOVE_HOOK } from "akanjs/base";
|
|
2
2
|
import { applyMixins, capitalize, Logger, lowerlize } from "akanjs/common";
|
|
3
3
|
import type { DatabaseModel } from "akanjs/document";
|
|
4
4
|
|
|
@@ -146,6 +146,10 @@ export function serve(
|
|
|
146
146
|
const postUpdateFns = extSrvs.map((srv) => srv.prototype._postUpdate);
|
|
147
147
|
const preRemoveFns = extSrvs.map((srv) => srv.prototype._preRemove);
|
|
148
148
|
const postRemoveFns = extSrvs.map((srv) => srv.prototype._postRemove);
|
|
149
|
+
|
|
150
|
+
Object.assign(srvRef, {
|
|
151
|
+
[LIBS_REMOVE_HOOK]: preRemoveFns.some(Boolean) || postRemoveFns.some(Boolean),
|
|
152
|
+
});
|
|
149
153
|
Object.assign(srvRef.prototype, {
|
|
150
154
|
async __libsPreCreate(this: DatabaseService, data: DatabaseServiceData) {
|
|
151
155
|
let result = data;
|
package/service/serviceModule.ts
CHANGED
|
@@ -9,6 +9,7 @@ import type {
|
|
|
9
9
|
FindQueryOption,
|
|
10
10
|
ListQueryOption,
|
|
11
11
|
SaveEventType,
|
|
12
|
+
UpdateChain,
|
|
12
13
|
} from "akanjs/document";
|
|
13
14
|
import type { ServiceCls } from "./serve";
|
|
14
15
|
import type { DatabaseService } from "./types";
|
|
@@ -210,6 +211,22 @@ export class ServiceModel<
|
|
|
210
211
|
[`query${capitalizedQueryKey}`]: function (this: DatabaseService, ...args: any) {
|
|
211
212
|
return queryFn(...args);
|
|
212
213
|
},
|
|
214
|
+
[`remove${capitalizedQueryKey}`]: async function (this: DatabaseService, ...args: any) {
|
|
215
|
+
const { query } = getQueryDataFromKey(queryKey, args);
|
|
216
|
+
return this.__removeMany(query);
|
|
217
|
+
},
|
|
218
|
+
[`removeOne${capitalizedQueryKey}`]: async function (this: DatabaseService, ...args: any) {
|
|
219
|
+
const { query } = getQueryDataFromKey(queryKey, args);
|
|
220
|
+
return this.__removeOne(query);
|
|
221
|
+
},
|
|
222
|
+
[`update${capitalizedQueryKey}`]: function (this: DatabaseService, ...args: any): UpdateChain {
|
|
223
|
+
const { query } = getQueryDataFromKey(queryKey, args);
|
|
224
|
+
return { set: (update) => this.__updateMany(query, update) };
|
|
225
|
+
},
|
|
226
|
+
[`updateOne${capitalizedQueryKey}`]: function (this: DatabaseService, ...args: any): UpdateChain {
|
|
227
|
+
const { query } = getQueryDataFromKey(queryKey, args);
|
|
228
|
+
return { set: (update) => this.__updateOne(query, update) };
|
|
229
|
+
},
|
|
213
230
|
};
|
|
214
231
|
return filterServiceMethods;
|
|
215
232
|
}
|
package/service/types.ts
CHANGED
|
@@ -5,12 +5,14 @@ import type {
|
|
|
5
5
|
CRUDEventType,
|
|
6
6
|
DatabaseModel,
|
|
7
7
|
DataInputOf,
|
|
8
|
+
DocumentUpdateInput,
|
|
8
9
|
FilterInstance,
|
|
9
10
|
FindQueryOption,
|
|
10
11
|
GetDocObject,
|
|
11
12
|
ListQueryOption,
|
|
12
13
|
QueryMethodPart,
|
|
13
14
|
SaveEventType,
|
|
15
|
+
UpdateResult,
|
|
14
16
|
} from "akanjs/document";
|
|
15
17
|
|
|
16
18
|
type ServiceMixinOmitKey =
|
|
@@ -101,6 +103,10 @@ export type DatabaseService<
|
|
|
101
103
|
__create: (data: _DataInputOfDoc) => Promise<Doc>;
|
|
102
104
|
__update: (id: string, data: Partial<Doc>) => Promise<Doc>;
|
|
103
105
|
__remove: (id: string) => Promise<Doc>;
|
|
106
|
+
__removeMany: (query: _QueryOfDoc) => Promise<UpdateResult>;
|
|
107
|
+
__removeOne: (query: _QueryOfDoc) => Promise<UpdateResult>;
|
|
108
|
+
__updateMany: (query: _QueryOfDoc, update: DocumentUpdateInput<Doc>) => Promise<UpdateResult>;
|
|
109
|
+
__updateOne: (query: _QueryOfDoc, update: DocumentUpdateInput<Doc>) => Promise<UpdateResult>;
|
|
104
110
|
__list(query?: _QueryOfDoc, queryOption?: _ListQueryOption): Promise<Doc[]>;
|
|
105
111
|
__listIds(query?: _QueryOfDoc, queryOption?: _ListQueryOption): Promise<string[]>;
|
|
106
112
|
__find(query?: _QueryOfDoc, queryOption?: _FindQueryOption): Promise<Doc | null>;
|
package/types/base/symbols.d.ts
CHANGED
|
@@ -3,6 +3,7 @@ export declare const SLICE_META: unique symbol;
|
|
|
3
3
|
export declare const FILTER_META: unique symbol;
|
|
4
4
|
export declare const LOADER_META: unique symbol;
|
|
5
5
|
export declare const INJECT_META: unique symbol;
|
|
6
|
+
export declare const LIBS_REMOVE_HOOK: unique symbol;
|
|
6
7
|
export declare const ENDPOINT_META: unique symbol;
|
|
7
8
|
export declare const ENDPOINT_DICT_SHAPE: unique symbol;
|
|
8
9
|
export declare const SLICE_DICT_SHAPE: unique symbol;
|
|
@@ -1,11 +1,29 @@
|
|
|
1
1
|
import type { FieldObject } from "./fieldInfo.d.ts";
|
|
2
2
|
import type { ConstantModelRef } from "./via.d.ts";
|
|
3
|
-
/**
|
|
4
|
-
|
|
3
|
+
/**
|
|
4
|
+
* Which end of a relation goes away with the other. `removeRef` removes what the field points at when this
|
|
5
|
+
* document is removed; `removeWith` removes this document when what the field points at is removed. The two
|
|
6
|
+
* read identically on a relation field, so the value has to name the direction — a mistake here is a data loss.
|
|
7
|
+
*/
|
|
8
|
+
export declare const cascadeActions: readonly ["removeRef", "removeWith"];
|
|
5
9
|
export type CascadeAction = (typeof cascadeActions)[number];
|
|
10
|
+
/** How a `removeWith` field names the owner whose removal takes this document with it. */
|
|
11
|
+
export interface CascadeWithPath {
|
|
12
|
+
readonly key: string;
|
|
13
|
+
/** Set when the field is a relation. Resolved to a refName later: the owner may not be registered yet. */
|
|
14
|
+
readonly modelRef: ConstantModelRef | null;
|
|
15
|
+
/** Set when the field declares `ref`, which names the owner at declaration. */
|
|
16
|
+
readonly refName: string | null;
|
|
17
|
+
/** Set when the field declares `refPath`: the sibling field holding the owner's refName. */
|
|
18
|
+
readonly typeKey: string | null;
|
|
19
|
+
/** The refNames `typeKey` may hold. Empty unless the field is polymorphic. */
|
|
20
|
+
readonly typeValues: readonly string[];
|
|
21
|
+
}
|
|
6
22
|
export declare class CascadePaths {
|
|
7
23
|
#private;
|
|
8
|
-
/** Field key → the model its ids point at
|
|
9
|
-
readonly
|
|
24
|
+
/** Field key → the model its ids point at, removed when this document is. */
|
|
25
|
+
readonly removeRef: Map<string, ConstantModelRef>;
|
|
26
|
+
/** Field key → the owner whose removal removes this document. */
|
|
27
|
+
readonly removeWith: Map<string, CascadeWithPath>;
|
|
10
28
|
collect(fieldMap: FieldObject): this;
|
|
11
29
|
}
|
|
@@ -3,8 +3,9 @@ import { Logger } from "akanjs/common";
|
|
|
3
3
|
import type { DocumentModel, QueryOf } from "akanjs/constant";
|
|
4
4
|
import type { CacheAdaptor, CacheSetOptions } from "akanjs/service";
|
|
5
5
|
import type { DataLoader } from "./dataLoader.d.ts";
|
|
6
|
+
import type { DocumentUpdateInput } from "./documentQuery.d.ts";
|
|
6
7
|
import type { ExtractQuery, ExtractSort, FilterInstance } from "./filterMeta.d.ts";
|
|
7
|
-
import type { CRUDEventType, Mdl, SaveEventType } from "./into.d.ts";
|
|
8
|
+
import type { CRUDEventType, Mdl, SaveEventType, UpdateResult } from "./into.d.ts";
|
|
8
9
|
import type { DataInputOf, FindQueryOption, ListQueryOption } from "./types.d.ts";
|
|
9
10
|
export declare class CacheDatabase<T = unknown> {
|
|
10
11
|
private readonly refName;
|
|
@@ -15,6 +16,14 @@ export declare class CacheDatabase<T = unknown> {
|
|
|
15
16
|
get<T extends string | number | Buffer>(topic: string, key: string): Promise<T | undefined>;
|
|
16
17
|
delete(topic: string, key: string): Promise<void>;
|
|
17
18
|
}
|
|
19
|
+
/**
|
|
20
|
+
* What `update<Filter>` returns. The patch cannot be a trailing parameter — a filter's own args may be optional,
|
|
21
|
+
* and no tuple type puts a required element after those — and leading it reads backwards. So it lands here, on a
|
|
22
|
+
* terminal `.set()` that mirrors the `UPDATE … SET …` it compiles to.
|
|
23
|
+
*/
|
|
24
|
+
export interface UpdateChain<Doc = any> {
|
|
25
|
+
set(update: DocumentUpdateInput<Doc>): Promise<UpdateResult>;
|
|
26
|
+
}
|
|
18
27
|
type QueryMethodOfKey<CapitalizedK extends string, Doc, Insight, _Args extends any[], _ListArgs extends any[], _FindArgs extends any[], _QueryOfDoc = QueryOf<Doc>> = {
|
|
19
28
|
[K in `list${CapitalizedK}`]: (...args: _ListArgs) => Promise<Doc[]>;
|
|
20
29
|
} & {
|
|
@@ -35,6 +44,14 @@ type QueryMethodOfKey<CapitalizedK extends string, Doc, Insight, _Args extends a
|
|
|
35
44
|
[K in `insight${CapitalizedK}`]: (...args: _Args) => Promise<Insight>;
|
|
36
45
|
} & {
|
|
37
46
|
[K in `query${CapitalizedK}`]: (...args: _Args) => _QueryOfDoc;
|
|
47
|
+
} & {
|
|
48
|
+
[K in `remove${CapitalizedK}`]: (...args: _Args) => Promise<UpdateResult>;
|
|
49
|
+
} & {
|
|
50
|
+
[K in `removeOne${CapitalizedK}`]: (...args: _Args) => Promise<UpdateResult>;
|
|
51
|
+
} & {
|
|
52
|
+
[K in `update${CapitalizedK}`]: (...args: _Args) => UpdateChain<Doc>;
|
|
53
|
+
} & {
|
|
54
|
+
[K in `updateOne${CapitalizedK}`]: (...args: _Args) => UpdateChain<Doc>;
|
|
38
55
|
};
|
|
39
56
|
type QueryMethodMap<Query, Doc, Insight, _FindQueryOption, _ListQueryOption, _QueryOfDoc> = {
|
|
40
57
|
[K in keyof Query]: K extends string ? Query[K] extends (...args: infer Args) => any ? QueryMethodOfKey<Capitalize<K>, Doc, Insight, Args, [
|
|
@@ -57,6 +74,10 @@ type DatabaseModelWithQuerySort<T extends string, Input, Doc, Obj, Insight, Quer
|
|
|
57
74
|
__create: (data: _DataInput) => Promise<Doc>;
|
|
58
75
|
__update: (id: string, data: Partial<Doc>) => Promise<Doc>;
|
|
59
76
|
__remove: (id: string) => Promise<Doc>;
|
|
77
|
+
__removeMany: (query: _QueryOfDoc) => Promise<UpdateResult>;
|
|
78
|
+
__removeOne: (query: _QueryOfDoc) => Promise<UpdateResult>;
|
|
79
|
+
__updateMany: (query: _QueryOfDoc, update: DocumentUpdateInput<Doc>) => Promise<UpdateResult>;
|
|
80
|
+
__updateOne: (query: _QueryOfDoc, update: DocumentUpdateInput<Doc>) => Promise<UpdateResult>;
|
|
60
81
|
__list(query: _QueryOfDoc, queryOption?: _ListQueryOption): Promise<Doc[]>;
|
|
61
82
|
__listIds(query: _QueryOfDoc, queryOption?: _ListQueryOption): Promise<string[]>;
|
|
62
83
|
__find(query: _QueryOfDoc, queryOption?: _FindQueryOption): Promise<Doc | null>;
|
|
@@ -8,7 +8,7 @@ export interface DatabaseModel<T extends string = string, Input = any, Doc = any
|
|
|
8
8
|
model: ModelCls<Model>;
|
|
9
9
|
filter: FilterCls<Filter>;
|
|
10
10
|
obj: ConstantCls<Obj>;
|
|
11
|
-
insight:
|
|
11
|
+
insight: DatabaseCls<Insight>;
|
|
12
12
|
_Input: Input;
|
|
13
13
|
_Doc: Doc;
|
|
14
14
|
_Model: Model;
|
|
@@ -32,6 +32,6 @@ export declare class DatabaseRegistry {
|
|
|
32
32
|
static getScalar<AllowEmpty extends boolean = false>(refName: string, { allowEmpty }?: {
|
|
33
33
|
allowEmpty?: AllowEmpty;
|
|
34
34
|
}): AllowEmpty extends true ? DatabaseCls | undefined : DatabaseCls;
|
|
35
|
-
static buildModel<T extends string, Input, Doc, Model, Obj, Insight, Filter extends FilterInstance, _Query extends ExtractQuery<Filter> = ExtractQuery<Filter>, _Sort extends ExtractSort<Filter> = ExtractSort<Filter>>(refName: T, input: DatabaseCls<Input>, doc: DatabaseCls<Doc>, model: ModelCls<Model>, obj: ConstantCls<Obj>, insight:
|
|
35
|
+
static buildModel<T extends string, Input, Doc, Model, Obj, Insight, Filter extends FilterInstance, _Query extends ExtractQuery<Filter> = ExtractQuery<Filter>, _Sort extends ExtractSort<Filter> = ExtractSort<Filter>>(refName: T, input: DatabaseCls<Input>, doc: DatabaseCls<Doc>, model: ModelCls<Model>, obj: ConstantCls<Obj>, insight: DatabaseCls<Insight>, filter: FilterCls<Filter>): DatabaseModel<T, Input, Doc, Model, Obj, Insight, Filter, _Query, _Sort>;
|
|
36
36
|
static buildScalar<T extends string, Model>(refName: T, Model: DatabaseCls<Model>): DatabaseCls<Model>;
|
|
37
37
|
}
|
|
@@ -19,6 +19,7 @@ export declare const getFilterInfoByKey: <ArgNames extends string[] = [], Args e
|
|
|
19
19
|
export declare const setFilterInfoByKey: <ArgNames extends string[] = [], Args extends any[] = any[], Model = any>(modelRef: Cls<Model>, key: string, filterInfo: FilterInfo<ArgNames, Args, Model>) => void;
|
|
20
20
|
export declare const getFilterSortByKey: (modelRef: FilterCls, key: string) => unknown;
|
|
21
21
|
export declare const fillMissingFilterArgs: (filterInfo: FilterInfo, args: unknown[]) => any[];
|
|
22
|
+
export declare const assertFilterFitsCrud: (refName: string, queryKey: string, className: string) => void;
|
|
22
23
|
export type BaseFilterSortKey = "latest" | "oldest" | "relevance";
|
|
23
24
|
export type BaseFilterQueryKey = "any";
|
|
24
25
|
export type BaseFilterKey = BaseFilterSortKey | BaseFilterQueryKey;
|
package/types/document/into.d.ts
CHANGED
|
@@ -56,20 +56,24 @@ export type Mdl<Doc, Raw, _RawDoc = DocumentModel<Raw>, _RawQuery extends Docume
|
|
|
56
56
|
find(query: _RawQuery, projection?: _Projection): FindManyChain<Doc>;
|
|
57
57
|
findOne(query: _RawQuery, projection?: _Projection): FindOneChain<Doc>;
|
|
58
58
|
findById(id: string | undefined, projection?: _Projection): Promise<Doc | null>;
|
|
59
|
-
|
|
59
|
+
count(query: _RawQuery): Promise<number>;
|
|
60
60
|
exists(query: _RawQuery): Promise<string | null>;
|
|
61
61
|
updateOne(query: _RawQuery, update: DocumentUpdateInput<_RawDoc>, options?: DocumentUpdateOptions): Promise<UpdateResult>;
|
|
62
62
|
updateMany(query: _RawQuery, update: DocumentUpdateInput<_RawDoc>): Promise<UpdateResult>;
|
|
63
|
-
|
|
63
|
+
removeOne(query: _RawQuery): Promise<UpdateResult>;
|
|
64
|
+
removeMany(query: _RawQuery): Promise<UpdateResult>;
|
|
64
65
|
bulkWrite(operations: BulkWriteOperation<Raw, _RawDoc, _RawQuery>[]): Promise<UpdateResult>;
|
|
66
|
+
/** @deprecated Renamed to `count`. */
|
|
67
|
+
countDocuments(query: _RawQuery): Promise<number>;
|
|
65
68
|
};
|
|
66
|
-
interface IntoConstantModel<T extends string, _CapitalizedRefName extends string, Raw> {
|
|
69
|
+
interface IntoConstantModel<T extends string, _CapitalizedRefName extends string, Raw, Insight> {
|
|
67
70
|
refName: T;
|
|
68
71
|
_CapitalizedRefName: _CapitalizedRefName;
|
|
69
72
|
_Full: Raw;
|
|
73
|
+
_Insight: Insight;
|
|
70
74
|
}
|
|
71
75
|
type NoInferType<T> = [T][T extends unknown ? 0 : never];
|
|
72
|
-
type IntoModelActions<T extends string, _CapitalizedRefName extends string, Doc, Raw, _Query, _Sort, _QueryOfDoc = QueryOf<Doc>> = {
|
|
76
|
+
type IntoModelActions<T extends string, _CapitalizedRefName extends string, Doc, Raw, Insight, _Query, _Sort, _QueryOfDoc = QueryOf<Doc>> = {
|
|
73
77
|
[key in _CapitalizedRefName]: Mdl<Doc, Raw>;
|
|
74
78
|
} & {
|
|
75
79
|
[key in `${Uncapitalize<_CapitalizedRefName>}Loader`]: DataLoader<string, Doc, string>;
|
|
@@ -87,6 +91,6 @@ type IntoModelActions<T extends string, _CapitalizedRefName extends string, Doc,
|
|
|
87
91
|
[K in `update${_CapitalizedRefName}`]: (id: string, data: Partial<Doc>) => Promise<Doc>;
|
|
88
92
|
} & {
|
|
89
93
|
[K in `remove${_CapitalizedRefName}`]: (id: string) => Promise<Doc>;
|
|
90
|
-
} & QueryMethodPart<_Query, _Sort, Raw, Doc,
|
|
91
|
-
export declare const into: <Doc, FilterRef extends FilterCls, T extends string, Raw, AddDbModels extends ModelCls[], _CapitalizedRefName extends string, _QueryOfDoc = QueryOf<Doc>, _Query = FilterQueryOf<FilterRef>, _Sort = FilterSortOf<FilterRef>, _LoaderBuilder extends LoaderBuilder<NoInferType<Doc>> = LoaderBuilder<Doc>>(docRef: Cls<Doc>, filterRef: FilterRef, cnst: IntoConstantModel<T, _CapitalizedRefName, Raw>, loaderBuilder: _LoaderBuilder, ...addMdls: [...AddDbModels]) => ModelCls<IntoModelActions<T, _CapitalizedRefName, Doc, Raw, _Query, _Sort, _QueryOfDoc>, ReturnType<_LoaderBuilder>>;
|
|
94
|
+
} & QueryMethodPart<_Query, _Sort, Raw, Doc, DocumentModel<Insight>, unknown, unknown, _QueryOfDoc>;
|
|
95
|
+
export declare const into: <Doc, FilterRef extends FilterCls, T extends string, Raw, Insight, AddDbModels extends ModelCls[], _CapitalizedRefName extends string, _QueryOfDoc = QueryOf<Doc>, _Query = FilterQueryOf<FilterRef>, _Sort = FilterSortOf<FilterRef>, _LoaderBuilder extends LoaderBuilder<NoInferType<Doc>> = LoaderBuilder<Doc>>(docRef: Cls<Doc>, filterRef: FilterRef, cnst: IntoConstantModel<T, _CapitalizedRefName, Raw, Insight>, loaderBuilder: _LoaderBuilder, ...addMdls: [...AddDbModels]) => ModelCls<IntoModelActions<T, _CapitalizedRefName, Doc, Raw, Insight, _Query, _Sort, _QueryOfDoc>, ReturnType<_LoaderBuilder>>;
|
|
92
96
|
export {};
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { type ConstantModel } from "akanjs/constant";
|
|
2
|
+
import { type DocumentSchema } from "akanjs/document";
|
|
3
|
+
import type { DatabaseService, ServiceCls } from "akanjs/service";
|
|
4
|
+
export declare class CascadeRunner {
|
|
5
|
+
#private;
|
|
6
|
+
register(constant: ConstantModel, schema: DocumentSchema, srvRef: ServiceCls): void;
|
|
7
|
+
/**
|
|
8
|
+
* Called once every service is live. A `_postRemove` and a `listenPost("remove")` registered during boot both
|
|
9
|
+
* count against bulk removal, so the strategy cannot be decided before the last service has initialized.
|
|
10
|
+
*/
|
|
11
|
+
seal(getService: (refName: string) => DatabaseService): void;
|
|
12
|
+
run(refName: string, doc: Record<string, unknown>): Promise<void>;
|
|
13
|
+
}
|
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
import { type ConstantModel } from "akanjs/constant";
|
|
2
|
-
import { type DatabaseInstance, type DatabaseModel } from "akanjs/document";
|
|
2
|
+
import { type DatabaseInstance, type DatabaseModel, DocumentSchema } from "akanjs/document";
|
|
3
3
|
import { type AdaptorCls } from "akanjs/service";
|
|
4
4
|
export declare class DatabaseResolver {
|
|
5
|
-
|
|
5
|
+
/** Returns the schema alongside the adaptor: the cascade planner reads its `remove` hooks to pick a strategy. */
|
|
6
|
+
static resolveDatabase(constant: ConstantModel, database: DatabaseModel): {
|
|
7
|
+
adaptor: AdaptorCls<DatabaseInstance>;
|
|
8
|
+
schema: DocumentSchema;
|
|
9
|
+
};
|
|
6
10
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { type ConstantModel } from "akanjs/constant";
|
|
2
1
|
import { type DatabaseModel } from "akanjs/document";
|
|
3
|
-
import type {
|
|
2
|
+
import type { ServiceCls } from "akanjs/service";
|
|
3
|
+
import type { CascadeRunner } from "./CascadeRunner.d.ts";
|
|
4
4
|
export declare class ServiceResolver {
|
|
5
5
|
#private;
|
|
6
|
-
static resolveDatabaseService(
|
|
6
|
+
static resolveDatabaseService(database: DatabaseModel, srvRef: ServiceCls, cascade: CascadeRunner): ServiceCls;
|
|
7
7
|
}
|
|
@@ -56,11 +56,17 @@ export interface DocumentStore {
|
|
|
56
56
|
matchedCount: number;
|
|
57
57
|
modifiedCount: number;
|
|
58
58
|
}>;
|
|
59
|
-
|
|
59
|
+
removeManyByQuery(query: DocumentQuery): Promise<{
|
|
60
60
|
acknowledged: boolean;
|
|
61
61
|
matchedCount: number;
|
|
62
62
|
modifiedCount: number;
|
|
63
63
|
}>;
|
|
64
|
+
removeOneByQuery(query: DocumentQuery): Promise<{
|
|
65
|
+
acknowledged: boolean;
|
|
66
|
+
matchedCount: number;
|
|
67
|
+
modifiedCount: number;
|
|
68
|
+
upsertedId: string | null;
|
|
69
|
+
}>;
|
|
64
70
|
bulkWrite(operations: {
|
|
65
71
|
updateOne: {
|
|
66
72
|
filter: DocumentQuery;
|
|
@@ -309,11 +315,17 @@ export declare class SqlDocumentStore {
|
|
|
309
315
|
matchedCount: number;
|
|
310
316
|
modifiedCount: number;
|
|
311
317
|
}>;
|
|
312
|
-
|
|
318
|
+
removeManyByQuery(query: DocumentQuery): Promise<{
|
|
313
319
|
acknowledged: boolean;
|
|
314
320
|
matchedCount: number;
|
|
315
321
|
modifiedCount: number;
|
|
316
322
|
}>;
|
|
323
|
+
removeOneByQuery(query: DocumentQuery): Promise<{
|
|
324
|
+
acknowledged: boolean;
|
|
325
|
+
matchedCount: number;
|
|
326
|
+
modifiedCount: number;
|
|
327
|
+
upsertedId: any;
|
|
328
|
+
}>;
|
|
317
329
|
private compiledUpdate;
|
|
318
330
|
bulkWrite(operations: {
|
|
319
331
|
updateOne: {
|
package/types/service/types.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { Cls, MergeAllTypes, PromiseOrObject } from "akanjs/base";
|
|
2
2
|
import type { Logger } from "akanjs/common";
|
|
3
3
|
import type { QueryOf } from "akanjs/constant";
|
|
4
|
-
import type { CRUDEventType, DatabaseModel, DataInputOf, FilterInstance, FindQueryOption, GetDocObject, ListQueryOption, QueryMethodPart, SaveEventType } from "akanjs/document";
|
|
4
|
+
import type { CRUDEventType, DatabaseModel, DataInputOf, DocumentUpdateInput, FilterInstance, FindQueryOption, GetDocObject, ListQueryOption, QueryMethodPart, SaveEventType, UpdateResult } from "akanjs/document";
|
|
5
5
|
type ServiceMixinOmitKey = "onInit" | "onDestroy" | "_libsOnInit" | "_libsOnDestroy" | "_preCreate" | "_postCreate" | "_preUpdate" | "_postUpdate" | "_preRemove" | "_postRemove" | "_libsPreCreate" | "_libsPostCreate" | "_libsPreUpdate" | "_libsPostUpdate" | "_libsPreRemove" | "_libsPostRemove";
|
|
6
6
|
type DatabaseQueryMethods<Query, Sort, Obj, Doc, Insight, FindQueryOption, ListQueryOption, DocQuery> = QueryMethodPart<Query, Sort, Obj, Doc, Insight, FindQueryOption, ListQueryOption, DocQuery>;
|
|
7
7
|
type DocumentLike = {
|
|
@@ -31,6 +31,10 @@ export type DatabaseService<T extends string = string, Input = any, Doc = any, O
|
|
|
31
31
|
__create: (data: _DataInputOfDoc) => Promise<Doc>;
|
|
32
32
|
__update: (id: string, data: Partial<Doc>) => Promise<Doc>;
|
|
33
33
|
__remove: (id: string) => Promise<Doc>;
|
|
34
|
+
__removeMany: (query: _QueryOfDoc) => Promise<UpdateResult>;
|
|
35
|
+
__removeOne: (query: _QueryOfDoc) => Promise<UpdateResult>;
|
|
36
|
+
__updateMany: (query: _QueryOfDoc, update: DocumentUpdateInput<Doc>) => Promise<UpdateResult>;
|
|
37
|
+
__updateOne: (query: _QueryOfDoc, update: DocumentUpdateInput<Doc>) => Promise<UpdateResult>;
|
|
34
38
|
__list(query?: _QueryOfDoc, queryOption?: _ListQueryOption): Promise<Doc[]>;
|
|
35
39
|
__listIds(query?: _QueryOfDoc, queryOption?: _ListQueryOption): Promise<string[]>;
|
|
36
40
|
__find(query?: _QueryOfDoc, queryOption?: _FindQueryOption): Promise<Doc | null>;
|