Skip to content

Commit 0355723

Browse files
authored
MOCKUP: Adding support for observations without geometry (#300)
Update the Observation logic to add a noGeometry flag.
1 parent e1bb4e1 commit 0355723

File tree

19 files changed

+458
-416
lines changed

19 files changed

+458
-416
lines changed

service/src/adapters/observations/adapters.observations.dto.ecma404-json.ts

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -24,28 +24,28 @@ export function exoObservationModFromJson(json: Json): ExoObservationMod | Inval
2424
return invalidInput('Observation update must be a JSON object.')
2525
}
2626
if (typeof json.id !== 'string') {
27-
return invalidInput('Observation must have a string ID.', [ 'id' ])
27+
return invalidInput('Observation must have a string ID.', ['id'])
2828
}
2929
const bbox = json.bbox
3030
if (bbox !== undefined && !Array.isArray(bbox)) {
31-
return invalidInput('BBox must be an array.', [ 'bbox' ])
31+
return invalidInput('BBox must be an array.', ['bbox'])
3232
}
3333
if (typeof json?.geometry !== 'object' || Array.isArray(json.geometry) || json.geometry === null) {
34-
return invalidInput('Geometry must be an object.', [ 'geometry' ])
34+
return invalidInput('Geometry must be an object.', ['geometry'])
3535
}
3636
if (json.type !== undefined && json.type !== 'Feature') {
37-
return invalidInput('GeoJSON type must be \'Feature\'', [ 'type' ])
37+
return invalidInput('GeoJSON type must be \'Feature\'', ['type'])
3838
}
3939
const properties = json.properties
4040
if (!properties || typeof properties !== 'object' || Array.isArray(properties)) {
41-
return invalidInput('Observation properties must be an object.', [ 'properties' ])
41+
return invalidInput('Observation properties must be an object.', ['properties'])
4242
}
4343
if (!properties.timestamp || typeof properties.timestamp !== 'string') {
44-
return invalidInput('Observation timestamp must be a string.', [ 'properties', 'timestamp' ])
44+
return invalidInput('Observation timestamp must be a string.', ['properties', 'timestamp'])
4545
}
4646
const timestamp = moment(properties.timestamp, moment.ISO_8601, true)
4747
if (!timestamp.isValid()) {
48-
return invalidInput('Observation timestamp must be a valid ISO-8601 date.', [ 'properties', 'timestamp' ])
48+
return invalidInput('Observation timestamp must be a valid ISO-8601 date.', ['properties', 'timestamp'])
4949
}
5050
const mod: ExoObservationMod = {
5151
id: json.id,
@@ -54,7 +54,8 @@ export function exoObservationModFromJson(json: Json): ExoObservationMod | Inval
5454
properties: {
5555
...properties as any,
5656
timestamp: timestamp.toDate()
57-
}
57+
},
58+
noGeometry: !!json.noGeometry
5859
}
5960
if (bbox) {
6061
mod.bbox = bbox as BBox

service/src/app.api/observations/app.api.observations.ts

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -19,12 +19,12 @@ export interface ObservationRequestContext<Principal = unknown> extends AppReque
1919
deviceId: string
2020
observationRepository: EventScopedObservationRepository
2121
}
22-
export interface ObservationRequest<Principal = unknown> extends AppRequest<Principal, ObservationRequestContext<Principal>> {}
22+
export interface ObservationRequest<Principal = unknown> extends AppRequest<Principal, ObservationRequestContext<Principal>> { }
2323

2424
export interface AllocateObservationId {
2525
(req: AllocateObservationIdRequest): Promise<AppResponse<ObservationId, PermissionDeniedError>>
2626
}
27-
export interface AllocateObservationIdRequest extends ObservationRequest {}
27+
export interface AllocateObservationIdRequest extends ObservationRequest { }
2828

2929
export interface SaveObservation {
3030
(req: SaveObservationRequest): Promise<AppResponse<ExoObservation, PermissionDeniedError | EntityNotFoundError | InvalidInputError>>
@@ -61,7 +61,8 @@ export type ExoObservation = Omit<ObservationAttrs, 'attachments' | 'important'
6161
user?: ExoObservationUserLite
6262
important?: ExoObservationImportantFlag
6363
state?: ObservationState
64-
attachments: ExoAttachment[]
64+
attachments: ExoAttachment[],
65+
noGeometry?: boolean
6566
}
6667

