Skip to content

Commit

Permalink
Merge pull request #104 from mbret/develop
Browse files Browse the repository at this point in the history
release
  • Loading branch information
mbret authored Mar 16, 2024
2 parents 745fe5a + 08b6937 commit 53265d3
Show file tree
Hide file tree
Showing 42 changed files with 913 additions and 761 deletions.
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.

235 changes: 235 additions & 0 deletions packages/api/src/libs/sync/books/createOrUpdateBook.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,235 @@
import {
BookDocType,
GoogleDriveDataSourceData,
directives
} from "@oboku/shared"
import {
DataSourcePlugin,
SynchronizeAbleDataSource
} from "@libs/plugins/types"
import nano from "nano"
import { Logger } from "@libs/logger"
import { updateTagsForBook } from "./updateTagsForBook"
import { synchronizeBookWithParentCollections } from "./synchronizeBookWithParentCollections"

type Helpers = Parameters<NonNullable<DataSourcePlugin["sync"]>>[1]
type Context = Parameters<NonNullable<DataSourcePlugin["sync"]>>[0] & {
db: nano.DocumentScope<unknown>
}
type SynchronizeAbleItem = SynchronizeAbleDataSource["items"][number]

const logger = Logger.namespace("sync.books")

function isFolder(
item: SynchronizeAbleDataSource | SynchronizeAbleItem
): item is SynchronizeAbleItem {
return (item as SynchronizeAbleItem).type === "folder"
}

