@akanjs/document 0.0.54 → 0.0.55
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/index.mjs +1 -0
- package/package.json +7 -1
- package/src/dataLoader.mjs +110 -0
- package/src/database.mjs +295 -0
- package/src/dbDecorators.mjs +93 -0
- package/src/index.mjs +5 -0
- package/src/schema.mjs +104 -0
- package/src/types.mjs +0 -0
package/index.mjs
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./src";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@akanjs/document",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.55",
|
|
4
4
|
"type": "commonjs",
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public"
|
|
@@ -18,5 +18,11 @@
|
|
|
18
18
|
"dataloader": "^2.2.3",
|
|
19
19
|
"lodash": "^4.17.21",
|
|
20
20
|
"mongoose": "^8.9.3"
|
|
21
|
+
},
|
|
22
|
+
"exports": {
|
|
23
|
+
".": {
|
|
24
|
+
"require": "./index.js",
|
|
25
|
+
"import": "./index.mjs"
|
|
26
|
+
}
|
|
21
27
|
}
|
|
22
28
|
}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import DataLoader from "dataloader";
|
|
2
|
+
import { flatMap, get, groupBy, keyBy } from "lodash";
|
|
3
|
+
import { Schema, Types } from "mongoose";
|
|
4
|
+
const Id = Types.ObjectId;
|
|
5
|
+
const ObjectId = Schema.Types.ObjectId;
|
|
6
|
+
const Mixed = Schema.Types.Mixed;
|
|
7
|
+
const createLoader = (model, fieldName = "_id", defaultQuery = {}) => {
|
|
8
|
+
return new DataLoader(
|
|
9
|
+
(fields) => {
|
|
10
|
+
const query = { ...defaultQuery };
|
|
11
|
+
query[fieldName] = { $in: fields };
|
|
12
|
+
const data = model.find(query).then((list) => {
|
|
13
|
+
const listByKey = keyBy(list, fieldName);
|
|
14
|
+
return fields.map((id) => get(listByKey, id, null));
|
|
15
|
+
});
|
|
16
|
+
return data;
|
|
17
|
+
},
|
|
18
|
+
{ name: "dataloader", cache: false }
|
|
19
|
+
);
|
|
20
|
+
};
|
|
21
|
+
const createArrayLoader = (model, fieldName = "_id", defaultQuery = {}) => {
|
|
22
|
+
return new DataLoader((fields) => {
|
|
23
|
+
const query = { ...defaultQuery };
|
|
24
|
+
query[fieldName] = { $in: fields };
|
|
25
|
+
const data = model.find(query).then((list) => {
|
|
26
|
+
return fields.map((field) => list.filter((item) => field === item[fieldName]));
|
|
27
|
+
});
|
|
28
|
+
return data;
|
|
29
|
+
});
|
|
30
|
+
};
|
|
31
|
+
const createArrayElementLoader = (model, fieldName = "_id", defaultQuery = {}) => {
|
|
32
|
+
return new DataLoader(
|
|
33
|
+
(fields) => {
|
|
34
|
+
const query = { ...defaultQuery };
|
|
35
|
+
query[fieldName] = { $in: fields };
|
|
36
|
+
const data = model.find(query).then((list) => {
|
|
37
|
+
const flat = flatMap(
|
|
38
|
+
list,
|
|
39
|
+
(dat) => dat[fieldName].map((datField) => ({
|
|
40
|
+
...dat.toObject(),
|
|
41
|
+
key: datField
|
|
42
|
+
}))
|
|
43
|
+
);
|
|
44
|
+
const listByKey = groupBy(flat, (dat) => dat.key);
|
|
45
|
+
return fields.map((id) => get(listByKey, id, null));
|
|
46
|
+
});
|
|
47
|
+
return data;
|
|
48
|
+
},
|
|
49
|
+
{ name: "dataloader", cache: false }
|
|
50
|
+
);
|
|
51
|
+
};
|
|
52
|
+
const createQueryLoader = (model, queryKeys, defaultQuery = {}) => {
|
|
53
|
+
return new DataLoader(
|
|
54
|
+
(queries) => {
|
|
55
|
+
const query = {
|
|
56
|
+
$and: [{ $or: queries }, defaultQuery]
|
|
57
|
+
};
|
|
58
|
+
const getQueryKey = (query2) => queryKeys.map((key) => query2[key].toString()).join("");
|
|
59
|
+
const data = model.find(query).then((list) => {
|
|
60
|
+
const listByKey = keyBy(list, getQueryKey);
|
|
61
|
+
return queries.map((query2) => get(listByKey, getQueryKey(query2), null));
|
|
62
|
+
});
|
|
63
|
+
return data;
|
|
64
|
+
},
|
|
65
|
+
{ name: "dataloader", cache: false }
|
|
66
|
+
);
|
|
67
|
+
};
|
|
68
|
+
const getLoaderMetaMapByPrototype = (prototype) => {
|
|
69
|
+
const loaderMetaMap = Reflect.getOwnMetadata("loaders", prototype) ?? /* @__PURE__ */ new Map();
|
|
70
|
+
return loaderMetaMap;
|
|
71
|
+
};
|
|
72
|
+
const getLoaderMetas = (target) => {
|
|
73
|
+
const metas = [...getLoaderMetaMapByPrototype(target.prototype).values()];
|
|
74
|
+
return metas;
|
|
75
|
+
};
|
|
76
|
+
const Loader = {
|
|
77
|
+
ByField: (fieldName, defaultQuery = {}) => {
|
|
78
|
+
return function(target, key) {
|
|
79
|
+
const loaderMetaMap = getLoaderMetaMapByPrototype(target);
|
|
80
|
+
loaderMetaMap.set(key, { key, type: "Field", fieldName, defaultQuery });
|
|
81
|
+
Reflect.defineMetadata("loaders", loaderMetaMap, target);
|
|
82
|
+
};
|
|
83
|
+
},
|
|
84
|
+
ByArrayField: (fieldName, defaultQuery = {}) => {
|
|
85
|
+
return function(target, key) {
|
|
86
|
+
const loaderMetaMap = getLoaderMetaMapByPrototype(target);
|
|
87
|
+
loaderMetaMap.set(key, { key, type: "ArrayField", fieldName, defaultQuery });
|
|
88
|
+
Reflect.defineMetadata("loaders", loaderMetaMap, target);
|
|
89
|
+
};
|
|
90
|
+
},
|
|
91
|
+
ByQuery: (queryKeys, defaultQuery = {}) => {
|
|
92
|
+
return function(target, key) {
|
|
93
|
+
const loaderMetaMap = getLoaderMetaMapByPrototype(target);
|
|
94
|
+
loaderMetaMap.set(key, { key, type: "Query", queryKeys, defaultQuery });
|
|
95
|
+
Reflect.defineMetadata("loaders", loaderMetaMap, target);
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
};
|
|
99
|
+
export {
|
|
100
|
+
DataLoader,
|
|
101
|
+
Id,
|
|
102
|
+
Loader,
|
|
103
|
+
Mixed,
|
|
104
|
+
ObjectId,
|
|
105
|
+
createArrayElementLoader,
|
|
106
|
+
createArrayLoader,
|
|
107
|
+
createLoader,
|
|
108
|
+
createQueryLoader,
|
|
109
|
+
getLoaderMetas
|
|
110
|
+
};
|
package/src/database.mjs
ADDED
|
@@ -0,0 +1,295 @@
|
|
|
1
|
+
import { dayjs } from "@akanjs/base";
|
|
2
|
+
import { capitalize, Logger, lowerlize } from "@akanjs/common";
|
|
3
|
+
import {
|
|
4
|
+
DEFAULT_PAGE_SIZE,
|
|
5
|
+
getFieldMetas,
|
|
6
|
+
getFilterKeyMetaMapOnPrototype,
|
|
7
|
+
getFilterQuery,
|
|
8
|
+
getFilterSort
|
|
9
|
+
} from "@akanjs/constant";
|
|
10
|
+
import { Transaction } from "@akanjs/nest";
|
|
11
|
+
import { createArrayLoader, createLoader, createQueryLoader, getLoaderMetas } from "./dataLoader";
|
|
12
|
+
import { convertAggregateMatch } from "./schema";
|
|
13
|
+
class CacheDatabase {
|
|
14
|
+
constructor(refName, redis) {
|
|
15
|
+
this.refName = refName;
|
|
16
|
+
this.redis = redis;
|
|
17
|
+
this.logger = new Logger(`${refName}Cache`);
|
|
18
|
+
}
|
|
19
|
+
logger;
|
|
20
|
+
async set(topic, key, value, option = {}) {
|
|
21
|
+
const setOption = { PXAT: option.expireAt?.toDate().getTime() };
|
|
22
|
+
await this.redis.set(`${this.refName}:${topic}:${key}`, value, setOption);
|
|
23
|
+
}
|
|
24
|
+
async get(topic, key) {
|
|
25
|
+
const value = await this.redis.get(`${this.refName}:${topic}:${key}`);
|
|
26
|
+
return value;
|
|
27
|
+
}
|
|
28
|
+
async delete(topic, key) {
|
|
29
|
+
await this.redis.del(`${this.refName}:${topic}:${key}`);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
class SearchDatabase {
|
|
33
|
+
constructor(refName, meili) {
|
|
34
|
+
this.refName = refName;
|
|
35
|
+
this.meili = meili;
|
|
36
|
+
this.logger = new Logger(`${refName}Search`);
|
|
37
|
+
this.index = meili.index(lowerlize(refName));
|
|
38
|
+
}
|
|
39
|
+
logger;
|
|
40
|
+
index;
|
|
41
|
+
async searchIds(searchText, option = {}) {
|
|
42
|
+
const { skip = 0, limit = DEFAULT_PAGE_SIZE, sort } = option;
|
|
43
|
+
if (!searchText) {
|
|
44
|
+
const { results, total } = await this.index.getDocuments({ offset: skip ?? 0, limit: limit ?? 0 });
|
|
45
|
+
return { ids: results.map((result) => result.id), total };
|
|
46
|
+
}
|
|
47
|
+
const { hits, estimatedTotalHits } = await this.index.search(searchText, {
|
|
48
|
+
offset: skip ?? 0,
|
|
49
|
+
limit: limit ?? 0,
|
|
50
|
+
sort: sort ?? [],
|
|
51
|
+
filter: option.filter,
|
|
52
|
+
attributesToRetrieve: ["id"]
|
|
53
|
+
});
|
|
54
|
+
return { ids: hits.map((hit) => hit.id), total: estimatedTotalHits };
|
|
55
|
+
}
|
|
56
|
+
async count(searchText, option = {}) {
|
|
57
|
+
const { skip = 0, limit = DEFAULT_PAGE_SIZE, sort = "" } = option;
|
|
58
|
+
if (!searchText) {
|
|
59
|
+
const { results, total } = await this.index.getDocuments({ offset: skip ?? 0, limit: limit ?? 0 });
|
|
60
|
+
return total;
|
|
61
|
+
}
|
|
62
|
+
const { hits, estimatedTotalHits } = await this.index.search(searchText, {
|
|
63
|
+
offset: skip ?? 0,
|
|
64
|
+
limit: limit ?? 0,
|
|
65
|
+
filter: option.filter,
|
|
66
|
+
attributesToRetrieve: ["id"]
|
|
67
|
+
});
|
|
68
|
+
return estimatedTotalHits;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
class DatabaseModelStorage {
|
|
72
|
+
__ModelType__ = "DatabaseModelStorage";
|
|
73
|
+
}
|
|
74
|
+
const databaseModelOf = (database, model, redis, meili) => {
|
|
75
|
+
//! 잘되는지 확인 필요
|
|
76
|
+
const [modelName, className] = [database.refName, capitalize(database.refName)];
|
|
77
|
+
const insightFieldMetas = getFieldMetas(database.Insight);
|
|
78
|
+
const accumulator = Object.fromEntries(insightFieldMetas.map((fieldMeta) => [fieldMeta.key, fieldMeta.accumulate]));
|
|
79
|
+
const defaultInsight = Object.fromEntries(insightFieldMetas.map((fieldMeta) => [fieldMeta.key, fieldMeta.default]));
|
|
80
|
+
const makeSafeQuery = (query) => ({ removedAt: { $exists: false }, ...query ?? {} });
|
|
81
|
+
const makeSafeMatchStage = (query) => ({
|
|
82
|
+
$match: { removedAt: { $exists: false }, ...convertAggregateMatch(query) }
|
|
83
|
+
});
|
|
84
|
+
const getListQuery = (query, queryOption) => {
|
|
85
|
+
const find = makeSafeQuery(query);
|
|
86
|
+
const sort = getFilterSort(database.Filter, queryOption?.sort ?? "latest");
|
|
87
|
+
const skip = queryOption?.skip ?? 0;
|
|
88
|
+
const limit = queryOption?.limit === null ? DEFAULT_PAGE_SIZE : queryOption?.limit ?? 0;
|
|
89
|
+
const select = queryOption?.select;
|
|
90
|
+
const sample = queryOption?.sample;
|
|
91
|
+
return { find, sort, skip, limit, select, sample };
|
|
92
|
+
};
|
|
93
|
+
const getFindQuery = (query, queryOption) => {
|
|
94
|
+
const find = makeSafeQuery(query);
|
|
95
|
+
const sort = getFilterSort(database.Filter, queryOption?.sort ?? "latest");
|
|
96
|
+
const skip = queryOption?.skip ?? 0;
|
|
97
|
+
const select = queryOption?.select;
|
|
98
|
+
const sample = queryOption?.sample ?? false;
|
|
99
|
+
return { find, sort, skip, select, sample };
|
|
100
|
+
};
|
|
101
|
+
const getSearchSort = (sortKey) => {
|
|
102
|
+
const sort = getFilterSort(database.Filter, sortKey ?? "latest");
|
|
103
|
+
return Object.entries(sort).map(([key, value]) => `${key}:${value === 1 ? "asc" : "desc"}`);
|
|
104
|
+
};
|
|
105
|
+
const loader = createLoader(model);
|
|
106
|
+
const cacheDatabase = new CacheDatabase(database.refName, redis);
|
|
107
|
+
const searchDatabase = new SearchDatabase(database.refName, meili);
|
|
108
|
+
const DatabaseModel = Reflect.getMetadata(database.refName, DatabaseModelStorage.prototype) ?? class DatabaseModel {
|
|
109
|
+
logger = new Logger(`${modelName}Model`);
|
|
110
|
+
__model = model;
|
|
111
|
+
__cache = cacheDatabase;
|
|
112
|
+
__search = searchDatabase;
|
|
113
|
+
__loader = loader;
|
|
114
|
+
async __list(query, queryOption) {
|
|
115
|
+
const { find, sort, skip, limit, select, sample } = getListQuery(query, queryOption);
|
|
116
|
+
return sample ? await this.__model.sample(find, limit) : await this.__model.find(find, select).sort(sort).skip(skip).limit(limit);
|
|
117
|
+
}
|
|
118
|
+
async __listIds(query, queryOption) {
|
|
119
|
+
const { find, sort, skip, limit, sample } = getListQuery(query, queryOption);
|
|
120
|
+
return (sample ? await this.__model.sample(find, limit, [{ $project: { _id: 1 } }]) : await this.__model.find(find).sort(sort).skip(skip).limit(limit).select("_id")).map(({ _id }) => _id.toString());
|
|
121
|
+
}
|
|
122
|
+
async __find(query, queryOption) {
|
|
123
|
+
const { find, sort, skip, select, sample } = getFindQuery(query, queryOption);
|
|
124
|
+
const doc = sample ? await this.__model.sampleOne(find) : await this.__model.findOne(find, select).sort(sort).skip(skip);
|
|
125
|
+
if (!doc)
|
|
126
|
+
return null;
|
|
127
|
+
return doc;
|
|
128
|
+
}
|
|
129
|
+
async __findId(query, queryOption) {
|
|
130
|
+
const { find, sort, skip, sample } = getFindQuery(query, queryOption);
|
|
131
|
+
const doc = sample ? await this.__model.sampleOne(find, [{ $project: { _id: 1 } }]) : await this.__model.findOne(find).sort(sort).skip(skip).select("_id");
|
|
132
|
+
if (!doc)
|
|
133
|
+
return null;
|
|
134
|
+
return doc._id.toString();
|
|
135
|
+
}
|
|
136
|
+
async __pick(query, queryOption) {
|
|
137
|
+
const { find, sort, skip, select, sample } = getFindQuery(query, queryOption);
|
|
138
|
+
const doc = sample ? await this.__model.sampleOne(find) : await this.__model.findOne(find, select).sort(sort).skip(skip);
|
|
139
|
+
if (!doc)
|
|
140
|
+
throw new Error(`No Document (${database.refName}): ${JSON.stringify(query)}`);
|
|
141
|
+
return doc;
|
|
142
|
+
}
|
|
143
|
+
async __pickId(query, queryOption) {
|
|
144
|
+
const { find, sort, skip, sample } = getFindQuery(query, queryOption);
|
|
145
|
+
const doc = sample ? await this.__model.sampleOne(find, [{ $project: { _id: 1 } }]) : await this.__model.findOne(find).sort(sort).skip(skip).select("_id");
|
|
146
|
+
if (!doc)
|
|
147
|
+
throw new Error(`No Document (${database.refName}): ${JSON.stringify(query)}`);
|
|
148
|
+
return doc._id.toString();
|
|
149
|
+
}
|
|
150
|
+
async __exists(query) {
|
|
151
|
+
const find = makeSafeQuery(query);
|
|
152
|
+
const existingId = await this.__model.exists(find);
|
|
153
|
+
return existingId?.toString() ?? null;
|
|
154
|
+
}
|
|
155
|
+
async __count(query) {
|
|
156
|
+
const find = makeSafeQuery(query);
|
|
157
|
+
return await this.__model.countDocuments(find);
|
|
158
|
+
}
|
|
159
|
+
async __insight(query) {
|
|
160
|
+
if (!accumulator)
|
|
161
|
+
throw new Error(`No Insight (${database.refName})`);
|
|
162
|
+
const res = await this.__model.aggregate([
|
|
163
|
+
makeSafeMatchStage(query),
|
|
164
|
+
{ $group: { _id: null, ...accumulator } }
|
|
165
|
+
]);
|
|
166
|
+
const data = res[0];
|
|
167
|
+
return data ?? defaultInsight;
|
|
168
|
+
}
|
|
169
|
+
async getDefaultSummary(addQuery = {}) {
|
|
170
|
+
if (!database.Summary)
|
|
171
|
+
return {};
|
|
172
|
+
const fieldMetas = getFieldMetas(database.Summary);
|
|
173
|
+
const summary = {};
|
|
174
|
+
await Promise.all(
|
|
175
|
+
fieldMetas.filter((fieldMeta) => !!fieldMeta.query).map(async (fieldMeta) => {
|
|
176
|
+
const query = typeof fieldMeta.query === "function" ? fieldMeta.query() : fieldMeta.query;
|
|
177
|
+
summary[fieldMeta.key] = query ? await model.countDocuments({ ...query, ...addQuery }) : fieldMeta.default;
|
|
178
|
+
})
|
|
179
|
+
);
|
|
180
|
+
return summary;
|
|
181
|
+
}
|
|
182
|
+
async [`get${className}`](id) {
|
|
183
|
+
const doc = await this.__loader.load(id);
|
|
184
|
+
if (!doc)
|
|
185
|
+
throw new Error(`No Document (${database.refName}): ${id}`);
|
|
186
|
+
return doc;
|
|
187
|
+
}
|
|
188
|
+
async [`load${className}`](id) {
|
|
189
|
+
return id ? await this.__loader.load(id) : null;
|
|
190
|
+
}
|
|
191
|
+
async [`load${className}Many`](ids) {
|
|
192
|
+
return await this.__loader.loadMany(ids);
|
|
193
|
+
}
|
|
194
|
+
async [`create${className}`](data) {
|
|
195
|
+
return await new this.__model(data).save();
|
|
196
|
+
}
|
|
197
|
+
async [`update${className}`](id, data) {
|
|
198
|
+
const doc = await this.__model.pickById(id);
|
|
199
|
+
return await doc.set(data).save();
|
|
200
|
+
}
|
|
201
|
+
async [`remove${className}`](id) {
|
|
202
|
+
const doc = await this.__model.pickById(id);
|
|
203
|
+
return await doc.set({ removedAt: dayjs() }).save();
|
|
204
|
+
}
|
|
205
|
+
async [`search${className}`](searchText, queryOption = {}) {
|
|
206
|
+
const { skip, limit, sort } = queryOption;
|
|
207
|
+
const { ids, total } = await this.__search.searchIds(searchText, { skip, limit, sort: getSearchSort(sort) });
|
|
208
|
+
const docs = await this.__loader.loadMany(ids);
|
|
209
|
+
return { docs, count: total };
|
|
210
|
+
}
|
|
211
|
+
async [`searchDocs${className}`](searchText, queryOption = {}) {
|
|
212
|
+
const { skip, limit, sort } = queryOption;
|
|
213
|
+
const { ids } = await this.__search.searchIds(searchText, { skip, limit, sort: getSearchSort(sort) });
|
|
214
|
+
return await this.__loader.loadMany(ids);
|
|
215
|
+
}
|
|
216
|
+
async [`searchCount${className}`](searchText) {
|
|
217
|
+
return await this.__search.count(searchText);
|
|
218
|
+
}
|
|
219
|
+
};
|
|
220
|
+
DatabaseModel.prototype[className] = model;
|
|
221
|
+
DatabaseModel.prototype[`${modelName}Loader`] = loader;
|
|
222
|
+
DatabaseModel.prototype[`${modelName}Cache`] = cacheDatabase;
|
|
223
|
+
DatabaseModel.prototype[`${modelName}Search`] = searchDatabase;
|
|
224
|
+
const getQueryDataFromKey = (queryKey, args) => {
|
|
225
|
+
const lastArg = args.at(-1);
|
|
226
|
+
const hasQueryOption = lastArg && typeof lastArg === "object" && (typeof lastArg.select === "object" || typeof lastArg.skip === "number" || typeof lastArg.limit === "number" || typeof lastArg.sort === "string");
|
|
227
|
+
const queryFn = getFilterQuery(database.Filter, queryKey);
|
|
228
|
+
const query = queryFn(...hasQueryOption ? args.slice(0, -1) : args);
|
|
229
|
+
const queryOption = hasQueryOption ? lastArg : {};
|
|
230
|
+
return { query, queryOption };
|
|
231
|
+
};
|
|
232
|
+
const filterKeyMetaMap = getFilterKeyMetaMapOnPrototype(database.Filter.prototype);
|
|
233
|
+
const queryKeys = [...filterKeyMetaMap.keys()];
|
|
234
|
+
queryKeys.forEach((queryKey) => {
|
|
235
|
+
const queryFn = getFilterQuery(database.Filter, queryKey);
|
|
236
|
+
DatabaseModel.prototype[`list${capitalize(queryKey)}`] = async function(...args) {
|
|
237
|
+
const { query, queryOption } = getQueryDataFromKey(queryKey, args);
|
|
238
|
+
return this.__list(query, queryOption);
|
|
239
|
+
};
|
|
240
|
+
DatabaseModel.prototype[`listIds${capitalize(queryKey)}`] = async function(...args) {
|
|
241
|
+
const { query, queryOption } = getQueryDataFromKey(queryKey, args);
|
|
242
|
+
return this.__listIds(query, queryOption);
|
|
243
|
+
};
|
|
244
|
+
DatabaseModel.prototype[`find${capitalize(queryKey)}`] = async function(...args) {
|
|
245
|
+
const { query, queryOption } = getQueryDataFromKey(queryKey, args);
|
|
246
|
+
return this.__find(query, queryOption);
|
|
247
|
+
};
|
|
248
|
+
DatabaseModel.prototype[`findId${capitalize(queryKey)}`] = async function(...args) {
|
|
249
|
+
const { query, queryOption } = getQueryDataFromKey(queryKey, args);
|
|
250
|
+
return this.__findId(query, queryOption);
|
|
251
|
+
};
|
|
252
|
+
DatabaseModel.prototype[`pick${capitalize(queryKey)}`] = async function(...args) {
|
|
253
|
+
const { query, queryOption } = getQueryDataFromKey(queryKey, args);
|
|
254
|
+
return this.__pick(query, queryOption);
|
|
255
|
+
};
|
|
256
|
+
DatabaseModel.prototype[`pickId${capitalize(queryKey)}`] = async function(...args) {
|
|
257
|
+
const { query, queryOption } = getQueryDataFromKey(queryKey, args);
|
|
258
|
+
return this.__pickId(query, queryOption);
|
|
259
|
+
};
|
|
260
|
+
DatabaseModel.prototype[`exists${capitalize(queryKey)}`] = async function(...args) {
|
|
261
|
+
const query = queryFn(...args);
|
|
262
|
+
return this.__exists(query);
|
|
263
|
+
};
|
|
264
|
+
DatabaseModel.prototype[`count${capitalize(queryKey)}`] = async function(...args) {
|
|
265
|
+
const query = queryFn(...args);
|
|
266
|
+
return this.__count(query);
|
|
267
|
+
};
|
|
268
|
+
DatabaseModel.prototype[`insight${capitalize(queryKey)}`] = async function(...args) {
|
|
269
|
+
const query = queryFn(...args);
|
|
270
|
+
return this.__insight(query);
|
|
271
|
+
};
|
|
272
|
+
});
|
|
273
|
+
const loaderMetas = getLoaderMetas(database.Model);
|
|
274
|
+
loaderMetas.forEach((loaderMeta) => {
|
|
275
|
+
const loader2 = loaderMeta.type === "Field" ? createLoader(model, loaderMeta.fieldName) : loaderMeta.type === "ArrayField" ? createArrayLoader(model, loaderMeta.fieldName) : loaderMeta.type === "Query" ? createQueryLoader(model, loaderMeta.queryKeys ?? []) : null;
|
|
276
|
+
DatabaseModel.prototype[loaderMeta.key] = loader2;
|
|
277
|
+
});
|
|
278
|
+
Object.getOwnPropertyNames(database.Model.prototype).forEach((key) => {
|
|
279
|
+
DatabaseModel.prototype[key] = database.Model.prototype[key];
|
|
280
|
+
});
|
|
281
|
+
const createDescriptor = Object.getOwnPropertyDescriptor(DatabaseModel.prototype, `create${className}`);
|
|
282
|
+
if (createDescriptor)
|
|
283
|
+
Transaction()(DatabaseModel.prototype, `create${className}`, createDescriptor);
|
|
284
|
+
const updateDescriptor = Object.getOwnPropertyDescriptor(DatabaseModel.prototype, `update${className}`);
|
|
285
|
+
if (updateDescriptor)
|
|
286
|
+
Transaction()(DatabaseModel.prototype, `update${className}`, updateDescriptor);
|
|
287
|
+
const removeDescriptor = Object.getOwnPropertyDescriptor(DatabaseModel.prototype, `remove${className}`);
|
|
288
|
+
if (removeDescriptor)
|
|
289
|
+
Transaction()(DatabaseModel.prototype, `remove${className}`, removeDescriptor);
|
|
290
|
+
Reflect.defineMetadata(database.refName, DatabaseModel, DatabaseModelStorage.prototype);
|
|
291
|
+
return new DatabaseModel();
|
|
292
|
+
};
|
|
293
|
+
export {
|
|
294
|
+
databaseModelOf
|
|
295
|
+
};
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import {
|
|
2
|
+
getClassMeta,
|
|
3
|
+
getFieldMetaMap,
|
|
4
|
+
setFieldMetaMap
|
|
5
|
+
} from "@akanjs/constant";
|
|
6
|
+
const dbOf = (refName, Input, Doc, Model2, Middleware, Obj, Insight, Filter, Summary) => {
|
|
7
|
+
return { refName, Input, Doc, Model: Model2, Middleware, Obj, Insight, Filter, Summary };
|
|
8
|
+
};
|
|
9
|
+
class InputDatabaseStorage {
|
|
10
|
+
}
|
|
11
|
+
class DocumentDatabaseStorage {
|
|
12
|
+
}
|
|
13
|
+
class ModelDatabaseStorage {
|
|
14
|
+
}
|
|
15
|
+
class MiddlewareDatabaseStorage {
|
|
16
|
+
}
|
|
17
|
+
const getAllDatabaseModelNames = () => {
|
|
18
|
+
const modelNames = Reflect.getMetadataKeys(ModelDatabaseStorage.prototype) ?? [];
|
|
19
|
+
return modelNames;
|
|
20
|
+
};
|
|
21
|
+
const Database = {
|
|
22
|
+
Input: (returns) => {
|
|
23
|
+
return function(target) {
|
|
24
|
+
const modelRef = returns();
|
|
25
|
+
const classMeta = getClassMeta(modelRef);
|
|
26
|
+
Reflect.defineMetadata(classMeta.refName, target, InputDatabaseStorage.prototype);
|
|
27
|
+
};
|
|
28
|
+
},
|
|
29
|
+
Document: (returns) => {
|
|
30
|
+
return function(target) {
|
|
31
|
+
const modelRef = returns();
|
|
32
|
+
const classMeta = getClassMeta(modelRef);
|
|
33
|
+
Reflect.defineMetadata(classMeta.refName, target, DocumentDatabaseStorage.prototype);
|
|
34
|
+
};
|
|
35
|
+
},
|
|
36
|
+
Model: (returns) => {
|
|
37
|
+
return function(target) {
|
|
38
|
+
const modelRef = returns();
|
|
39
|
+
const classMeta = getClassMeta(modelRef);
|
|
40
|
+
Reflect.defineMetadata(classMeta.refName, target, ModelDatabaseStorage.prototype);
|
|
41
|
+
};
|
|
42
|
+
},
|
|
43
|
+
Middleware: (returns) => {
|
|
44
|
+
return function(target) {
|
|
45
|
+
const modelRef = returns();
|
|
46
|
+
const classMeta = getClassMeta(modelRef);
|
|
47
|
+
Reflect.defineMetadata(classMeta.refName, target, MiddlewareDatabaseStorage.prototype);
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
};
|
|
51
|
+
const Model = (docRef, cnst) => {
|
|
52
|
+
class DefaultModel {
|
|
53
|
+
}
|
|
54
|
+
return DefaultModel;
|
|
55
|
+
};
|
|
56
|
+
const into = Model;
|
|
57
|
+
const AddModel = (modelRef, cnst) => {
|
|
58
|
+
return modelRef;
|
|
59
|
+
};
|
|
60
|
+
const inside = AddModel;
|
|
61
|
+
const by = (modelRef, addRef) => {
|
|
62
|
+
if (!addRef)
|
|
63
|
+
return InputOrDocument(modelRef);
|
|
64
|
+
return AddInputOrDocument(modelRef, addRef);
|
|
65
|
+
};
|
|
66
|
+
const InputOrDocument = (inputRef) => {
|
|
67
|
+
return inputRef;
|
|
68
|
+
};
|
|
69
|
+
const AddInputOrDocument = (modelRef, addRef) => {
|
|
70
|
+
const fieldMetaMap = getFieldMetaMap(modelRef);
|
|
71
|
+
const addFieldMetas = getFieldMetaMap(addRef);
|
|
72
|
+
setFieldMetaMap(modelRef, new Map([...fieldMetaMap, ...addFieldMetas]));
|
|
73
|
+
return modelRef;
|
|
74
|
+
};
|
|
75
|
+
const beyond = (model, doc) => {
|
|
76
|
+
return class Middleware {
|
|
77
|
+
onSchema(schema) {
|
|
78
|
+
}
|
|
79
|
+
};
|
|
80
|
+
};
|
|
81
|
+
export {
|
|
82
|
+
Database,
|
|
83
|
+
DocumentDatabaseStorage,
|
|
84
|
+
InputDatabaseStorage,
|
|
85
|
+
MiddlewareDatabaseStorage,
|
|
86
|
+
ModelDatabaseStorage,
|
|
87
|
+
beyond,
|
|
88
|
+
by,
|
|
89
|
+
dbOf,
|
|
90
|
+
getAllDatabaseModelNames,
|
|
91
|
+
inside,
|
|
92
|
+
into
|
|
93
|
+
};
|
package/src/index.mjs
ADDED
package/src/schema.mjs
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { dayjs } from "@akanjs/base";
|
|
2
|
+
import { isValidDate } from "@akanjs/common";
|
|
3
|
+
import { isValidObjectId, Types } from "mongoose";
|
|
4
|
+
const getDefaultSchemaOptions = () => ({
|
|
5
|
+
toJSON: { getters: false, virtuals: true },
|
|
6
|
+
toObject: { getters: false, virtuals: true },
|
|
7
|
+
_id: true,
|
|
8
|
+
id: true,
|
|
9
|
+
timestamps: true,
|
|
10
|
+
methods: {
|
|
11
|
+
refresh: async function() {
|
|
12
|
+
Object.assign(this, await this.constructor.findById(this._id));
|
|
13
|
+
return this;
|
|
14
|
+
}
|
|
15
|
+
},
|
|
16
|
+
statics: {
|
|
17
|
+
pickOne: async function(query, projection) {
|
|
18
|
+
const doc = await this.findOne(query, projection);
|
|
19
|
+
if (!doc)
|
|
20
|
+
throw new Error("No Document");
|
|
21
|
+
return doc;
|
|
22
|
+
},
|
|
23
|
+
pickById: async function(docId, projection) {
|
|
24
|
+
if (!docId)
|
|
25
|
+
throw new Error("No Document ID");
|
|
26
|
+
const doc = await this.findById(docId, projection);
|
|
27
|
+
if (!doc)
|
|
28
|
+
throw new Error("No Document");
|
|
29
|
+
return doc;
|
|
30
|
+
},
|
|
31
|
+
sample: async function(query, size = 1, aggregations = []) {
|
|
32
|
+
const objs = await this.aggregate([
|
|
33
|
+
{ $match: convertAggregateMatch(query) },
|
|
34
|
+
{ $sample: { size } },
|
|
35
|
+
...aggregations
|
|
36
|
+
]);
|
|
37
|
+
return objs.map((obj) => new this(obj));
|
|
38
|
+
},
|
|
39
|
+
sampleOne: async function(query, aggregations = []) {
|
|
40
|
+
const obj = await this.aggregate([
|
|
41
|
+
{ $match: convertAggregateMatch(query) },
|
|
42
|
+
{ $sample: { size: 1 } },
|
|
43
|
+
...aggregations
|
|
44
|
+
]);
|
|
45
|
+
return obj.length ? new this(obj[0]) : null;
|
|
46
|
+
},
|
|
47
|
+
addSummary: async function(prefix = "total", num = 1) {
|
|
48
|
+
const update = Array.isArray(prefix) ? {
|
|
49
|
+
$inc: {
|
|
50
|
+
...prefix.reduce((acc, cur) => ({ ...acc, [`${cur}${this.modelName}`]: num }), {})
|
|
51
|
+
}
|
|
52
|
+
} : { $inc: { [`${prefix}${this.modelName}`]: num } };
|
|
53
|
+
await this.db.collection("summaries").updateOne({ status: "active" }, update);
|
|
54
|
+
},
|
|
55
|
+
moveSummary: async function(prev, next, num = 1) {
|
|
56
|
+
await this.db.collection("summaries").updateOne(
|
|
57
|
+
{ status: "active" },
|
|
58
|
+
{
|
|
59
|
+
$inc: {
|
|
60
|
+
[`${prev}${this.modelName}`]: -num,
|
|
61
|
+
[`${next}${this.modelName}`]: num
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
);
|
|
65
|
+
},
|
|
66
|
+
subSummary: async function(prefix = "total", num = 1) {
|
|
67
|
+
const update = Array.isArray(prefix) ? {
|
|
68
|
+
$inc: {
|
|
69
|
+
...prefix.reduce((acc, cur) => ({ ...acc, [`${cur}${this.modelName}`]: -num }), {})
|
|
70
|
+
}
|
|
71
|
+
} : { $inc: { [`${prefix}${this.modelName}`]: -num } };
|
|
72
|
+
await this.db.collection("summaries").updateOne({ status: "active" }, update);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
const convertOperatorValue = (value) => {
|
|
77
|
+
if (Array.isArray(value))
|
|
78
|
+
return value.map((v) => convertOperatorValue(v));
|
|
79
|
+
else if (!value)
|
|
80
|
+
return value;
|
|
81
|
+
else if (isValidObjectId(value))
|
|
82
|
+
return new Types.ObjectId(value);
|
|
83
|
+
else if (isValidDate(value))
|
|
84
|
+
return dayjs(value).toDate();
|
|
85
|
+
else if (value.constructor !== Object)
|
|
86
|
+
return value;
|
|
87
|
+
else if (typeof value !== "object")
|
|
88
|
+
return value;
|
|
89
|
+
else
|
|
90
|
+
return Object.fromEntries(
|
|
91
|
+
Object.entries(value).map(([key, value2]) => [key, convertOperatorValue(value2)])
|
|
92
|
+
);
|
|
93
|
+
};
|
|
94
|
+
const convertAggregateMatch = (query) => {
|
|
95
|
+
return Object.fromEntries(
|
|
96
|
+
Object.entries(query).map(([key, value]) => {
|
|
97
|
+
return [key, convertOperatorValue(value)];
|
|
98
|
+
})
|
|
99
|
+
);
|
|
100
|
+
};
|
|
101
|
+
export {
|
|
102
|
+
convertAggregateMatch,
|
|
103
|
+
getDefaultSchemaOptions
|
|
104
|
+
};
|
package/src/types.mjs
ADDED
|
File without changes
|