Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 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
2 changes: 1 addition & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

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