export const createOrUpdateBook = async ({
ctx: { dataSourceType, dataSourceId },
helpers,
parents,
item
}: {
ctx: Context
parents: (SynchronizeAbleItem | SynchronizeAbleDataSource)[]
item: SynchronizeAbleItem
helpers: Helpers
}) => {
try {
logger.log(`createOrUpdateBook "${item.name}":`, item.resourceId)

const parentTagNames = parents.reduce(
(tags: string[], parent) => [
...tags,
...directives.extractDirectivesFromName(parent.name).tags
],
[]
)
const metadata = directives.extractDirectivesFromName(item.name)
const parentFolders = parents.filter((parent) =>
isFolder(parent)
) as SynchronizeAbleItem[]

const linkForResourceId = await helpers.findOne("link", {
selector: { resourceId: item.resourceId }
})

if (linkForResourceId) {
/**
* We have a matching link for this item but it's not attached to dataSource.
* We update it
*/
if (!linkForResourceId.dataSourceId) {
logger.log(
`${item.name} has a link not yet attached to any dataSource, updating it with current dataSource ${dataSourceId}`
)

await helpers.atomicUpdate("link", linkForResourceId._id, (old) => ({
...old,
dataSourceId,
modifiedAt: new Date().toISOString()
}))
}

/**
* We have a matching link for this item but it's attached to a different
* dataSource. We will check if the dataSource actually exist, if not we
* will attach it to this one. This help repair broken link.
* This scenario can happen when the user delete a dataSource without deleting
* the books associated with it.
*/
if (
linkForResourceId.dataSourceId &&
linkForResourceId.dataSourceId !== dataSourceId
) {
const dataSourceFoundForThisLink = await helpers.findOne("datasource", {
selector: { _id: linkForResourceId.dataSourceId }
})

/**
* If we find a dataSource, we don't need to synchronize this item
* as it's managed by another dataSource
*/
if (dataSourceFoundForThisLink) {
return
}

logger.log(
`${item.name} has a link attached to a non existing dataSource, updating it with current dataSource ${dataSourceId}`
)

await helpers.atomicUpdate("link", linkForResourceId._id, (old) => ({
...old,
dataSourceId,
modifiedAt: new Date().toISOString()
}))
}
}

let existingBook: BookDocType | null = null

if (linkForResourceId?.book) {
existingBook = await helpers.findOne("book", {
selector: { _id: linkForResourceId.book }
})

if (existingBook) {
if (!existingBook.isAttachedToDataSource) {
logger.log(
`createOrUpdateBook "${item.name}": isAttachedToDataSource is false and therefore will be migrated as true`,
existingBook._id
)
await helpers.atomicUpdate("book", existingBook._id, (data) => ({
...data,
isAttachedToDataSource: true
}))
}
// logger.log(`createOrUpdateBook "${item.name}": existingBook`, existingBook._id)
}
}

if (!linkForResourceId || !existingBook) {
let bookId = existingBook?._id

if (!bookId) {
logger.log(
`createOrUpdateBook "${item.name}": new file detected, creating book`
)
const insertedBook = await helpers.createBook({
isAttachedToDataSource: true,
metadata: [
{
title: item.name,
type: "link"
}
]
})
bookId = insertedBook.id
}

if (!bookId) return

const insertedLink = await helpers.create("link", {
type: dataSourceType,
resourceId: item.resourceId,
book: bookId,
data: JSON.stringify({}),
createdAt: new Date().toISOString(),
modifiedAt: null,
dataSourceId,
rxdbMeta: {
lwt: new Date().getTime()
}
})
await helpers.addLinkToBook(bookId, insertedLink.id)
await updateTagsForBook(
bookId,
[...metadata.tags, ...parentTagNames],
helpers
)
await synchronizeBookWithParentCollections(bookId, parentFolders, helpers)

/**
* Because it's a new book, we start a metadata refresh
*/
helpers.refreshBookMetadata({ bookId: bookId }).catch(logger.error)
} else {
/**
* We already have a link that exist for this datasource with this book.
* We will try to retrieve the book and update it if needed.
*/
// We check the last updated date of the book
const lastMetadataUpdatedAt = new Date(
existingBook?.lastMetadataUpdatedAt || 0
)

const metadataAreOlderThanModifiedDate =
lastMetadataUpdatedAt < new Date(item.modifiedAt || 0)

if (
metadataAreOlderThanModifiedDate ||
!(await helpers.isBookCoverExist(existingBook._id))
) {
helpers
.refreshBookMetadata({ bookId: existingBook?._id })
.catch(logger.error)

logger.log(
`book ${
linkForResourceId.book
} has changed in metadata, refresh triggered ${lastMetadataUpdatedAt} ${new Date(
item.modifiedAt || 0
)}`
)
}

await synchronizeBookWithParentCollections(
existingBook._id,
parentFolders,
helpers
)

await updateTagsForBook(
existingBook._id,
[...metadata.tags, ...parentTagNames],
helpers
)

// Finally we update the tags to the book if needed
const { applyTags } =
await helpers.getDataSourceData<GoogleDriveDataSourceData>()
await helpers.addTagsToBook(existingBook._id, applyTags || [])
}

logger.log(`createOrUpdateBook "${item.name}": DONE!`)
} catch (e) {
logger.error(
`createOrUpdateBook something went wrong for book ${item.name} (${item.resourceId})`
)
logger.error(e)

throw e
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
import { Logger } from "@libs/logger"
import {
DataSourcePlugin,
SynchronizeAbleDataSource
} from "@libs/plugins/types"

const logger = Logger.namespace("sync")

type Helpers = Parameters<NonNullable<DataSourcePlugin["sync"]>>[1]
type SynchronizeAbleItem = SynchronizeAbleDataSource["items"][number]

/**
* For every parents of the book we will lookup if there are collections that exist without
* referencing it. If so then we will attach the collection and the book together
*/
export const synchronizeBookWithParentCollections = async (
bookId: string,
parents: SynchronizeAbleItem[],
helpers: Helpers
) => {
const parentResourceIds = parents?.map((parent) => parent.resourceId) || []

logger.log(
`synchronizeBookWithParentCollections`,
`${bookId} with ${parentResourceIds.length} parentResourceIds ${parentResourceIds}`
)

// Retrieve all the new collection to which attach the book and add the book in the list
// if there is no collection we don't run the query since it will return everything because of the empty $or
if (parentResourceIds.length > 0) {
/**
* Use case:
* Some collections does not have the book yet
*
* Result:
* We attach all the parent collections to the book by combining them with existing collection of the book.
* Make sure to not remove any existing collection from the book and to avoid duplicate
*/
const collectionsThatHaveNotThisBookAsReferenceYet = await helpers.find(
"obokucollection",
{
selector: {
$or: parentResourceIds.map((linkResourceId) => ({ linkResourceId })),
books: {
$nin: [bookId]
}
}
}
)

if (collectionsThatHaveNotThisBookAsReferenceYet.length > 0) {
logger.log(
`synchronizeBookWithParentCollections ${collectionsThatHaveNotThisBookAsReferenceYet.length} collections does not have ${bookId} attached to them yet`
)
await Promise.all(
collectionsThatHaveNotThisBookAsReferenceYet.map((collection) =>
helpers.atomicUpdate("obokucollection", collection._id, (old) => ({
...old,
books: [...old.books.filter((id) => id !== bookId), bookId]
}))
)
)
}

const parentCollections = await helpers.find("obokucollection", {
selector: {
$or: parentResourceIds.map((linkResourceId) => ({ linkResourceId }))
}
})
const parentCollectionIds = parentCollections.map(({ _id }) => _id)

/**
* Use case:
* The book does not have one of the parent collection yet
*
* Result:
* We attach all the parent collections to the book by combining them with existing collection of the book.
* Make sure to not remove any existing collection from the book and to avoid duplicate
*/
const { collections: bookCollections } =
(await helpers.findOne("book", {
selector: { _id: bookId },
fields: [`collections`]
})) || {}

if (bookCollections) {
const bookHasNotOneOfTheCollectionsYet = parentCollectionIds.some(
(collectionId) => !bookCollections.includes(collectionId)
)
if (bookHasNotOneOfTheCollectionsYet) {
logger.log(
`synchronizeBookWithParentCollections ${bookId} has some missing parent collections. It will be updated to include them`
)
await helpers.atomicUpdate("book", bookId, (old) => ({
...old,
collections: [
...new Set([...old.collections, ...parentCollectionIds])
]
}))
}
}
}

/**
* Use case:
* The book does not have parent collections
*
* Result:
* We do not remove collection yet. See for the future
*/
if (parentResourceIds.length === 0) {
// @todo remove collections from the book ?
}
}
Loading

0 comments on commit 53265d3

Please sign in to comment.