Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -24,28 +24,28 @@ export function exoObservationModFromJson(json: Json): ExoObservationMod | Inval
return invalidInput('Observation update must be a JSON object.')
}
if (typeof json.id !== 'string') {
return invalidInput('Observation must have a string ID.', [ 'id' ])
return invalidInput('Observation must have a string ID.', ['id'])
}
const bbox = json.bbox
if (bbox !== undefined && !Array.isArray(bbox)) {
return invalidInput('BBox must be an array.', [ 'bbox' ])
return invalidInput('BBox must be an array.', ['bbox'])
}
if (typeof json?.geometry !== 'object' || Array.isArray(json.geometry) || json.geometry === null) {
return invalidInput('Geometry must be an object.', [ 'geometry' ])
return invalidInput('Geometry must be an object.', ['geometry'])
}
if (json.type !== undefined && json.type !== 'Feature') {
return invalidInput('GeoJSON type must be \'Feature\'', [ 'type' ])
return invalidInput('GeoJSON type must be \'Feature\'', ['type'])
}
const properties = json.properties
if (!properties || typeof properties !== 'object' || Array.isArray(properties)) {
return invalidInput('Observation properties must be an object.', [ 'properties' ])
return invalidInput('Observation properties must be an object.', ['properties'])
}
if (!properties.timestamp || typeof properties.timestamp !== 'string') {
return invalidInput('Observation timestamp must be a string.', [ 'properties', 'timestamp' ])
return invalidInput('Observation timestamp must be a string.', ['properties', 'timestamp'])
}
const timestamp = moment(properties.timestamp, moment.ISO_8601, true)
if (!timestamp.isValid()) {
return invalidInput('Observation timestamp must be a valid ISO-8601 date.', [ 'properties', 'timestamp' ])
return invalidInput('Observation timestamp must be a valid ISO-8601 date.', ['properties', 'timestamp'])
}
const mod: ExoObservationMod = {
id: json.id,
Expand All @@ -54,7 +54,8 @@ export function exoObservationModFromJson(json: Json): ExoObservationMod | Inval
properties: {
...properties as any,
timestamp: timestamp.toDate()
}
},
noGeometry: !!json.noGeometry
}
if (bbox) {
mod.bbox = bbox as BBox
Expand Down
17 changes: 9 additions & 8 deletions service/src/app.api/observations/app.api.observations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,12 @@ export interface ObservationRequestContext<Principal = unknown> extends AppReque
deviceId: string
observationRepository: EventScopedObservationRepository
}
export interface ObservationRequest<Principal = unknown> extends AppRequest<Principal, ObservationRequestContext<Principal>> {}
export interface ObservationRequest<Principal = unknown> extends AppRequest<Principal, ObservationRequestContext<Principal>> { }

export interface AllocateObservationId {
(req: AllocateObservationIdRequest): Promise<AppResponse<ObservationId, PermissionDeniedError>>
}
export interface AllocateObservationIdRequest extends ObservationRequest {}
export interface AllocateObservationIdRequest extends ObservationRequest { }

export interface SaveObservation {
(req: SaveObservationRequest): Promise<AppResponse<ExoObservation, PermissionDeniedError | EntityNotFoundError | InvalidInputError>>
Expand Down Expand Up @@ -61,7 +61,8 @@ export type ExoObservation = Omit<ObservationAttrs, 'attachments' | 'important'
user?: ExoObservationUserLite
important?: ExoObservationImportantFlag
state?: ObservationState
attachments: ExoAttachment[]
attachments: ExoAttachment[],
noGeometry?: boolean
}