6768
export type ExoAttachment = Omit<Attachment, 'thumbnails' | 'contentLocator'> & {
@@ -124,12 +125,12 @@ export function exoObservationFor(from: ObservationAttrs, users?: { creator?: Us
124125
return {
125126
...attrs,
126127
attachments,
127-
user: from.userId === users.creator?.id ? exoObservationUserLiteFor(users.creator) : void(0),
128-
state: states ? states[0] : void(0),
128+
user: from.userId === users.creator?.id ? exoObservationUserLiteFor(users.creator) : void (0),
129+
state: states ? states[0] : void (0),
129130
important: from.important ? {
130131
...from.important,
131-
user: from.important.userId === users.importantFlagger?.id ? exoObservationUserLiteFor(users.importantFlagger) : void(0)
132-
} : void(0)
132+
user: from.important.userId === users.importantFlagger?.id ? exoObservationUserLiteFor(users.importantFlagger) : void (0)
133+
} : void (0)
133134
}
134135
}
135136

@@ -160,7 +161,7 @@ export function exoAttachmentForThumbnailDimension(targetDimension: number, atta
160161
}
161162

162163
export function exoObservationUserLiteFor(from: User | null | undefined): ExoObservationUserLite | undefined {
163-
return from ? { id: from.id, displayName: from.displayName } : void(0)
164+
return from ? { id: from.id, displayName: from.displayName } : void (0)
164165
}
165166

166167
export function domainObservationFor(from: ExoObservation): ObservationAttrs {

service/src/app.impl/observations/app.impl.observations.ts

Lines changed: 39 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -23,14 +23,14 @@ export function SaveObservation(permissionService: api.ObservationPermissionServ
2323
return async function saveObservation(req: api.SaveObservationRequest): ReturnType<api.SaveObservation> {
2424
const repo = req.context.observationRepository
2525
const mod = req.observation
26-
const before = await repo.findById(mod.id)
27-
const denied = before ?
28-
await permissionService.ensureUpdateObservationPermission(req.context) :
29-
await permissionService.ensureCreateObservationPermission(req.context)
26+
const existingObservation = await repo.findById(mod.id)
27+
const denied = existingObservation ?
28+
await permissionService.ensureUpdateObservationPermission(req.context) :
29+
await permissionService.ensureCreateObservationPermission(req.context)
3030
if (denied) {
3131
return AppResponse.error(denied)
3232
}
33-
const obs = await prepareObservationMod(mod, before, req.context)
33+
const obs = await prepareObservationMod(mod, existingObservation, req.context)
3434
if (obs instanceof MageError) {
3535
return AppResponse.error(obs)
3636
}
@@ -111,11 +111,11 @@ export function ReadAttachmentContent(permissionService: api.ObservationPermissi
111111
return AppResponse.error(entityNotFound(req.attachmentId, 'Attachment'))
112112
}
113113
const contentRange = typeof req.contentRange?.start === 'number' && typeof req.contentRange.end === 'number' ?
114-
{ start: req.contentRange.start, end: req.contentRange.end } : void(0)
114+
{ start: req.contentRange.start, end: req.contentRange.end } : void (0)
115115
let contentStream: NodeJS.ReadableStream | null | AttachmentStoreError = null
116116
let exoAttachment: api.ExoAttachment = api.exoAttachmentFor(attachment)
117117
if (typeof req.minDimension === 'number') {
118-
const thumbIndex = thumbnailIndexForTargetDimension(req.minDimension, attachment)
118+
const thumbIndex = thumbnailIndexForTargetDimension(req.minDimension, attachment)
119119
const thumb = attachment.thumbnails[Number(thumbIndex)]
120120
if (thumb) {
121121
contentStream = await attachmentStore.readThumbnailContent(thumb.minDimension, attachment.id, obs)
@@ -138,7 +138,7 @@ export function ReadAttachmentContent(permissionService: api.ObservationPermissi
138138
return AppResponse.success({
139139
attachment: exoAttachment,
140140
bytes: contentStream,
141-
bytesRange: typeof req.minDimension === 'number' ? void(0) : contentRange
141+
bytesRange: typeof req.minDimension === 'number' ? void (0) : contentRange
142142
})
143143
}
144144
}
@@ -169,33 +169,33 @@ export function registerDeleteRemovedAttachmentsHandler(domainEvents: EventEmitt
169169
* an `isPending` property. That should be reasonable to implement, but no
170170
* time now, as usual.
171171
*/
172-
async function prepareObservationMod(mod: api.ExoObservationMod, before: Observation | null, context: api.ObservationRequestContext): Promise<Observation | InvalidInputError> {
172+
async function prepareObservationMod(mod: api.ExoObservationMod, observationToUpdate: Observation | null, context: api.ObservationRequestContext): Promise<Observation | InvalidInputError> {
173173
const event = context.mageEvent
174174
const repo = context.observationRepository
175-
const modAttrs = baseObservationAttrsForMod(mod, before, context)
175+
const modAttrs = baseObservationAttrsForMod(mod, observationToUpdate, context)
176176
// first get new form entry ids so new attachments have a proper id to reference
177-
const [ removedFormEntries, newFormEntries ] = mod.properties.forms.reduce(([ removed, added ], entryMod ) => {
178-
if (entryMod.id && before?.formEntryForId(entryMod.id)) {
177+
const [removedFormEntries, newFormEntries] = mod.properties.forms.reduce(([removed, added], entryMod) => {
178+
if (entryMod.id && observationToUpdate?.formEntryForId(entryMod.id)) {
179179
removed.delete(entryMod.id)
180180
}
181181
else {
182182
added.push(entryMod)
183183
}
184-
return [ removed, added ]
185-
}, [ new Map(before?.formEntries.map(x => [ x.id, x ]) || []), [] as api.ExoFormEntryMod[] ])
184+
return [removed, added]
185+
}, [new Map(observationToUpdate?.formEntries.map(x => [x.id, x]) || []), [] as api.ExoFormEntryMod[]])
186186
const newFormEntryIds = newFormEntries.length ? await repo.nextFormEntryIds(newFormEntries.length) : []
187187
newFormEntries.forEach(x => x.id = newFormEntryIds.shift())
188188
const attachmentExtraction = extractAttachmentModsFromFormEntries(mod, event)
189189
modAttrs.properties.forms = attachmentExtraction.formEntries
190190
const attachmentMods = attachmentExtraction.attachmentMods
191191
const addCount = attachmentMods.reduce((count, x) => x.action === api.AttachmentModAction.Add ? count + 1 : count, 0)
192192
const attachmentIds = addCount ? await repo.nextAttachmentIds(addCount) : []
193-
const afterRemovedFormEntryAttachments = before?.attachments.reduce((obs, attachment) => {
193+
const afterRemovedFormEntryAttachments = observationToUpdate?.attachments.reduce((obs, attachment) => {
194194
if (removedFormEntries.has(attachment.observationFormId)) {
195195
return removeAttachment(obs, attachment.id) as Observation
196196
}
197197
return obs
198-
}, before)
198+
}, observationToUpdate)
199199
if (afterRemovedFormEntryAttachments) {
200200
modAttrs.attachments = afterRemovedFormEntryAttachments.attachments
201201
}
@@ -206,18 +206,17 @@ async function prepareObservationMod(mod: api.ExoObservationMod, before: Observa
206206
if (obs instanceof MageError) {
207207
return obs
208208
}
209-
const mod =
210-
attachmentMod.action === api.AttachmentModAction.Add ?
211-
addAttachment(obs, attachmentIds.shift() as string, attachmentMod.fieldName, attachmentMod.formEntryId, attachmentCreateAttrsForMod(attachmentMod)) :
212-
attachmentMod.action === api.AttachmentModAction.Delete ?
213-
removeAttachment(obs, attachmentMod.id) :
214-
null
209+
let mod;
210+
if (attachmentMod.action === api.AttachmentModAction.Add) {
211+
mod = addAttachment(obs, attachmentIds.shift() as string, attachmentMod.fieldName, attachmentMod.formEntryId, attachmentCreateAttrsForMod(attachmentMod))
212+
} else if (attachmentMod.action === api.AttachmentModAction.Delete) {
213+
mod = removeAttachment(obs, attachmentMod.id)
214+
} else {
215+
return invalidInput(`invalid attachment action: ${attachmentMod.action}`)
216+
}
215217
if (mod instanceof Observation) {
216218
return mod
217219
}
218-
if (mod === null) {
219-
return invalidInput(`invalid attachment action: ${attachmentMod.action}`)
220-
}
221220
const message = `error adding attachment on observation ${obs.id}`
222221
return invalidInput(`${message}: ${String(mod)}`)
223222
}, afterFormEntriesRemoved)
@@ -229,32 +228,33 @@ async function prepareObservationMod(mod: api.ExoObservationMod, before: Observa
229228
* existing observation. The result will not include form entries and
230229
* attachments, which require separate processing to resolve IDs and actions.
231230
* @param mod the modifications from an external client
232-
* @param before the observation to update, or null if none exists
231+
* @param observationToUpdate the observation to update, or null if none exists
233232
*/
234-
function baseObservationAttrsForMod(mod: api.ExoObservationMod, before: Observation | null, context: api.ObservationRequestContext): ObservationAttrs {
233+
function baseObservationAttrsForMod(mod: api.ExoObservationMod, observationToUpdate: Observation | null, context: api.ObservationRequestContext): ObservationAttrs {
235234
const attrs: ObservationAttrs = {
236235
id: mod.id,
237236
eventId: context.mageEvent.id,
238-
userId: before ? before.userId : context.userId,
239-
deviceId: before ? before.deviceId : context.deviceId,
240-
createdAt: before ? before.createdAt : new Date(),
237+
userId: observationToUpdate ? observationToUpdate.userId : context.userId,
238+
deviceId: observationToUpdate ? observationToUpdate.deviceId : context.deviceId,
239+
createdAt: observationToUpdate ? observationToUpdate.createdAt : new Date(),
241240
lastModified: new Date(),
242241
geometry: mod.geometry,
243242
type: 'Feature',
244-
states: before ? before.states : [],
245-
bbox: mod.bbox || before?.bbox,
246-
favoriteUserIds: before?.favoriteUserIds || [],
247-
important: before?.important,
243+
states: observationToUpdate ? observationToUpdate.states : [],
244+
bbox: mod.bbox || observationToUpdate?.bbox,
245+
favoriteUserIds: observationToUpdate?.favoriteUserIds || [],
246+
important: observationToUpdate?.important,
248247
properties: {
249248
// TODO: should timestamp be optional on the mod object?
250249
timestamp: mod.properties.timestamp,
251250
forms: []
252251
},
253252
attachments: [],
253+
noGeometry: !!mod.noGeometry,
254254
}
255-
assignFirstDefined('accuracy', attrs.properties, mod.properties, before?.properties)
256-
assignFirstDefined('delta', attrs.properties, mod.properties, before?.properties)
257-
assignFirstDefined('provider', attrs.properties, mod.properties, before?.properties)
255+
assignFirstDefined('accuracy', attrs.properties, mod.properties, observationToUpdate?.properties)
256+
assignFirstDefined('delta', attrs.properties, mod.properties, observationToUpdate?.properties)
257+
assignFirstDefined('provider', attrs.properties, mod.properties, observationToUpdate?.properties)
258258
return attrs
259259
}
260260

@@ -295,11 +295,11 @@ function extractAttachmentModsFromFormEntries(mod: api.ExoObservationMod, event:
295295
function extractAttachmentModsFromFormEntry(formEntryMod: api.ExoFormEntryMod, event: MageEvent): { formEntry: FormEntry, attachmentMods: QualifiedAttachmentMod[] } {
296296
const attachmentMods = [] as QualifiedAttachmentMod[]
297297
const { id, formId, ...fieldEntries } = formEntryMod as Required<api.ExoFormEntryMod>
298-
const formEntry = Object.entries(fieldEntries).reduce((formEntry, [ fieldName, fieldEntry ]) => {
298+
const formEntry = Object.entries(fieldEntries).reduce((formEntry, [fieldName, fieldEntry]) => {
299299
const field = event.formFieldFor(fieldName, formId)
300300
if (field?.type === FormFieldType.Attachment) {
301301
const attachmentModEntry = (fieldEntry || []) as api.ExoAttachmentMod[]
302-
attachmentModEntry.forEach(x => void(x.action && attachmentMods.push({ ...x, formEntryId: formEntry.id, fieldName })))
302+
attachmentModEntry.forEach(x => void (x.action && attachmentMods.push({ ...x, formEntryId: formEntry.id, fieldName })))
303303
}
304304
else {
305305
// let it be invalid

0 commit comments

Comments
 (0)