generated from hyper63/adapter-template
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathutils.ts
193 lines (182 loc) · 5.35 KB
/
utils.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
import { crocks, HyperErr, isHyperErr, R } from './deps.ts'
const { map, omit, ifElse, evolve, applyTo, propOr, always } = R
const { Async } = crocks
export async function mkdir(dir: string) {
try {
return await Deno.mkdir(dir, { recursive: true })
} catch (err) {
if (err instanceof Deno.errors.AlreadyExists) {
// already exists so return
return true
} else {
// unexpected error, maybe permissions, pass it along
throw err
}
}
}
export const handleHyperErr = ifElse(
isHyperErr,
Async.Resolved,
Async.Rejected,
)
export const toBulkOperations = map(
(d: Record<string, unknown> & { _deleted?: boolean; _update?: boolean }) => {
if (d._deleted) {
return { deleteOne: { filter: { _id: d._id } } }
} else {
return {
replaceOne: {
filter: { _id: d._id },
replacement: omit(['_update'], d),
/**
* Always insert the document, if not found,
* and replace otherwise
*/
upsert: true,
},
}
}
},
)
export const queryOptions = ({
limit,
skip,
fields,
sort,
}: {
limit?: number | string
skip?: number | string
fields?: string[]
sort?: string[] | { [field: string]: 'ASC' | 'DESC' }[]
}) => {
/**
* Notice use_index is not mapped here, as MongoDB
* internally chooses an index to use.
*
* So use_index is effectively ignored
*/
const options: {
limit?: number
skip?: number
projection?: { [field: string]: 0 | 1 }
sort?: { [field: string]: 1 | -1 }
} = {
/**
* See https://www.mongodb.com/docs/manual/reference/operator/aggregation/limit/
*/
...(limit ? { limit: Number(limit) } : { limit: 25 }),
...(skip ? { skip: Number(skip) } : {}),
...(fields
? {
projection: fields.reduce(
(acc, field) => ({ ...acc, [field]: 1 }),
/**
* Mongo will always return the _id, even if not present in the projection.
* You can explicitly instruct Mongo not to return the _id by setting it to 0
* in the projection.
*
* So we start with setting _id to 0. If it is in the fields array, it will overridden
* with a 1, which will cause it to be included in the projection, which is what we want
*
* See https://www.mongodb.com/docs/manual/tutorial/project-fields-from-query-results/#suppress-_id-field
*/
{ _id: 0 },
),
}
: {}),
...(sort ? { sort: mapSort(sort) } : {}),
}
return options
}
/**
* Map the hyper sort syntax to the mongo sort syntax
*
* See https://www.mongodb.com/docs/manual/reference/operator/aggregation/sort/
*
* TL;DR: 1 is ascending, -1 is descending
*/
export const mapSort = (
sort: string[] | { [field: string]: 'ASC' | 'DESC' }[],
): { [field: string]: 1 | -1 } => {
if (!sort || !sort.length) return {}
// deno-lint-ignore ban-ts-comment
// @ts-ignore
return sort.reduce((acc, cur) => {
/**
* The default order is ascending, if only the field name is provided
*/
if (typeof cur === 'string') return { ...acc, [cur]: 1 }
if (typeof cur === 'object') {
const key = Object.keys(cur)[0]
return { ...acc, [key]: cur[key] === 'DESC' ? -1 : 1 }
}
/**
* ignore the invalid sort value
*
* This should never happen because the wrapping zod schema would catch it
* but just to be explicit
*/
return acc
}, {} as { [field: string]: 1 | -1 })
}
/**
* Generate string templates using named keys
* to pull values from a provided dictionary
*/
const template =
(strings: TemplateStringsArray, ...keys: string[]) => (dict: Record<string, string>) => {
const result = [strings[0]]
keys.forEach((key, i) => {
result.push(dict[key], strings[i + 1])
})
return result.join('')
}
export const mongoErrToHyperErr =
// deno-lint-ignore no-explicit-any
(context: Record<string, string>) => (mongoErr: any) => {
if (isHyperErr(mongoErr)) return mongoErr
const params = evolve(
/**
* Apply the msg template to the provided context
* to produce the final msg on the HyperErr
*/
{ msg: applyTo({ ...context }) },
/**
* Map MongoDB statuses to corresponding HyperErr status
* and templated msg
*/
// deno-lint-ignore ban-ts-comment
// @ts-ignore
propOr(
{
status: mongoErr?.status || 500,
msg: always(mongoErr?.message || 'an error occurred'),
},
/**
* Each MongoDB error comes back with a code
*/
String(mongoErr.code),
/**
* A map of MongoDB error codes to HyperErr shapes,
* each containing a corresponding status and msg template
* to generate the msg on the HyperErr using the provided context
*
* TODO: add more mappings and corresponding tests
*
* See https://github.com/mongodb/mongo/blob/master/src/mongo/base/error_codes.yml
*/
{
'11000': {
status: 409,
msg: template`${'subject'} already exists`,
},
'86': {
status: 409,
msg: template`${'subject'} fields do not match the existing index with the same name`,
},
},
),
)
// deno-lint-ignore no-explicit-any
return HyperErr(params as any)
}