export type ExoAttachment = Omit<Attachment, 'thumbnails' | 'contentLocator'> & {
Expand Down Expand Up @@ -124,12 +125,12 @@ export function exoObservationFor(from: ObservationAttrs, users?: { creator?: Us
return {
...attrs,
attachments,
user: from.userId === users.creator?.id ? exoObservationUserLiteFor(users.creator) : void(0),
state: states ? states[0] : void(0),
user: from.userId === users.creator?.id ? exoObservationUserLiteFor(users.creator) : void (0),
state: states ? states[0] : void (0),
important: from.important ? {
...from.important,
user: from.important.userId === users.importantFlagger?.id ? exoObservationUserLiteFor(users.importantFlagger) : void(0)
} : void(0)
user: from.important.userId === users.importantFlagger?.id ? exoObservationUserLiteFor(users.importantFlagger) : void (0)
} : void (0)
}
}

Expand Down Expand Up @@ -160,7 +161,7 @@ export function exoAttachmentForThumbnailDimension(targetDimension: number, atta
}

export function exoObservationUserLiteFor(from: User | null | undefined): ExoObservationUserLite | undefined {
return from ? { id: from.id, displayName: from.displayName } : void(0)
return from ? { id: from.id, displayName: from.displayName } : void (0)
}

export function domainObservationFor(from: ExoObservation): ObservationAttrs {
Expand Down
78 changes: 39 additions & 39 deletions service/src/app.impl/observations/app.impl.observations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,14 +23,14 @@ export function SaveObservation(permissionService: api.ObservationPermissionServ
return async function saveObservation(req: api.SaveObservationRequest): ReturnType<api.SaveObservation> {
const repo = req.context.observationRepository
const mod = req.observation
const before = await repo.findById(mod.id)
const denied = before ?
await permissionService.ensureUpdateObservationPermission(req.context) :
await permissionService.ensureCreateObservationPermission(req.context)
const existingObservation = await repo.findById(mod.id)
const denied = existingObservation ?
await permissionService.ensureUpdateObservationPermission(req.context) :
await permissionService.ensureCreateObservationPermission(req.context)
if (denied) {
return AppResponse.error(denied)
}
const obs = await prepareObservationMod(mod, before, req.context)
const obs = await prepareObservationMod(mod, existingObservation, req.context)
if (obs instanceof MageError) {
return AppResponse.error(obs)
}
Expand Down Expand Up @@ -111,11 +111,11 @@ export function ReadAttachmentContent(permissionService: api.ObservationPermissi
return AppResponse.error(entityNotFound(req.attachmentId, 'Attachment'))
}
const contentRange = typeof req.contentRange?.start === 'number' && typeof req.contentRange.end === 'number' ?
{ start: req.contentRange.start, end: req.contentRange.end } : void(0)
{ start: req.contentRange.start, end: req.contentRange.end } : void (0)
let contentStream: NodeJS.ReadableStream | null | AttachmentStoreError = null
let exoAttachment: api.ExoAttachment = api.exoAttachmentFor(attachment)
if (typeof req.minDimension === 'number') {
const thumbIndex = thumbnailIndexForTargetDimension(req.minDimension, attachment)
const thumbIndex = thumbnailIndexForTargetDimension(req.minDimension, attachment)
const thumb = attachment.thumbnails[Number(thumbIndex)]
if (thumb) {
contentStream = await attachmentStore.readThumbnailContent(thumb.minDimension, attachment.id, obs)
Expand All @@ -138,7 +138,7 @@ export function ReadAttachmentContent(permissionService: api.ObservationPermissi
return AppResponse.success({
attachment: exoAttachment,
bytes: contentStream,
bytesRange: typeof req.minDimension === 'number' ? void(0) : contentRange
bytesRange: typeof req.minDimension === 'number' ? void (0) : contentRange
})
}
}
Expand Down Expand Up @@ -169,33 +169,33 @@ export function registerDeleteRemovedAttachmentsHandler(domainEvents: EventEmitt
* an `isPending` property. That should be reasonable to implement, but no
* time now, as usual.
*/
async function prepareObservationMod(mod: api.ExoObservationMod, before: Observation | null, context: api.ObservationRequestContext): Promise<Observation | InvalidInputError> {
async function prepareObservationMod(mod: api.ExoObservationMod, observationToUpdate: Observation | null, context: api.ObservationRequestContext): Promise<Observation | InvalidInputError> {
const event = context.mageEvent
const repo = context.observationRepository
const modAttrs = baseObservationAttrsForMod(mod, before, context)
const modAttrs = baseObservationAttrsForMod(mod, observationToUpdate, context)
// first get new form entry ids so new attachments have a proper id to reference
const [ removedFormEntries, newFormEntries ] = mod.properties.forms.reduce(([ removed, added ], entryMod ) => {
if (entryMod.id && before?.formEntryForId(entryMod.id)) {
const [removedFormEntries, newFormEntries] = mod.properties.forms.reduce(([removed, added], entryMod) => {
if (entryMod.id && observationToUpdate?.formEntryForId(entryMod.id)) {
removed.delete(entryMod.id)
}
else {
added.push(entryMod)
}
return [ removed, added ]
}, [ new Map(before?.formEntries.map(x => [ x.id, x ]) || []), [] as api.ExoFormEntryMod[] ])
return [removed, added]
}, [new Map(observationToUpdate?.formEntries.map(x => [x.id, x]) || []), [] as api.ExoFormEntryMod[]])
const newFormEntryIds = newFormEntries.length ? await repo.nextFormEntryIds(newFormEntries.length) : []
newFormEntries.forEach(x => x.id = newFormEntryIds.shift())
const attachmentExtraction = extractAttachmentModsFromFormEntries(mod, event)
modAttrs.properties.forms = attachmentExtraction.formEntries
const attachmentMods = attachmentExtraction.attachmentMods
const addCount = attachmentMods.reduce((count, x) => x.action === api.AttachmentModAction.Add ? count + 1 : count, 0)
const attachmentIds = addCount ? await repo.nextAttachmentIds(addCount) : []
const afterRemovedFormEntryAttachments = before?.attachments.reduce((obs, attachment) => {
const afterRemovedFormEntryAttachments = observationToUpdate?.attachments.reduce((obs, attachment) => {
if (removedFormEntries.has(attachment.observationFormId)) {
return removeAttachment(obs, attachment.id) as Observation
}
return obs
}, before)
}, observationToUpdate)
if (afterRemovedFormEntryAttachments) {
modAttrs.attachments = afterRemovedFormEntryAttachments.attachments
}
Expand All @@ -206,18 +206,17 @@ async function prepareObservationMod(mod: api.ExoObservationMod, before: Observa
if (obs instanceof MageError) {
return obs
}
const mod =
attachmentMod.action === api.AttachmentModAction.Add ?
addAttachment(obs, attachmentIds.shift() as string, attachmentMod.fieldName, attachmentMod.formEntryId, attachmentCreateAttrsForMod(attachmentMod)) :
attachmentMod.action === api.AttachmentModAction.Delete ?
removeAttachment(obs, attachmentMod.id) :
null
let mod;
if (attachmentMod.action === api.AttachmentModAction.Add) {
mod = addAttachment(obs, attachmentIds.shift() as string, attachmentMod.fieldName, attachmentMod.formEntryId, attachmentCreateAttrsForMod(attachmentMod))
} else if (attachmentMod.action === api.AttachmentModAction.Delete) {
mod = removeAttachment(obs, attachmentMod.id)
} else {
return invalidInput(`invalid attachment action: ${attachmentMod.action}`)
}
if (mod instanceof Observation) {
return mod
}
if (mod === null) {
return invalidInput(`invalid attachment action: ${attachmentMod.action}`)
}
const message = `error adding attachment on observation ${obs.id}`
return invalidInput(`${message}: ${String(mod)}`)
}, afterFormEntriesRemoved)
Expand All @@ -229,32 +228,33 @@ async function prepareObservationMod(mod: api.ExoObservationMod, before: Observa
* existing observation. The result will not include form entries and
* attachments, which require separate processing to resolve IDs and actions.
* @param mod the modifications from an external client
* @param before the observation to update, or null if none exists
* @param observationToUpdate the observation to update, or null if none exists
*/
function baseObservationAttrsForMod(mod: api.ExoObservationMod, before: Observation | null, context: api.ObservationRequestContext): ObservationAttrs {
function baseObservationAttrsForMod(mod: api.ExoObservationMod, observationToUpdate: Observation | null, context: api.ObservationRequestContext): ObservationAttrs {
const attrs: ObservationAttrs = {
id: mod.id,
eventId: context.mageEvent.id,
userId: before ? before.userId : context.userId,
deviceId: before ? before.deviceId : context.deviceId,
createdAt: before ? before.createdAt : new Date(),
userId: observationToUpdate ? observationToUpdate.userId : context.userId,
deviceId: observationToUpdate ? observationToUpdate.deviceId : context.deviceId,
createdAt: observationToUpdate ? observationToUpdate.createdAt : new Date(),
lastModified: new Date(),
geometry: mod.geometry,
type: 'Feature',
states: before ? before.states : [],
bbox: mod.bbox || before?.bbox,
favoriteUserIds: before?.favoriteUserIds || [],
important: before?.important,
states: observationToUpdate ? observationToUpdate.states : [],
bbox: mod.bbox || observationToUpdate?.bbox,
favoriteUserIds: observationToUpdate?.favoriteUserIds || [],
important: observationToUpdate?.important,
properties: {
// TODO: should timestamp be optional on the mod object?
timestamp: mod.properties.timestamp,
forms: []
},
attachments: [],
noGeometry: !!mod.noGeometry,
}
assignFirstDefined('accuracy', attrs.properties, mod.properties, before?.properties)
assignFirstDefined('delta', attrs.properties, mod.properties, before?.properties)
assignFirstDefined('provider', attrs.properties, mod.properties, before?.properties)
assignFirstDefined('accuracy', attrs.properties, mod.properties, observationToUpdate?.properties)
assignFirstDefined('delta', attrs.properties, mod.properties, observationToUpdate?.properties)
assignFirstDefined('provider', attrs.properties, mod.properties, observationToUpdate?.properties)
return attrs
}

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