Skip to content

Commit

Permalink
fix(core): cascading remove
Browse files Browse the repository at this point in the history
  • Loading branch information
minenwerfer committed Oct 13, 2024
1 parent 5ee39fa commit 4f9f9dc
Show file tree
Hide file tree
Showing 15 changed files with 210 additions and 132 deletions.
8 changes: 8 additions & 0 deletions .changeset/sharp-drinks-confess.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
"@aeriajs/builtins": patch
"@aeriajs/common": patch
"@aeriajs/types": patch
"@aeriajs/core": patch
---

Fix cascading remove
5 changes: 1 addition & 4 deletions packages/builtins/src/collections/file/remove.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,7 @@ import type { description } from './description.js'
import { remove as originalRemove, type ObjectId } from '@aeriajs/core'
import * as fs from 'fs/promises'

export const remove = async (
payload: RemovePayload<SchemaWithId<typeof description>>,
context: Context<typeof description>,
) => {
export const remove = async (payload: RemovePayload<SchemaWithId<typeof description>>, context: Context<typeof description>) => {
const file = await context.collection.model.findOne({
_id: <ObjectId>payload.filters._id,
}, {
Expand Down
9 changes: 3 additions & 6 deletions packages/builtins/src/collections/file/removeAll.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,12 @@
import type { Context, SchemaWithId, PackReferences, RemoveAllPayload } from '@aeriajs/types'
import type { description } from './description.js'
import { remove as originalRemoveAll, type ObjectId } from '@aeriajs/core'
import { remove as originalRemoveAll } from '@aeriajs/core'
import * as fs from 'fs/promises'

export const removeAll = async (
payload: RemoveAllPayload,
context: Context<typeof description>,
) => {
export const removeAll = async (payload: RemoveAllPayload, context: Context<typeof description>) => {
const files = context.collection.model.find({
_id: {
$in: payload.filters as ObjectId[],
$in: payload.filters,
},
}, {
projection: {
Expand Down
4 changes: 2 additions & 2 deletions packages/common/src/isReference.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import type { Property, RefProperty, ArrayOfRefs } from '@aeriajs/types'
import type { Property, RefProperty, ArrayProperty } from '@aeriajs/types'

export const isReference = (property: Property): property is RefProperty | ArrayOfRefs => {
export const isReference = (property: Property): property is RefProperty | ArrayProperty<RefProperty> => {
return 'items' in property
? '$ref' in property.items
: '$ref' in property
Expand Down
68 changes: 34 additions & 34 deletions packages/core/src/collection/cascadingRemove.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,39 @@
import type { Context } from '@aeriajs/types'
import type { Context, RouteContext } from '@aeriajs/types'
import type * as functions from '../functions/index.js'
import { ObjectId } from 'mongodb'
import { createContext } from '../context.js'
import { getFunction } from '../assets.js'
import { getDatabaseCollection } from '../database.js'
import { getReferences, type ReferenceMap, type Reference } from './reference.js'

const isObject = (value: unknown): value is Record<string, unknown> => {
return typeof value === 'object'
const internalCascadingRemove = async (target: Record<string, unknown>, refMap: ReferenceMap, context: RouteContext) => {
for( const refName in refMap ) {
const reference = refMap[refName]

if( !target[refName] ) {
continue
}

if( reference.referencedCollection ) {
if( reference.isInline || reference.referencedCollection === 'file' ) {
if( target[refName] instanceof ObjectId || Array.isArray(target[refName]) ) {
await preferredRemove(target[refName], reference, context)
}
}
} else if( reference.deepReferences ) {
if( Array.isArray(target[refName]) ) {
for( const elem of target[refName] ) {
await internalCascadingRemove(elem, reference.deepReferences, context)
}
continue
}

await internalCascadingRemove(target[refName] as Record<string, unknown>, reference.deepReferences, context)
}
}
}

const preferredRemove = async (targetId: ObjectId | ObjectId[], reference: Reference, parentContext: Context) => {
export const preferredRemove = async (targetId: ObjectId | (ObjectId | null)[], reference: Reference, parentContext: RouteContext) => {
if( !reference.referencedCollection ) {
return
}
Expand All @@ -22,16 +45,21 @@ const preferredRemove = async (targetId: ObjectId | ObjectId[], reference: Refer
})

if( Array.isArray(targetId) ) {
if( targetId.length === 0 ) {
return
}

const nonNullable = targetId.filter((id) => !!id)
const { result: removeAll } = await getFunction<typeof functions.removeAll>(reference.referencedCollection, 'removeAll')
if( removeAll ) {
return removeAll({
filters: targetId,
filters: nonNullable,
}, context)
}

return coll.deleteMany({
_id: {
$in: targetId,
$in: nonNullable,
},
})
}
Expand All @@ -50,34 +78,6 @@ const preferredRemove = async (targetId: ObjectId | ObjectId[], reference: Refer
})
}

const internalCascadingRemove = async (target: Record<string, unknown>, refMap: ReferenceMap, context: Context) => {
for( const refName in refMap ) {
const reference = refMap[refName]
if( !target[refName] ) {
continue
}

if( reference.isInline || reference.referencedCollection === 'file' ) {
if( target[refName] instanceof ObjectId ) {
await preferredRemove(target[refName], reference, context)
}
}

if( reference.deepReferences ) {
if( Array.isArray(target[refName]) ) {
for( const elem of target[refName] ) {
await internalCascadingRemove(elem, reference.deepReferences, context)
}
continue
}

if( isObject(target[refName]) ) {
await internalCascadingRemove(target[refName], reference.deepReferences, context)
}
}
}
}

export const cascadingRemove = async (target: Record<string, unknown>, context: Context) => {
const refMap = await getReferences(context.description.properties)
return internalCascadingRemove(target, refMap, context)
Expand Down
8 changes: 5 additions & 3 deletions packages/core/src/collection/reference.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,16 +67,18 @@ export const getReferences = async (properties: FixedObjectProperty['properties'
if( refProperty ) {
const description = throwIfError(await getCollectionAsset(refProperty.$ref, 'description'))

if( refProperty.populate ) {
if( refProperty.populate.length === 0 ) {
if( refProperty.inline || refProperty.populate ) {
if( refProperty.populate && refProperty.populate.length === 0 ) {
continue
}

const deepReferences = await getReferences(description.properties, {
depth: depth + 1,
maxDepth: refProperty.populateDepth || maxDepth,
memoize: `${memoize}.${propName}`,
populate: Array.from(refProperty.populate),
populate: refProperty.populate
? Array.from(refProperty.populate)
: undefined,
isArrayElement: 'items' in property,
})

Expand Down
101 changes: 44 additions & 57 deletions packages/core/src/collection/traverseDocument.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
import type { WithId } from 'mongodb'
import type { Description, Property, ValidationError, RouteContext } from '@aeriajs/types'
import { Result, ACError, ValidationErrorCode, TraverseError } from '@aeriajs/types'
import { throwIfError, pipe, isReference, getValueFromPath, isError } from '@aeriajs/common'
import { throwIfError, pipe, isReference, getReferenceProperty, getValueFromPath, isError } from '@aeriajs/common'
import { makeValidationError, validateProperty, validateWholeness } from '@aeriajs/validation'
import { getCollection } from '@aeriajs/entrypoint'
import { ObjectId } from 'mongodb'
import { getCollectionAsset } from '../assets.js'
import { createContext } from '../context.js'
import { preloadDescription } from './preload.js'
import { getReferences, type Reference } from './reference.js'
import { preferredRemove } from './cascadingRemove.js'
import * as path from 'path'
import * as fs from 'fs/promises'

Expand All @@ -19,6 +22,7 @@ export type TraverseOptionsBase = {
undefinedToNull?: boolean
preserveHidden?: boolean
recurseDeep?: boolean
cleanupReferences?: boolean
recurseReferences?: boolean
}

Expand Down Expand Up @@ -85,60 +89,52 @@ const getProperty = (propName: string, parentProperty: Property | Description) =
}
}

const disposeOldFiles = async (ctx: PhaseContext, options: { preserveIds?: ObjectId[] } = {}) => {
if( !options.preserveIds && Array.isArray(ctx.target[ctx.propName]) ) {
return
}
const cleanupReferences = async (value: unknown, ctx: PhaseContext) => {
if( ctx.root._id ) {
const refProperty = getReferenceProperty(ctx.property)
if( refProperty && (refProperty.$ref === 'file' || refProperty.inline) ) {
if( ctx.isArray && !Array.isArray(value) ) {
return value
}

const context = ctx.options.context!
const context = ctx.options.context!

const doc = await context.collections[ctx.options.description.$id].model.findOne({
_id: new ObjectId(ctx.root._id),
}, {
projection: {
[ctx.propPath]: 1,
},
})
const doc = await context.collections[ctx.options.description.$id].model.findOne({
_id: new ObjectId(ctx.root._id),
}, {
projection: {
[ctx.propPath]: 1,
},
})

if( !doc ) {
return Result.error(TraverseError.InvalidDocumentId)
}
if( !doc ) {
return Result.error(TraverseError.InvalidDocumentId)
}

let fileIds = getValueFromPath<(ObjectId | null)[] | undefined>(doc, ctx.propPath)
if( !fileIds ) {
return
}
let referenceIds = getValueFromPath<(ObjectId | null)[] | ObjectId | undefined>(doc, ctx.propPath)
if( !referenceIds ) {
return value
}

if( options.preserveIds ) {
fileIds = fileIds.filter((id) => !id || !options.preserveIds!.some((fromId) => {
return id.equals(fromId)
}))
}
if( Array.isArray(referenceIds) ) {
if( !Array.isArray(value) ) {
throw new Error
}

const fileFilters = {
_id: {
$in: Array.isArray(fileIds)
? fileIds
: [fileIds],
},
}
referenceIds = referenceIds.filter((oldId) => !(value as ObjectId[]).some((valueId) => valueId.equals(oldId)))
}

const files = context.collections.file.model.find(fileFilters, {
projection: {
absolute_path: 1,
},
})
const refMap = await getReferences(ctx.options.description.properties)
const reference = getValueFromPath<Reference>(refMap, ctx.propPath)

let file: Awaited<ReturnType<typeof files.next>>
while( file = await files.next() ) {
try {
await fs.unlink(file.absolute_path)
} catch( err ) {
console.trace(err)
await preferredRemove(referenceIds, reference, await createContext({
parentContext: context,
collectionName: refProperty.$ref,
}))
}
}

return context.collections.file.model.deleteMany(fileFilters)
return value
}

const autoCast = (value: unknown, ctx: Omit<PhaseContext, 'options'> & { options: (TraverseOptions & TraverseNormalized) | {} }): unknown => {
Expand Down Expand Up @@ -276,9 +272,6 @@ const moveFiles = async (value: unknown, ctx: PhaseContext) => {
}

if( !value ) {
if( ctx.root._id && !ctx.isArray ) {
await disposeOldFiles(ctx)
}
return null
}

Expand All @@ -298,10 +291,6 @@ const moveFiles = async (value: unknown, ctx: PhaseContext) => {
return Result.error(TraverseError.InvalidTempfile)
}

if( ctx.root._id && !ctx.isArray ) {
await disposeOldFiles(ctx)
}

const { _id: fileId, ...newFile } = tempFile
newFile.absolute_path = `${ctx.options.context.config.storage!.fs}/${tempFile.absolute_path.split(path.sep).at(-1)}`
newFile.owner = ctx.options.context.token.sub
Expand Down Expand Up @@ -341,12 +330,6 @@ const recurseDeep = async (value: unknown, ctx: PhaseContext) => {
items.push(result)
}

if( 'moveFiles' in ctx.options && ctx.options.moveFiles && '$ref' in ctx.property.items && ctx.property.items.$ref === 'file' ) {
await disposeOldFiles(ctx, {
preserveIds: items,
})
}

return items
}

Expand Down Expand Up @@ -592,6 +575,10 @@ export const traverseDocument = async <TWhat>(
functions.push(validate)
}

if( options.cleanupReferences ) {
functions.push(cleanupReferences)
}

if( 'moveFiles' in options && options.moveFiles ) {
functions.push(moveFiles)
}
Expand Down
1 change: 1 addition & 0 deletions packages/core/src/database.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ export const getDatabase = async () => {
console.debug(inspect(event, {
colors: true,
compact: false,
depth: Infinity,
}))
})
}
Expand Down
1 change: 1 addition & 0 deletions packages/core/src/functions/insert.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ const internalInsert = async <TContext extends Context>(
? []
: context.description.required,
moveFiles: true,
cleanupReferences: true,
fromProperties: !isUpdate,
undefinedToNull: true,
preserveHidden: true,
Expand Down
7 changes: 2 additions & 5 deletions packages/core/src/functions/removeAll.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,11 @@ export type RemoveAllOptions = {
}

const internalRemoveAll = async <TContext extends Context>(payload: RemoveAllPayload, context: TContext) => {
const filtersWithId = {
...payload.filters,
const filters = throwIfError(await traverseDocument<Record<string, unknown>>({
_id: {
$in: payload.filters,
},
}

const filters = throwIfError(await traverseDocument<Record<string, unknown>>(filtersWithId, context.description, {
}, context.description, {
autoCast: true,
}))

Expand Down
Loading

0 comments on commit 4f9f9dc

Please sign in to comment.