@voxgig/apidef 5.7.0 → 5.8.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/dist/guide/heuristic01.js +8 -8
- package/dist/guide/heuristic01.js.map +1 -1
- package/dist/parse.js +12 -0
- package/dist/parse.js.map +1 -1
- package/dist/transform/args.js +9 -2
- package/dist/transform/args.js.map +1 -1
- package/dist/transform/entity.js +21 -1
- package/dist/transform/entity.js.map +1 -1
- package/dist/transform/field.js +60 -4
- package/dist/transform/field.js.map +1 -1
- package/dist/transform/flowstep.js +120 -10
- package/dist/transform/flowstep.js.map +1 -1
- package/dist/transform/operation.js +8 -5
- package/dist/transform/operation.js.map +1 -1
- package/dist/transform/select.js +1 -1
- package/dist/transform/select.js.map +1 -1
- package/dist/utility.d.ts +3 -1
- package/dist/utility.js +6 -0
- package/dist/utility.js.map +1 -1
- package/package.json +6 -6
- package/src/guide/heuristic01.ts +10 -8
- package/src/parse.ts +13 -0
- package/src/transform/args.ts +9 -2
- package/src/transform/entity.ts +24 -2
- package/src/transform/field.ts +54 -4
- package/src/transform/flowstep.ts +119 -11
- package/src/transform/operation.ts +8 -7
- package/src/transform/select.ts +1 -1
- package/src/utility.ts +7 -0
package/src/transform/args.ts
CHANGED
|
@@ -71,9 +71,16 @@ function resolveArgs(ment: ModelEntity, mop: ModelOp, mtarget: ModelTarget, argd
|
|
|
71
71
|
const touchedKeys = new Set<string>()
|
|
72
72
|
|
|
73
73
|
each(argdefs, (argdef: ParameterDef) => {
|
|
74
|
-
|
|
74
|
+
// Spec name as written (e.g. `dataType`) is what the rename map is keyed
|
|
75
|
+
// by; the snakified form is the user-friendly runtime identifier.
|
|
76
|
+
const specName = normalizeFieldName(argdef.name)
|
|
77
|
+
const orig = depluralize(snakify(specName))
|
|
75
78
|
const kind = ARG_KIND[argdef.in] ?? 'query'
|
|
76
|
-
|
|
79
|
+
// Rename map can be keyed by either the spec original (camelCase) or by
|
|
80
|
+
// the snakified form depending on which path went through heuristic01.
|
|
81
|
+
// Try both before falling through to `orig`.
|
|
82
|
+
const renameMap = mtarget.rename[kind]
|
|
83
|
+
const name = renameMap?.[specName] ?? renameMap?.[orig] ?? orig
|
|
77
84
|
const marg: ModelArg = {
|
|
78
85
|
name,
|
|
79
86
|
orig,
|
package/src/transform/entity.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
|
|
2
2
|
|
|
3
|
-
import { each } from 'jostraca'
|
|
3
|
+
import { each, snakify } from 'jostraca'
|
|
4
4
|
|
|
5
5
|
import type { TransformResult, Transform } from '../transform'
|
|
6
6
|
|
|
@@ -21,6 +21,11 @@ import type {
|
|
|
21
21
|
ModelEntity,
|
|
22
22
|
} from '../model'
|
|
23
23
|
|
|
24
|
+
import {
|
|
25
|
+
depluralize,
|
|
26
|
+
normalizeFieldName,
|
|
27
|
+
} from '../utility'
|
|
28
|
+
|
|
24
29
|
|
|
25
30
|
|
|
26
31
|
const entityTransform: Transform = async function(
|
|
@@ -67,7 +72,24 @@ function resolvePathList(guideEntity: GuideEntity, def: { paths: Record<string,
|
|
|
67
72
|
|
|
68
73
|
each(rename.param, (param: any) => {
|
|
69
74
|
const pI = parts.indexOf('{' + param.key$ + '}')
|
|
70
|
-
parts[pI] = '{' + param.val$ + '}'
|
|
75
|
+
if (pI >= 0) parts[pI] = '{' + param.val$ + '}'
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
// Implicit snake_case normalization for any path placeholder that wasn't
|
|
79
|
+
// explicitly renamed. apidef's args transform snake-cases param names
|
|
80
|
+
// (e.g. spec `platformKey` → param.name `platform_key`); without
|
|
81
|
+
// normalizing the placeholder to match, runtime URL substitution by
|
|
82
|
+
// param.name fails to fill `{platformKey}`.
|
|
83
|
+
rename.param = (rename.param as any) ?? {}
|
|
84
|
+
parts.forEach((part: string, i: number) => {
|
|
85
|
+
const m = part.match(/^\{(.+)\}$/)
|
|
86
|
+
if (!m) return
|
|
87
|
+
const placeholder = m[1]
|
|
88
|
+
const snake = depluralize(snakify(normalizeFieldName(placeholder)))
|
|
89
|
+
if (snake !== placeholder && (rename.param as any)[placeholder] === undefined) {
|
|
90
|
+
; (rename.param as any)[placeholder] = snake
|
|
91
|
+
parts[i] = '{' + snake + '}'
|
|
92
|
+
}
|
|
71
93
|
})
|
|
72
94
|
|
|
73
95
|
const pathdesc: PathDesc = {
|
package/src/transform/field.ts
CHANGED
|
@@ -119,9 +119,23 @@ function findFieldDefs(
|
|
|
119
119
|
if (responses) {
|
|
120
120
|
fieldSets = getx(responses, '200 content "application/json" schema') ??
|
|
121
121
|
getx(responses, '200 schema')
|
|
122
|
-
if ('
|
|
123
|
-
|
|
124
|
-
|
|
122
|
+
if ('list' == mop.name) {
|
|
123
|
+
// List responses commonly come in three shapes:
|
|
124
|
+
// 1. direct array — { type: array, items: { ...item } }
|
|
125
|
+
// 2. wrapper object — { properties: { items: [Item], page, ... } }
|
|
126
|
+
// (a single array-of-object property inside an object schema)
|
|
127
|
+
// 3. legacy "list of created items" under 201
|
|
128
|
+
// Resolve to the inner item schema when we can identify one
|
|
129
|
+
// unambiguously; otherwise fall through to the 200 schema as-is.
|
|
130
|
+
const unwrapped = unwrapArrayWrapper(fieldSets)
|
|
131
|
+
if (unwrapped) {
|
|
132
|
+
fieldSets = unwrapped
|
|
133
|
+
}
|
|
134
|
+
else {
|
|
135
|
+
const fromCreated = getx(responses, '201 content "application/json" schema items') ??
|
|
136
|
+
getx(responses, '201 schema items')
|
|
137
|
+
if (fromCreated) fieldSets = fromCreated
|
|
138
|
+
}
|
|
125
139
|
}
|
|
126
140
|
else if ('put' === method && null == fieldSets) {
|
|
127
141
|
fieldSets = getx(responses, '201 content "application/json" schema') ??
|
|
@@ -178,7 +192,7 @@ function inferFieldsFromExamples(opdef: any): SchemaDef[] {
|
|
|
178
192
|
}
|
|
179
193
|
|
|
180
194
|
const fielddefs: SchemaDef[] = []
|
|
181
|
-
for (const [key, value] of Object.entries(example)) {
|
|
195
|
+
for (const [key, value] of Object.entries(example).sort(([a],[b]) => a < b ? -1 : a > b ? 1 : 0)) {
|
|
182
196
|
const fielddef: any = {
|
|
183
197
|
key$: key,
|
|
184
198
|
type: inferTypeFromValue(value),
|
|
@@ -237,6 +251,42 @@ function unwrapExample(example: any): any {
|
|
|
237
251
|
}
|
|
238
252
|
|
|
239
253
|
|
|
254
|
+
// unwrapArrayWrapper inspects a list-response schema and, when it is an
|
|
255
|
+
// object with a single array-of-object-schema property (e.g.
|
|
256
|
+
// { boards: [Board] }, { items: [Foo], page, total, ... }), returns the
|
|
257
|
+
// inner item schema so that field resolution sees the actual entity
|
|
258
|
+
// properties rather than the wrapper's bookkeeping.
|
|
259
|
+
//
|
|
260
|
+
// Returns null if the input is not unambiguously such a wrapper:
|
|
261
|
+
// - schema is already an array → return null (let caller use it directly)
|
|
262
|
+
// - no array-of-object-schema property → return null
|
|
263
|
+
// - more than one array-of-object-schema property → ambiguous, return null
|
|
264
|
+
function unwrapArrayWrapper(schema: any): any {
|
|
265
|
+
if (null == schema || 'object' !== typeof schema) return null
|
|
266
|
+
// Direct list shape — caller can resolve from items directly.
|
|
267
|
+
if (schema.type === 'array' && schema.items) {
|
|
268
|
+
const items = schema.items
|
|
269
|
+
if (items && (items.properties || Array.isArray(items.allOf))) {
|
|
270
|
+
return items
|
|
271
|
+
}
|
|
272
|
+
return null
|
|
273
|
+
}
|
|
274
|
+
if (null == schema.properties || 'object' !== typeof schema.properties) return null
|
|
275
|
+
let resolved: any = null
|
|
276
|
+
for (const key of Object.keys(schema.properties)) {
|
|
277
|
+
const prop = schema.properties[key]
|
|
278
|
+
if (null == prop || 'object' !== typeof prop) continue
|
|
279
|
+
if (prop.type !== 'array' || null == prop.items) continue
|
|
280
|
+
const items = prop.items
|
|
281
|
+
if (null == items || 'object' !== typeof items) continue
|
|
282
|
+
if (!items.properties && !Array.isArray(items.allOf)) continue
|
|
283
|
+
if (resolved != null) return null // ambiguous: multiple array-of-object props
|
|
284
|
+
resolved = items
|
|
285
|
+
}
|
|
286
|
+
return resolved
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
|
|
240
290
|
function inferTypeFromValue(value: any): string {
|
|
241
291
|
if (null == value) return 'string'
|
|
242
292
|
if ('boolean' === typeof value) return 'boolean'
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
|
|
2
2
|
|
|
3
|
-
import { each } from 'jostraca'
|
|
3
|
+
import { each, camelify, lcf } from 'jostraca'
|
|
4
4
|
|
|
5
5
|
import type { TransformResult, Transform } from '../transform'
|
|
6
6
|
|
|
@@ -10,6 +10,43 @@ import {
|
|
|
10
10
|
nom,
|
|
11
11
|
} from '../utility'
|
|
12
12
|
|
|
13
|
+
|
|
14
|
+
// Detect a path-parameter that is in fact the entity's own id, after URL
|
|
15
|
+
// renaming. Three ways an identity can show up:
|
|
16
|
+
// 1. The param literally has name 'id' (the common case for e.g. /things/{id}).
|
|
17
|
+
// 2. The param's lower-camelCase name appears in `point.rename.param` mapping
|
|
18
|
+
// to 'id' — e.g. `{connectionId: 'id'}` for `/companies/{company_id}/connections/{id}`.
|
|
19
|
+
// In this case the param's own name is `connection_id` (apidef snake-cased
|
|
20
|
+
// it), which doesn't equal 'id' but the placeholder in `parts` is `{id}`.
|
|
21
|
+
// 3. Positional convention: for singleton ops (load/update/remove), the
|
|
22
|
+
// LAST `{X}` placeholder in the path is the entity's own id. Catches
|
|
23
|
+
// cases where the entity name and path placeholder differ in spelling
|
|
24
|
+
// (e.g. entity `enviroment` vs path `/environments/{environment_id}`)
|
|
25
|
+
// and apidef therefore didn't synthesize a rename-to-id.
|
|
26
|
+
//
|
|
27
|
+
// Without this helper, the flow generator double-counts the entity's id —
|
|
28
|
+
// emitting it as both `srcdatavar.id` AND a separate body field — which the
|
|
29
|
+
// in-memory test mock then requires to match a non-existent field on the
|
|
30
|
+
// stored entity.
|
|
31
|
+
function isEntityIdParam(point: any, param: any, opname?: string): boolean {
|
|
32
|
+
if ('id' === param?.name) return true
|
|
33
|
+
const renameMap = point?.rename?.param
|
|
34
|
+
if (renameMap && param?.name) {
|
|
35
|
+
const camel = lcf(camelify(param.name))
|
|
36
|
+
if ('id' === renameMap[camel]) return true
|
|
37
|
+
}
|
|
38
|
+
if ('update' === opname || 'load' === opname || 'remove' === opname) {
|
|
39
|
+
const parts: any[] = point?.parts || []
|
|
40
|
+
let last: string | null = null
|
|
41
|
+
for (const p of parts) {
|
|
42
|
+
const m = String(p).match(/^\{(.+)\}$/)
|
|
43
|
+
if (m) last = m[1]
|
|
44
|
+
}
|
|
45
|
+
if (last && last === param?.name) return true
|
|
46
|
+
}
|
|
47
|
+
return false
|
|
48
|
+
}
|
|
49
|
+
|
|
13
50
|
import { KIT } from '../types'
|
|
14
51
|
|
|
15
52
|
import type { KitModel } from '../types'
|
|
@@ -116,6 +153,24 @@ function newFlowStep(opname: OpName, args: Record<string, any>): ModelEntityFlow
|
|
|
116
153
|
}
|
|
117
154
|
|
|
118
155
|
|
|
156
|
+
// Reverse-lookup: given a point with rename.param like {spaceId: 'id'} or
|
|
157
|
+
// {space_id: 'id'}, return the snake_case ORIGINAL name (e.g. 'space_id') of
|
|
158
|
+
// any param whose URL placeholder is now `{id}`. Returns null when no
|
|
159
|
+
// rename-to-id is recorded — the literal `id` then represents the entity's
|
|
160
|
+
// own id and createStep should skip it.
|
|
161
|
+
function originalSnakeNameOfRenamedId(point: any): string | null {
|
|
162
|
+
const renameMap = point?.rename?.param || {}
|
|
163
|
+
for (const [src, dst] of Object.entries(renameMap)) {
|
|
164
|
+
if ('id' === dst) {
|
|
165
|
+
const srcStr = String(src)
|
|
166
|
+
// Already snake_case? Use as-is. Otherwise convert to snake form.
|
|
167
|
+
return srcStr.includes('_') ? srcStr : (srcStr.replace(/[A-Z]/g, m => '_' + m.toLowerCase()).replace(/^_/, ''))
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
return null
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
|
|
119
174
|
const createStep: MakeFlowStep = (
|
|
120
175
|
opmap: any,
|
|
121
176
|
flow: ModelEntityFlow,
|
|
@@ -128,17 +183,59 @@ const createStep: MakeFlowStep = (
|
|
|
128
183
|
const step = newFlowStep('create', args)
|
|
129
184
|
|
|
130
185
|
each(point.args.params, (param: any) => {
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
186
|
+
if ('id' === param.name) {
|
|
187
|
+
// For CREATE, `id` in the path is NOT the entity's own id (entity is
|
|
188
|
+
// being created here — its id doesn't exist yet). It's some parent's
|
|
189
|
+
// id renamed by apidef's path normalization (e.g. `space_id` → `id`
|
|
190
|
+
// in `/spaces/{id}/space_memberships` for SpaceMembership). Recover
|
|
191
|
+
// the original snake_case name so the test seeds the parent's id
|
|
192
|
+
// into both the created entity's data AND the URL.
|
|
193
|
+
const origName = originalSnakeNameOfRenamedId(point)
|
|
194
|
+
if (origName) {
|
|
195
|
+
step.match[origName] = args.input?.[origName] ?? origName.replace(/_id/, '') + '01'
|
|
196
|
+
}
|
|
197
|
+
// If there's no rename-from, this is genuinely the entity's id — skip
|
|
198
|
+
// (the create call generates it).
|
|
199
|
+
return
|
|
134
200
|
}
|
|
201
|
+
step.match[param.name] = args.input?.[param.name] ?? param.name.replace(/_id/, '') + '01'
|
|
135
202
|
})
|
|
136
203
|
|
|
204
|
+
// Also seed any path-param fields required by other ops (typically LIST
|
|
205
|
+
// through a sibling parent path), so the in-memory test mock can find
|
|
206
|
+
// the just-created entity when a later step queries by those fields.
|
|
207
|
+
// Without this, a metric created at /pages/{page_id}/metrics/data lacks
|
|
208
|
+
// the page_access_user_id field required by
|
|
209
|
+
// /pages/{page_id}/page_access_users/{page_access_user_id}/metrics LIST.
|
|
210
|
+
seedRelatedOpParams(opmap, point, step)
|
|
211
|
+
|
|
137
212
|
flow.step.push(step)
|
|
138
213
|
}
|
|
139
214
|
}
|
|
140
215
|
|
|
141
216
|
|
|
217
|
+
function seedRelatedOpParams(opmap: any, createPoint: any, step: ModelEntityFlowStep) {
|
|
218
|
+
const otherOps = ['list', 'load', 'update', 'remove']
|
|
219
|
+
for (const opname of otherOps) {
|
|
220
|
+
const op = opmap[opname]
|
|
221
|
+
if (!op?.points) continue
|
|
222
|
+
for (const point of op.points) {
|
|
223
|
+
const params: any[] = point?.args?.params || []
|
|
224
|
+
for (const param of params) {
|
|
225
|
+
if (!param?.name) continue
|
|
226
|
+
if (isEntityIdParam(point, param, opname as any)) continue
|
|
227
|
+
if (step.match[param.name] !== undefined) continue
|
|
228
|
+
// For renamed-from-id params on CREATE's chosen point we'd already
|
|
229
|
+
// have set the snake-case origin; don't double-write.
|
|
230
|
+
if ('id' === param.name) continue
|
|
231
|
+
step.match[param.name] =
|
|
232
|
+
param.name.replace(/_id/, '') + '01'
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
|
|
142
239
|
const listStep: MakeFlowStep = (
|
|
143
240
|
opmap: any,
|
|
144
241
|
flow: ModelEntityFlow,
|
|
@@ -151,6 +248,17 @@ const listStep: MakeFlowStep = (
|
|
|
151
248
|
const step = newFlowStep('list', args)
|
|
152
249
|
|
|
153
250
|
each(point.args.params, (param: any) => {
|
|
251
|
+
if ('id' === param.name) {
|
|
252
|
+
// For LIST, `id` in the path is a parent's id renamed by apidef
|
|
253
|
+
// (LIST doesn't address a single entity by id). Recover the original
|
|
254
|
+
// snake_case name so test code references a real idmap entry rather
|
|
255
|
+
// than landing on the bogus `id01` default.
|
|
256
|
+
const origName = originalSnakeNameOfRenamedId(point)
|
|
257
|
+
if (origName) {
|
|
258
|
+
step.match[origName] = args.input?.[origName] ?? origName.replace(/_id/, '') + '01'
|
|
259
|
+
}
|
|
260
|
+
return
|
|
261
|
+
}
|
|
154
262
|
step.match[param.name] = args.input?.[param.name] ?? param.name.replace(/_id/, '') + '01'
|
|
155
263
|
})
|
|
156
264
|
|
|
@@ -171,12 +279,12 @@ const updateStep: MakeFlowStep = (
|
|
|
171
279
|
const step = newFlowStep('update', args)
|
|
172
280
|
|
|
173
281
|
each(point.args.params, (param: any) => {
|
|
174
|
-
if ('
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
step.data[param.name] = args.input?.[param.name] ?? param.name.replace(/_id/, '') + '01'
|
|
282
|
+
if (isEntityIdParam(point, param, 'update')) {
|
|
283
|
+
// Entity's own id — supplied at test time via the loaded/created
|
|
284
|
+
// entity's id field, not as a separate body parameter. Skip.
|
|
285
|
+
return
|
|
179
286
|
}
|
|
287
|
+
step.data[param.name] = args.input?.[param.name] ?? param.name.replace(/_id/, '') + '01'
|
|
180
288
|
})
|
|
181
289
|
|
|
182
290
|
flow.step.push(step)
|
|
@@ -196,7 +304,7 @@ const loadStep: MakeFlowStep = (
|
|
|
196
304
|
const step = newFlowStep('load', args)
|
|
197
305
|
|
|
198
306
|
each(point.args.params, (param: any) => {
|
|
199
|
-
if ('
|
|
307
|
+
if (isEntityIdParam(point, param, 'load')) {
|
|
200
308
|
step.match.id = args.input?.id ?? ent.name + '01'
|
|
201
309
|
}
|
|
202
310
|
else {
|
|
@@ -221,7 +329,7 @@ const removeStep: MakeFlowStep = (
|
|
|
221
329
|
const step = newFlowStep('remove', args)
|
|
222
330
|
|
|
223
331
|
each(point.args.params, (param: any) => {
|
|
224
|
-
if ('
|
|
332
|
+
if (isEntityIdParam(point, param, 'remove')) {
|
|
225
333
|
step.match.id = args.input?.id ?? ent.name + '01'
|
|
226
334
|
}
|
|
227
335
|
else {
|
|
@@ -137,7 +137,14 @@ function resolveOp(opname: OpName, gent: GuideEntity): undefined | ModelOp {
|
|
|
137
137
|
mop = {
|
|
138
138
|
name: opname,
|
|
139
139
|
points: opdesc.paths.map((p: PathDesc) => {
|
|
140
|
-
|
|
140
|
+
// Renames already applied by entity.ts resolvePathList — re-applying
|
|
141
|
+
// here corrupted paths for any spec where rename map maps an old
|
|
142
|
+
// name to a value that another rename maps to a different new name
|
|
143
|
+
// (e.g. gitlab `/groups/{id}/badges/{badge_id}` with rename
|
|
144
|
+
// `{badge_id: 'id', id: 'project_id'}` ended up as
|
|
145
|
+
// `/groups/{project_id}/badges/{project_id}` — the second pass
|
|
146
|
+
// rewrote the freshly-renamed `{id}` into `{project_id}` again).
|
|
147
|
+
const parts = p.parts
|
|
141
148
|
|
|
142
149
|
const mtarget: ModelTarget = {
|
|
143
150
|
orig: p.orig,
|
|
@@ -162,12 +169,6 @@ function resolveOp(opname: OpName, gent: GuideEntity): undefined | ModelOp {
|
|
|
162
169
|
}
|
|
163
170
|
|
|
164
171
|
|
|
165
|
-
function applyRename(pathdesc: PathDesc): string[] {
|
|
166
|
-
const prn: Record<string, string> = pathdesc.rename?.param ?? {}
|
|
167
|
-
return pathdesc.parts.map(p => '{' === p[0] ? (prn[p.substring(1, p.length - 1)] ?? p) : p)
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
|
|
171
172
|
|
|
172
173
|
|
|
173
174
|
export {
|
package/src/transform/select.ts
CHANGED
package/src/utility.ts
CHANGED
|
@@ -24,6 +24,11 @@ import type {
|
|
|
24
24
|
|
|
25
25
|
const KONSOLE_LOG = console['log']
|
|
26
26
|
|
|
27
|
+
// Sorted iteration helpers — ensures deterministic key order matching Go.
|
|
28
|
+
const sortedKeys = (obj: any): string[] => Object.keys(obj ?? {}).sort()
|
|
29
|
+
const sortedEntries = (obj: any): [string, any][] =>
|
|
30
|
+
Object.entries(obj ?? {}).sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0)
|
|
31
|
+
|
|
27
32
|
// Pre-compiled regex patterns for formatJsonSrc to avoid recompilation per call.
|
|
28
33
|
const RE_JSON_KEY = /"([a-zA-Z_][a-zA-Z_0-9]*)": /g
|
|
29
34
|
const RE_JSON_TRAILING_BRACE = /},/g
|
|
@@ -1163,5 +1168,7 @@ export {
|
|
|
1163
1168
|
warnOnError,
|
|
1164
1169
|
relativizePath,
|
|
1165
1170
|
getModelPath,
|
|
1171
|
+
sortedKeys,
|
|
1172
|
+
sortedEntries,
|
|
1166
1173
|
|
|
1167
1174
|
}
|