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("./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 |
|
94 | async function fetchObject(ctx, id) {
|
95 | let dbQuery = { where: primaryKeyValue(ctx.model, id), rejectOnEmpty: true }
|
96 | if (ctx.associations)
|
97 | dbQuery = deepMerge(getAssociationsParams(ctx.model, {}, ctx.associations), dbQuery)
|
98 | return await ctx.model.findOne(await ctx.customize(ctx, dbQuery))
|
99 | }
|
100 |
|
101 | function primaryKeyValue(model, object) {
|
102 | let value = isObject(object) ? object[model.primaryKeyAttribute] : object
|
103 | return fromPairs([[model.primaryKeyAttribute, value]])
|
104 | }
|
105 |
|
106 | function ctxExtend(ctx, modelName, options) {
|
107 | const model = isString(modelName) ? ctx.models[modelName] : modelName
|
108 | let [callback, opts] = isFunction(options) ? [options, {}] : [null, options || {}]
|
109 | let defaultOptions = {
|
110 | model: model,
|
111 | customize: async (_ctx, dbQuery) => dbQuery,
|
112 | serialize: async object => omitBy(object.toJSON(), (_v, k) => k.match(/password|encrypted/)),
|
113 | callback: callback || get(options, findKey(pick(options, "update", "create", "destroy"), isFunction)),
|
114 | associations: opts.associations || model.preloadAssociations || []
|
115 | }
|
116 | return defaults(ctx, opts, defaultOptions)
|
117 | }
|
118 | const handlers = {
|
119 | auth: (ctx, next) => {
|
120 | if (ctx.state.allowedRoles) {
|
121 | if (ctx.state.user && includes(ctx.state.allowedRoles, ctx.state.user.role)) {
|
122 | return next()
|
123 | } else {
|
124 | ctx.status = 401
|
125 | }
|
126 | }
|
127 | else
|
128 | return next()
|
129 | },
|
130 | show: (...args) => async (ctx) => {
|
131 | let { model, serialize } = ctxExtend(ctx, ...args)
|
132 | let object = await fetchObject(ctx, ctx.params.id)
|
133 |
|
134 | ctx.body = {
|
135 | data: await serialize(object, ctx),
|
136 | meta: {
|
137 | id: model.primaryKeyAttribute
|
138 | }
|
139 | }
|
140 | },
|
141 | list: (...args) => async (ctx) => {
|
142 | const { model, serialize, customize, associations } = ctxExtend(ctx, ...args)
|
143 |
|
144 | let filter = filterOptions(model, ctx.request.query)
|
145 | let associationsFilter = getAssociationsFilter(model, ctx.request.query, associations)
|
146 | let associationsParams = getAssociationsParams(model, ctx.request.query, associations)
|
147 |
|
148 | let dbQuery = await customize(ctx, deepMerge({}, filter, associationsParams))
|
149 | let countQuery = await customize(ctx, deepMerge({}, filter, associationsFilter))
|
150 |
|
151 | let rows = await model.findAll(dbQuery)
|
152 | let count = await model.count(countQuery)
|
153 |
|
154 | ctx.body = {
|
155 | data: await Promise.all(rows.map(row => serialize(row, ctx))),
|
156 | meta: {
|
157 | total: count,
|
158 | id: model.primaryKeyAttribute
|
159 | }
|
160 | }
|
161 | },
|
162 | update: (...args) => async (ctx) => {
|
163 | let { model, serialize, callback } = ctxExtend(ctx, ...args)
|
164 | let object = await fetchObject(ctx, ctx.params.id)
|
165 |
|
166 | if (callback.length == 4) {
|
167 | let afterCalback = async () => { }
|
168 | await ctx.models.sequelize.transaction(async (transaction) => {
|
169 | transaction.then = (callback) => afterCalback = callback
|
170 | object = await callback(ctx, object, ctx.request.body, transaction)
|
171 | })
|
172 | await afterCalback()
|
173 | } else {
|
174 | object = await callback(ctx, object, ctx.request.body)
|
175 | }
|
176 | object = await fetchObject(ctx, object[model.primaryKeyAttribute])
|
177 | ctx.body = {
|
178 | data: await serialize(object, ctx),
|
179 | meta: {
|
180 | id: model.primaryKeyAttribute
|
181 | }
|
182 | }
|
183 | },
|
184 | create: (...args) => async (ctx) => {
|
185 | let { model, serialize, callback } = ctxExtend(ctx, ...args)
|
186 |
|
187 | let object = model.build({})
|
188 | if (callback.length == 4) {
|
189 | let afterCalback = async () => { }
|
190 | await ctx.models.sequelize.transaction(async (transaction) => {
|
191 | transaction.then = (callback) => afterCalback = callback
|
192 | object = await callback(ctx, object, ctx.request.body, transaction)
|
193 | })
|
194 | await afterCalback()
|
195 | } else {
|
196 | object = await callback(ctx, object, ctx.request.body)
|
197 | }
|
198 | object = await fetchObject(ctx, object[model.primaryKeyAttribute])
|
199 | ctx.body = {
|
200 | data: await serialize(object, ctx),
|
201 | meta: {
|
202 | id: model.primaryKeyAttribute
|
203 | }
|
204 | }
|
205 | },
|
206 | destroy: (...args) => async (ctx) => {
|
207 | let { model, callback } = ctxExtend(ctx, ...args)
|
208 | let object = await fetchObject(ctx, ctx.params.id)
|
209 | if (callback) {
|
210 | if (callback.length == 3) {
|
211 | await ctx.models.sequelize.transaction(async (transaction) => {
|
212 | object = await callback(ctx, object, transaction)
|
213 | })
|
214 | } else {
|
215 | object = await callback(ctx, object)
|
216 | }
|
217 | } else {
|
218 | await object.destroy()
|
219 | }
|
220 | ctx.body = {
|
221 | data: primaryKeyValue(model, object),
|
222 | meta: {
|
223 | id: model.primaryKeyAttribute
|
224 | }
|
225 | }
|
226 | }
|
227 | }
|
228 | module.exports = { filterOptions, getAssociationsParams, handlers } |
\ | No newline at end of file |