1 | "use strict"
|
2 | const {
|
3 | pick, keys, map, reject, filter, groupBy, includes, fromPairs, omitBy, findKey, get,
|
4 | isNull, isObject, isFunction, isString,
|
5 | defaults } = require("lodash")
|
6 | const deepMerge = require("@iin-mdc/koa-utils/lib/deep-merge-with-symbols")
|
7 |
|
8 | function isEmpty(object) {
|
9 | return !object || Object.getOwnPropertySymbols(object).concat(Object.keys(object)).length === 0
|
10 | }
|
11 |
|
12 | function filterOptions(model, query) {
|
13 | let { like, gt, or } = model.sequelize.Sequelize.Op
|
14 | let options = {}
|
15 | const attributes = model.rawAttributes
|
16 |
|
17 | options.where = pick(query, keys(attributes))
|
18 |
|
19 | if (query.q && model.searchAttributes) {
|
20 | let search = map(model.searchAttributes, attr => {
|
21 | let s = {}
|
22 | if (query.q.match(/^\*|\*$/)) {
|
23 | s[attr] = { [like]: query.q.replace(/^\*|\*$/g, "%") }
|
24 | } else {
|
25 | s[attr] = { [like]: `${query.q}%` }
|
26 | }
|
27 | return s
|
28 | })
|
29 | options.where = deepMerge(options.where, { [or]: search })
|
30 | }
|
31 |
|
32 | let { order, limit, offset, lastUpdatedAt } = query
|
33 | if (order) {
|
34 | let [field, dir] = order.split(" ")
|
35 | if (field == "id") field = model.primaryKeyAttribute
|
36 | if (attributes[field])
|
37 | options.order = [[field, dir]]
|
38 | else if (field.indexOf(".") > 0) {
|
39 | const [associationName, associationField] = field.split(".")
|
40 | if (model.associations[associationName]) {
|
41 | const association = model.associations[associationName]
|
42 | options.order = [[association, associationField, dir]]
|
43 | }
|
44 | }
|
45 | }
|
46 | if (limit) options.limit = parseInt(limit, 10)
|
47 | if (offset) options.offset = parseInt(offset, 10)
|
48 |
|
49 |
|
50 | if (query.ids) {
|
51 | options.where[model.primaryKeyAttribute] = isString(query.ids) ? query.ids.split(",") : query.ids
|
52 | }
|
53 | if (lastUpdatedAt) {
|
54 | options.where.updatedAt = { [gt]: lastUpdatedAt }
|
55 | }
|
56 | return options
|
57 | }
|
58 |
|
59 | function getAssociationsFilter(model, query = {}, associations_filter = null) {
|
60 | let params = getAssociationsParams(model, query, associations_filter)
|
61 | if (params.include) {
|
62 | return { include: filter(params.include, options => !isEmpty(options.where)) }
|
63 | }
|
64 | return {}
|
65 | }
|
66 | function getAssociationsParams(model, query = {}, associations_filter = null) {
|
67 | const _keys = filter(keys(query), (k) => includes(k, "."))
|
68 | const associationKeys = groupBy(_keys, (k) => k.split(".")[0])
|
69 | associations_filter = associations_filter || keys(model.associations)
|
70 |
|
71 | if (query.order && query.order.indexOf(".") > 0) {
|
72 | const [orderAssociation, orderField, dir] = query.order.split(/[\\. ]/g)
|
73 | if (orderAssociation && orderField && dir && !includes(associations_filter, orderAssociation)) {
|
74 | associations_filter.push(orderAssociation)
|
75 | }
|
76 | }
|
77 |
|
78 | let include = map(model.associations, (association, name) => {
|
79 | let config = { model: association.target, as: association.as }
|
80 | let queryKeys = associationKeys[name]
|
81 | if (queryKeys) {
|
82 | config.where = fromPairs(map(queryKeys, key => [key.split(".")[1], query[key]]))
|
83 | }
|
84 | if (config.where || includes(associations_filter, name)) {
|
85 | return config
|
86 | } else {
|
87 | return null
|
88 | }
|
89 | })
|
90 | include = reject(include, isNull)
|
91 | return isEmpty(include) ? {} : { include }
|
92 | }
|
93 | function requiredIncludes(include) {
|
94 | return include
|
95 | .map(includeQ => {
|
96 | if (isObject(includeQ) && includeQ.required) {
|
97 | return ({
|
98 | ...includeQ,
|
99 | include: requiredIncludes(includeQ.include || [])
|
100 | })
|
101 | }
|
102 | return null
|
103 | })
|
104 | .filter(includeQ => !isNull(includeQ))
|
105 | }
|
106 | function createCountQuery(model, query) {
|
107 | const include = requiredIncludes(query.include || [])
|
108 | const distinctQuery = include.length > 0 && model.primaryKeyAttribute ? {
|
109 | distinct: true,
|
110 | col: model.primaryKeyAttribute
|
111 | } : {
|
112 | col: model.primaryKeyAttribute
|
113 | }
|
114 |
|
115 | return ({
|
116 | ...distinctQuery,
|
117 | include: include,
|
118 | where: query.where
|
119 | })
|
120 | }
|
121 | async function fetchObject(ctx, id) {
|
122 | let dbQuery = { where: primaryKeyValue(ctx.model, id), rejectOnEmpty: true }
|
123 | if (ctx.associations)
|
124 | dbQuery = deepMerge(getAssociationsParams(ctx.model, {}, ctx.associations), dbQuery)
|
125 | return await ctx.model.findOne(await ctx.customize(ctx, dbQuery))
|
126 | }
|
127 |
|
128 | function primaryKeyValue(model, object) {
|
129 | let value = isObject(object) ? object[model.primaryKeyAttribute] : object
|
130 | return fromPairs([[model.primaryKeyAttribute, value]])
|
131 | }
|
132 |
|
133 | function ctxExtend(ctx, modelName, options) {
|
134 | const model = isString(modelName) ? ctx.models[modelName] : modelName
|
135 | let [callback, opts] = isFunction(options) ? [options, {}] : [null, options || {}]
|
136 | let defaultOptions = {
|
137 | model: model,
|
138 | customize: async (_ctx, dbQuery) => dbQuery,
|
139 | serialize: async object => omitBy(object.toJSON(), (_v, k) => k.match(/password|encrypted/)),
|
140 | callback: callback || get(options, findKey(pick(options, "update", "create", "destroy"), isFunction)),
|
141 | associations: opts.associations || model.preloadAssociations || []
|
142 | }
|
143 | return defaults(ctx, opts, defaultOptions)
|
144 | }
|
145 | const handlers = {
|
146 | auth: (ctx, next) => {
|
147 | if (ctx.state.allowedRoles) {
|
148 | if (ctx.state.user && includes(ctx.state.allowedRoles, ctx.state.user.role)) {
|
149 | return next()
|
150 | } else {
|
151 | ctx.status = 401
|
152 | }
|
153 | }
|
154 | else
|
155 | return next()
|
156 | },
|
157 | show: (...args) => async (ctx) => {
|
158 | let { model, serialize } = ctxExtend(ctx, ...args)
|
159 | let object = await fetchObject(ctx, ctx.params.id)
|
160 |
|
161 | ctx.body = {
|
162 | data: await serialize(object, ctx),
|
163 | meta: {
|
164 | id: model.primaryKeyAttribute
|
165 | }
|
166 | }
|
167 | },
|
168 | list: (...args) => async (ctx) => {
|
169 | const { model, serialize, customize, associations } = ctxExtend(ctx, ...args)
|
170 |
|
171 | const filter = filterOptions(model, ctx.request.query)
|
172 | const associationsFilter = getAssociationsFilter(model, ctx.request.query, associations)
|
173 | const associationsParams = getAssociationsParams(model, ctx.request.query, associations)
|
174 |
|
175 | const dbQuery = await customize(ctx, deepMerge({}, filter, associationsParams))
|
176 | const countQuery = createCountQuery(model, await customize(ctx, deepMerge({}, filter, associationsFilter)))
|
177 |
|
178 | const rows = await model.findAll(dbQuery)
|
179 | const count = await model.count(countQuery)
|
180 |
|
181 | ctx.body = {
|
182 | data: await Promise.all(rows.map(row => serialize(row, ctx))),
|
183 | meta: {
|
184 | total: count,
|
185 | id: model.primaryKeyAttribute
|
186 | }
|
187 | }
|
188 | },
|
189 | update: (...args) => async (ctx) => {
|
190 | let { model, serialize, callback } = ctxExtend(ctx, ...args)
|
191 | let object = await fetchObject(ctx, ctx.params.id)
|
192 |
|
193 | if (callback.length == 4) {
|
194 | let afterCalback = async () => { }
|
195 | await ctx.models.sequelize.transaction(async (transaction) => {
|
196 | transaction.then = (callback) => afterCalback = callback
|
197 | object = await callback(ctx, object, ctx.request.body, transaction)
|
198 | })
|
199 | await afterCalback()
|
200 | } else {
|
201 | object = await callback(ctx, object, ctx.request.body)
|
202 | }
|
203 | object = await fetchObject(ctx, object[model.primaryKeyAttribute])
|
204 | ctx.body = {
|
205 | data: await serialize(object, ctx),
|
206 | meta: {
|
207 | id: model.primaryKeyAttribute
|
208 | }
|
209 | }
|
210 | },
|
211 | create: (...args) => async (ctx) => {
|
212 | let { model, serialize, callback } = ctxExtend(ctx, ...args)
|
213 |
|
214 | let object = model.build({})
|
215 | if (callback.length == 4) {
|
216 | let afterCalback = async () => { }
|
217 | await ctx.models.sequelize.transaction(async (transaction) => {
|
218 | transaction.then = (callback) => afterCalback = callback
|
219 | object = await callback(ctx, object, ctx.request.body, transaction)
|
220 | })
|
221 | await afterCalback()
|
222 | } else {
|
223 | object = await callback(ctx, object, ctx.request.body)
|
224 | }
|
225 | object = await fetchObject(ctx, object[model.primaryKeyAttribute])
|
226 | ctx.body = {
|
227 | data: await serialize(object, ctx),
|
228 | meta: {
|
229 | id: model.primaryKeyAttribute
|
230 | }
|
231 | }
|
232 | },
|
233 | destroy: (...args) => async (ctx) => {
|
234 | let { model, callback } = ctxExtend(ctx, ...args)
|
235 | let object = await fetchObject(ctx, ctx.params.id)
|
236 | if (callback) {
|
237 | if (callback.length == 3) {
|
238 | await ctx.models.sequelize.transaction(async (transaction) => {
|
239 | object = await callback(ctx, object, transaction)
|
240 | })
|
241 | } else {
|
242 | object = await callback(ctx, object)
|
243 | }
|
244 | } else {
|
245 | await object.destroy()
|
246 | }
|
247 | ctx.body = {
|
248 | data: primaryKeyValue(model, object),
|
249 | meta: {
|
250 | id: model.primaryKeyAttribute
|
251 | }
|
252 | }
|
253 | }
|
254 | }
|
255 | module.exports = { filterOptions, getAssociationsParams, handlers } |
\ | No newline at end of file |