akanjs 3.0.0-alpha.1 → 3.0.0-alpha.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/base/symbols.ts +1 -0
- package/constant/cascadePaths.ts +79 -8
- package/document/database.ts +2 -1
- package/document/databaseRegistry.ts +2 -2
- package/document/into.ts +10 -6
- 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 +23 -3
- package/server/resolver/index.ts +1 -0
- package/server/resolver/service.resolver.ts +9 -25
- package/service/predefinedAdaptor/database.adaptor.ts +3 -3
- package/service/serve.ts +5 -1
- package/service/types.ts +2 -0
- package/types/base/symbols.d.ts +1 -0
- package/types/constant/cascadePaths.d.ts +22 -4
- package/types/document/database.d.ts +2 -1
- package/types/document/databaseRegistry.d.ts +2 -2
- package/types/document/into.d.ts +6 -5
- 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 +2 -2
- package/types/service/types.d.ts +2 -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
|
@@ -4,7 +4,7 @@ import type { DocumentModel, QueryOf } from "akanjs/constant";
|
|
|
4
4
|
import type { CacheAdaptor, CacheSetOptions } from "akanjs/service";
|
|
5
5
|
import type { DataLoader } from "./dataLoader";
|
|
6
6
|
import type { ExtractQuery, ExtractSort, FilterInstance } from "./filterMeta";
|
|
7
|
-
import type { CRUDEventType, Mdl, SaveEventType } from "./into";
|
|
7
|
+
import type { CRUDEventType, Mdl, SaveEventType, UpdateResult } from "./into";
|
|
8
8
|
import type { DataInputOf, FindQueryOption, ListQueryOption } from "./types";
|
|
9
9
|
|
|
10
10
|
export class CacheDatabase<T = unknown> {
|
|
@@ -105,6 +105,7 @@ type DatabaseModelWithQuerySort<
|
|
|
105
105
|
__create: (data: _DataInput) => Promise<Doc>;
|
|
106
106
|
__update: (id: string, data: Partial<Doc>) => Promise<Doc>;
|
|
107
107
|
__remove: (id: string) => Promise<Doc>;
|
|
108
|
+
__removeMany: (query: _QueryOfDoc) => Promise<UpdateResult>;
|
|
108
109
|
__list(query: _QueryOfDoc, queryOption?: _ListQueryOption): Promise<Doc[]>;
|
|
109
110
|
__listIds(query: _QueryOfDoc, queryOption?: _ListQueryOption): Promise<string[]>;
|
|
110
111
|
__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/into.ts
CHANGED
|
@@ -85,14 +85,15 @@ export type Mdl<
|
|
|
85
85
|
options?: DocumentUpdateOptions,
|
|
86
86
|
): Promise<UpdateResult>;
|
|
87
87
|
updateMany(query: _RawQuery, update: DocumentUpdateInput<_RawDoc>): Promise<UpdateResult>;
|
|
88
|
-
|
|
88
|
+
removeMany(query: _RawQuery): Promise<UpdateResult>;
|
|
89
89
|
bulkWrite(operations: BulkWriteOperation<Raw, _RawDoc, _RawQuery>[]): Promise<UpdateResult>;
|
|
90
90
|
};
|
|
91
91
|
|
|
92
|
-
interface IntoConstantModel<T extends string, _CapitalizedRefName extends string, Raw> {
|
|
92
|
+
interface IntoConstantModel<T extends string, _CapitalizedRefName extends string, Raw, Insight> {
|
|
93
93
|
refName: T;
|
|
94
94
|
_CapitalizedRefName: _CapitalizedRefName;
|
|
95
95
|
_Full: Raw;
|
|
96
|
+
_Insight: Insight;
|
|
96
97
|
}
|
|
97
98
|
type NoInferType<T> = [T][T extends unknown ? 0 : never];
|
|
98
99
|
type IntoModelActions<
|
|
@@ -100,6 +101,8 @@ type IntoModelActions<
|
|
|
100
101
|
_CapitalizedRefName extends string,
|
|
101
102
|
Doc,
|
|
102
103
|
Raw,
|
|
104
|
+
|
|
105
|
+
Insight,
|
|
103
106
|
_Query,
|
|
104
107
|
_Sort,
|
|
105
108
|
_QueryOfDoc = QueryOf<Doc>,
|
|
@@ -121,13 +124,14 @@ type IntoModelActions<
|
|
|
121
124
|
[K in `update${_CapitalizedRefName}`]: (id: string, data: Partial<Doc>) => Promise<Doc>;
|
|
122
125
|
} & {
|
|
123
126
|
[K in `remove${_CapitalizedRefName}`]: (id: string) => Promise<Doc>;
|
|
124
|
-
} & QueryMethodPart<_Query, _Sort, Raw, Doc,
|
|
127
|
+
} & QueryMethodPart<_Query, _Sort, Raw, Doc, DocumentModel<Insight>, unknown, unknown, _QueryOfDoc>;
|
|
125
128
|
|
|
126
129
|
export const into = <
|
|
127
130
|
Doc,
|
|
128
131
|
FilterRef extends FilterCls,
|
|
129
132
|
T extends string,
|
|
130
133
|
Raw,
|
|
134
|
+
Insight,
|
|
131
135
|
AddDbModels extends ModelCls[],
|
|
132
136
|
_CapitalizedRefName extends string,
|
|
133
137
|
_QueryOfDoc = QueryOf<Doc>,
|
|
@@ -137,11 +141,11 @@ export const into = <
|
|
|
137
141
|
>(
|
|
138
142
|
docRef: Cls<Doc>,
|
|
139
143
|
filterRef: FilterRef,
|
|
140
|
-
cnst: IntoConstantModel<T, _CapitalizedRefName, Raw>,
|
|
144
|
+
cnst: IntoConstantModel<T, _CapitalizedRefName, Raw, Insight>,
|
|
141
145
|
loaderBuilder: _LoaderBuilder,
|
|
142
146
|
...addMdls: [...AddDbModels]
|
|
143
147
|
): ModelCls<
|
|
144
|
-
IntoModelActions<T, _CapitalizedRefName, Doc, Raw, _Query, _Sort, _QueryOfDoc>,
|
|
148
|
+
IntoModelActions<T, _CapitalizedRefName, Doc, Raw, Insight, _Query, _Sort, _QueryOfDoc>,
|
|
145
149
|
ReturnType<_LoaderBuilder>
|
|
146
150
|
> => {
|
|
147
151
|
const loaderInfoMap = loaderBuilder(makeLoaderBuilder<Doc>());
|
|
@@ -162,7 +166,7 @@ export const into = <
|
|
|
162
166
|
});
|
|
163
167
|
});
|
|
164
168
|
return DefaultModel as unknown as ModelCls<
|
|
165
|
-
IntoModelActions<T, _CapitalizedRefName, Doc, Raw, _Query, _Sort, _QueryOfDoc>,
|
|
169
|
+
IntoModelActions<T, _CapitalizedRefName, Doc, Raw, Insight, _Query, _Sort, _QueryOfDoc>,
|
|
166
170
|
ReturnType<_LoaderBuilder>
|
|
167
171
|
>;
|
|
168
172
|
};
|
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
|
+
}
|
|
@@ -47,7 +47,11 @@ const timedQuery = async <T>(fn: () => Promise<T>): Promise<T> => {
|
|
|
47
47
|
};
|
|
48
48
|
|
|
49
49
|
export class DatabaseResolver {
|
|
50
|
-
|
|
50
|
+
/** Returns the schema alongside the adaptor: the cascade planner reads its `remove` hooks to pick a strategy. */
|
|
51
|
+
static resolveDatabase(
|
|
52
|
+
constant: ConstantModel,
|
|
53
|
+
database: DatabaseModel,
|
|
54
|
+
): { adaptor: AdaptorCls<DatabaseInstance>; schema: DocumentSchema } {
|
|
51
55
|
const [modelName, className]: [string, string] = [database.refName, capitalize(database.refName)];
|
|
52
56
|
|
|
53
57
|
const resolveSort = (sortKey?: string | null) =>
|
|
@@ -85,6 +89,16 @@ export class DatabaseResolver {
|
|
|
85
89
|
schema.index(fields);
|
|
86
90
|
}
|
|
87
91
|
|
|
92
|
+
for (const path of constant.full.cascade.removeWith.values()) {
|
|
93
|
+
const fields = path.typeKey
|
|
94
|
+
? { removedAt: 1, [path.typeKey]: 1, [path.key]: 1 }
|
|
95
|
+
: { removedAt: 1, [path.key]: 1 };
|
|
96
|
+
const key = Object.keys(fields).join(",");
|
|
97
|
+
if (indexedSortFieldKeys.has(key)) continue;
|
|
98
|
+
indexedSortFieldKeys.add(key);
|
|
99
|
+
schema.index(fields as { [key: string]: 1 | -1 });
|
|
100
|
+
}
|
|
101
|
+
|
|
88
102
|
class DatabaseModelInstance extends adapt(`${modelName}Model`, ({ plug }) => ({
|
|
89
103
|
__database: plug(DatabaseAdaptorRole, (database) => database),
|
|
90
104
|
__cache: plug(CacheAdaptorRole, (cache) => new CacheDatabase(modelName, cache)),
|
|
@@ -222,7 +236,7 @@ export class DatabaseResolver {
|
|
|
222
236
|
updateOne: (query: QueryOf<any>, update: DocumentUpdateInput, options?: { upsert?: boolean }) =>
|
|
223
237
|
store.updateOneByQuery(query, update, options),
|
|
224
238
|
updateMany: (query: QueryOf<any>, update: DocumentUpdateInput) => store.updateManyByQuery(query, update),
|
|
225
|
-
|
|
239
|
+
removeMany: (query: QueryOf<any>) => store.removeManyByQuery(query),
|
|
226
240
|
bulkWrite: (
|
|
227
241
|
operations: { updateOne: { filter: QueryOf<any>; update: DocumentUpdateInput; upsert?: boolean } }[],
|
|
228
242
|
) => store.bulkWrite(operations),
|
|
@@ -322,6 +336,9 @@ export class DatabaseResolver {
|
|
|
322
336
|
async __remove(id: string) {
|
|
323
337
|
return await this.__store.remove(id);
|
|
324
338
|
}
|
|
339
|
+
async __removeMany(query: QueryOf<any>) {
|
|
340
|
+
return await timedQuery(() => this.__store.removeManyByQuery(query));
|
|
341
|
+
}
|
|
325
342
|
async [`remove${className}`](id: string) {
|
|
326
343
|
return this.__remove(id);
|
|
327
344
|
}
|
|
@@ -389,6 +406,9 @@ export class DatabaseResolver {
|
|
|
389
406
|
});
|
|
390
407
|
});
|
|
391
408
|
applyMixins(DatabaseModelInstance, [database.model]);
|
|
392
|
-
return
|
|
409
|
+
return {
|
|
410
|
+
adaptor: DatabaseModelInstance as unknown as AdaptorCls<DatabaseInstance<any, any, any, any, any, any>>,
|
|
411
|
+
schema,
|
|
412
|
+
};
|
|
393
413
|
}
|
|
394
414
|
}
|
package/server/resolver/index.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
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
5
|
type CRUDEventType,
|
|
6
6
|
type DatabaseModel,
|
|
@@ -15,13 +15,10 @@ import {
|
|
|
15
15
|
type SaveEventType,
|
|
16
16
|
} from "akanjs/document";
|
|
17
17
|
import type { DatabaseService, ServiceCls } from "akanjs/service";
|
|
18
|
+
import type { CascadeRunner } from "./CascadeRunner";
|
|
18
19
|
|
|
19
20
|
export class ServiceResolver {
|
|
20
|
-
static #getDefaultDbServiceMethods(
|
|
21
|
-
className: string,
|
|
22
|
-
cascades: [string, string][],
|
|
23
|
-
getService: (refName: string) => DatabaseService,
|
|
24
|
-
) {
|
|
21
|
+
static #getDefaultDbServiceMethods(refName: string, className: string, cascade: CascadeRunner) {
|
|
25
22
|
const dbServiceMethods = {
|
|
26
23
|
async __get(this: DatabaseService, id: string) {
|
|
27
24
|
return await this.__databaseModel.__get(id);
|
|
@@ -99,37 +96,24 @@ export class ServiceResolver {
|
|
|
99
96
|
return this.__update(id, data);
|
|
100
97
|
},
|
|
101
98
|
async __remove(this: DatabaseService, id: string): Promise<Doc> {
|
|
102
|
-
|
|
103
|
-
const targets = cascades.map(([key, refName]) => [key, getService(refName)] as const);
|
|
104
99
|
await this.__libsPreRemove(id);
|
|
105
100
|
const doc = await this.__databaseModel.__remove(id);
|
|
106
101
|
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
|
-
}
|
|
102
|
+
await cascade.run(refName, removed as Record<string, unknown>);
|
|
113
103
|
return removed;
|
|
114
104
|
},
|
|
115
105
|
async [`remove${className}`](this: DatabaseService, id: string): Promise<Doc> {
|
|
116
106
|
return this.__remove(id);
|
|
117
107
|
},
|
|
108
|
+
async __removeMany(this: DatabaseService, query: QueryOf<any>) {
|
|
109
|
+
return await this.__databaseModel.__removeMany(query);
|
|
110
|
+
},
|
|
118
111
|
};
|
|
119
112
|
return dbServiceMethods;
|
|
120
113
|
}
|
|
121
|
-
static resolveDatabaseService(
|
|
122
|
-
constant: ConstantModel,
|
|
123
|
-
database: DatabaseModel,
|
|
124
|
-
srvRef: ServiceCls,
|
|
125
|
-
getService: (refName: string) => DatabaseService,
|
|
126
|
-
): ServiceCls {
|
|
114
|
+
static resolveDatabaseService(database: DatabaseModel, srvRef: ServiceCls, cascade: CascadeRunner): ServiceCls {
|
|
127
115
|
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));
|
|
116
|
+
Object.assign(srvRef.prototype, ServiceResolver.#getDefaultDbServiceMethods(database.refName, className, cascade));
|
|
133
117
|
const getQueryDataFromKey = (queryKey: string, args: any): { query: any; queryOption: any } => {
|
|
134
118
|
const lastArg = args.at(-1);
|
|
135
119
|
const hasQueryOption =
|
|
@@ -95,7 +95,7 @@ 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
101
|
bulkWrite(
|
|
@@ -1081,8 +1081,8 @@ export class SqlDocumentStore {
|
|
|
1081
1081
|
return { acknowledged: true, matchedCount: changes, modifiedCount: changes };
|
|
1082
1082
|
}
|
|
1083
1083
|
|
|
1084
|
-
async
|
|
1085
|
-
|
|
1084
|
+
async removeManyByQuery(query: DocumentQuery) {
|
|
1085
|
+
|
|
1086
1086
|
return this.updateManyByQuery(query, { removedAt: dayjs() });
|
|
1087
1087
|
}
|
|
1088
1088
|
|
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/types.ts
CHANGED
|
@@ -11,6 +11,7 @@ import type {
|
|
|
11
11
|
ListQueryOption,
|
|
12
12
|
QueryMethodPart,
|
|
13
13
|
SaveEventType,
|
|
14
|
+
UpdateResult,
|
|
14
15
|
} from "akanjs/document";
|
|
15
16
|
|
|
16
17
|
type ServiceMixinOmitKey =
|
|
@@ -101,6 +102,7 @@ export type DatabaseService<
|
|
|
101
102
|
__create: (data: _DataInputOfDoc) => Promise<Doc>;
|
|
102
103
|
__update: (id: string, data: Partial<Doc>) => Promise<Doc>;
|
|
103
104
|
__remove: (id: string) => Promise<Doc>;
|
|
105
|
+
__removeMany: (query: _QueryOfDoc) => Promise<UpdateResult>;
|
|
104
106
|
__list(query?: _QueryOfDoc, queryOption?: _ListQueryOption): Promise<Doc[]>;
|
|
105
107
|
__listIds(query?: _QueryOfDoc, queryOption?: _ListQueryOption): Promise<string[]>;
|
|
106
108
|
__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
|
}
|
|
@@ -4,7 +4,7 @@ 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
6
|
import type { ExtractQuery, ExtractSort, FilterInstance } from "./filterMeta.d.ts";
|
|
7
|
-
import type { CRUDEventType, Mdl, SaveEventType } from "./into.d.ts";
|
|
7
|
+
import type { CRUDEventType, Mdl, SaveEventType, UpdateResult } from "./into.d.ts";
|
|
8
8
|
import type { DataInputOf, FindQueryOption, ListQueryOption } from "./types.d.ts";
|
|
9
9
|
export declare class CacheDatabase<T = unknown> {
|
|
10
10
|
private readonly refName;
|
|
@@ -57,6 +57,7 @@ type DatabaseModelWithQuerySort<T extends string, Input, Doc, Obj, Insight, Quer
|
|
|
57
57
|
__create: (data: _DataInput) => Promise<Doc>;
|
|
58
58
|
__update: (id: string, data: Partial<Doc>) => Promise<Doc>;
|
|
59
59
|
__remove: (id: string) => Promise<Doc>;
|
|
60
|
+
__removeMany: (query: _QueryOfDoc) => Promise<UpdateResult>;
|
|
60
61
|
__list(query: _QueryOfDoc, queryOption?: _ListQueryOption): Promise<Doc[]>;
|
|
61
62
|
__listIds(query: _QueryOfDoc, queryOption?: _ListQueryOption): Promise<string[]>;
|
|
62
63
|
__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
|
}
|
package/types/document/into.d.ts
CHANGED
|
@@ -60,16 +60,17 @@ export type Mdl<Doc, Raw, _RawDoc = DocumentModel<Raw>, _RawQuery extends Docume
|
|
|
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
|
+
removeMany(query: _RawQuery): Promise<UpdateResult>;
|
|
64
64
|
bulkWrite(operations: BulkWriteOperation<Raw, _RawDoc, _RawQuery>[]): Promise<UpdateResult>;
|
|
65
65
|
};
|
|
66
|
-
interface IntoConstantModel<T extends string, _CapitalizedRefName extends string, Raw> {
|
|
66
|
+
interface IntoConstantModel<T extends string, _CapitalizedRefName extends string, Raw, Insight> {
|
|
67
67
|
refName: T;
|
|
68
68
|
_CapitalizedRefName: _CapitalizedRefName;
|
|
69
69
|
_Full: Raw;
|
|
70
|
+
_Insight: Insight;
|
|
70
71
|
}
|
|
71
72
|
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>> = {
|
|
73
|
+
type IntoModelActions<T extends string, _CapitalizedRefName extends string, Doc, Raw, Insight, _Query, _Sort, _QueryOfDoc = QueryOf<Doc>> = {
|
|
73
74
|
[key in _CapitalizedRefName]: Mdl<Doc, Raw>;
|
|
74
75
|
} & {
|
|
75
76
|
[key in `${Uncapitalize<_CapitalizedRefName>}Loader`]: DataLoader<string, Doc, string>;
|
|
@@ -87,6 +88,6 @@ type IntoModelActions<T extends string, _CapitalizedRefName extends string, Doc,
|
|
|
87
88
|
[K in `update${_CapitalizedRefName}`]: (id: string, data: Partial<Doc>) => Promise<Doc>;
|
|
88
89
|
} & {
|
|
89
90
|
[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>>;
|
|
91
|
+
} & QueryMethodPart<_Query, _Sort, Raw, Doc, DocumentModel<Insight>, unknown, unknown, _QueryOfDoc>;
|
|
92
|
+
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
93
|
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,7 +56,7 @@ 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;
|
|
@@ -309,7 +309,7 @@ export declare class SqlDocumentStore {
|
|
|
309
309
|
matchedCount: number;
|
|
310
310
|
modifiedCount: number;
|
|
311
311
|
}>;
|
|
312
|
-
|
|
312
|
+
removeManyByQuery(query: DocumentQuery): Promise<{
|
|
313
313
|
acknowledged: boolean;
|
|
314
314
|
matchedCount: number;
|
|
315
315
|
modifiedCount: number;
|
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, 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,7 @@ 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>;
|
|
34
35
|
__list(query?: _QueryOfDoc, queryOption?: _ListQueryOption): Promise<Doc[]>;
|
|
35
36
|
__listIds(query?: _QueryOfDoc, queryOption?: _ListQueryOption): Promise<string[]>;
|
|
36
37
|
__find(query?: _QueryOfDoc, queryOption?: _FindQueryOption): Promise<Doc | null>;
|