Skip to content

Commit

Permalink
!refactor: update tables name and schema and better checksum check
Browse files Browse the repository at this point in the history
  • Loading branch information
farnabaz committed Nov 8, 2024
1 parent 06ba18d commit 893252b
Show file tree
Hide file tree
Showing 23 changed files with 101 additions and 85 deletions.
4 changes: 2 additions & 2 deletions docs/content/docs/2.usage/1.collections.md
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ const { data: posts } = await useAsyncData('blog', () => queryCollection('blog')
<div>
<h1>Blog</h1>
<ul>
<li v-for="post in posts" :key="post._id">
<li v-for="post in posts" :key="post.id">
<NuxtLink :to="post.path">{{ post.title }}</NuxtLink>
</li>
</ul>
Expand Down Expand Up @@ -138,7 +138,7 @@ type CollectionSource = {
Every collection includes these default fields:
- `_id`: Unique content identifier
- `id`: Unique content identifier
- `stem`: File path without extension (used for sorting and location)
- `extension`: File extension
- `meta`: Custom fields not defined in the collection schema
Expand Down
4 changes: 2 additions & 2 deletions examples/blog/app/pages/index.vue
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<script lang="ts" setup>
const { data: posts } = await useAsyncData(() => {
return queryCollection('blog')
.select('title', 'description', 'path', '_id', 'date')
.select('title', 'description', 'path', 'id', 'date')
.order('date', 'DESC')
.all()
})
Expand All @@ -12,7 +12,7 @@ const { data: posts } = await useAsyncData(() => {
<h1>Blog</h1>
<p
v-for="post in posts"
:key="post._id"
:key="post.id"
>
>
<nuxt-link :to="post.path">
Expand Down
13 changes: 7 additions & 6 deletions src/module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -213,17 +213,18 @@ async function processCollectionItems(nuxt: Nuxt, collections: ResolvedCollectio
contentBuild: options.build?.markdown,
})

const infoCollection = collections.find(c => c.name === '_info')!
const infoCollection = collections.find(c => c.name === 'info')!

const startTime = performance.now()
let filesCount = 0
let cachedFilesCount = 0
let parsedFilesCount = 0
// Create database dump
for await (const collection of collections) {
if (collection.name === '_info') {
if (collection.name === 'info') {
continue
}
const collectionHash = hash(collection)
collectionDump[collection.name] = []
// Collection table definition
collectionDump[collection.name]!.push(
Expand All @@ -249,7 +250,7 @@ async function processCollectionItems(nuxt: Nuxt, collections: ResolvedCollectio
await Promise.all(chunk.map(async (key) => {
const keyInCollection = join(collection.name, collection.source?.prefix || '', key)
const content = await readFile(join(cwd, key), 'utf8')
const checksum = getContentChecksum(configHash + content)
const checksum = getContentChecksum(configHash + collectionHash + content)
const cache = databaseContents[keyInCollection]

let parsedContent
Expand All @@ -274,14 +275,14 @@ async function processCollectionItems(nuxt: Nuxt, collections: ResolvedCollectio

collectionDump[collection.name]!.push(
generateCollectionTableDefinition(infoCollection, { drop: false }),
`DELETE FROM ${infoCollection.tableName} WHERE _id = 'checksum_${collection.name}'`,
generateCollectionInsert(infoCollection, { _id: `checksum_${collection.name}`, version: collectionChecksum[collection.name] }),
`DELETE FROM ${infoCollection.tableName} WHERE id = 'checksum_${collection.name}'`,
generateCollectionInsert(infoCollection, { id: `checksum_${collection.name}`, value: collectionChecksum[collection.name] }),
)
}

const sqlDumpList = Object.values(collectionDump).flatMap(a => a)

// Drop _info table and recreate it
// Drop info table and recreate it
db.exec(`DROP TABLE IF EXISTS ${infoCollection.tableName}`)
for (const sql of sqlDumpList) {
db.exec(sql)
Expand Down
2 changes: 1 addition & 1 deletion src/runtime/components/ContentRenderer.vue
Original file line number Diff line number Diff line change
Expand Up @@ -214,6 +214,6 @@ function findMappedTag(node: MDCElement, tags: Record<string, string>) {
:prose="props.prose"
:unwrap="props.unwrap"
:components="componentsMap"
:data-content-id="debug ? value._id : undefined"
:data-content-id="debug ? value.id : undefined"
/>
</template>
2 changes: 1 addition & 1 deletion src/runtime/internal/collection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,5 +29,5 @@ function findJsonFields(sql: string): string[] {
}

function getCollectionName(table: string) {
return table.replace(/^content_/, '')
return table.replace(/^_content_/, '')
}
4 changes: 2 additions & 2 deletions src/runtime/internal/database.client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ async function loadAdapter<T>(collection: T) {
const dumpId = `collection_${collection}`
let checksumState = 'matched'
try {
const dbChecksum = db.exec({ sql: `SELECT * FROM ${tables._info} where _id = '${checksumId}'`, rowMode: 'object', returnValue: 'resultRows' })
const dbChecksum = db.exec({ sql: `SELECT * FROM ${tables.info} where id = '${checksumId}'`, rowMode: 'object', returnValue: 'resultRows' })
.shift()

if (dbChecksum?.version !== checksums[String(collection)]) {
Expand Down Expand Up @@ -90,7 +90,7 @@ async function loadAdapter<T>(collection: T) {

await db.exec({ sql: `DROP TABLE IF EXISTS ${tables[String(collection)]}` })
if (checksumState === 'mismatch') {
await db.exec({ sql: `DELETE FROM ${tables._info} WHERE _id = '${checksumId}'` })
await db.exec({ sql: `DELETE FROM ${tables.info} WHERE id = '${checksumId}'` })
}

for (const command of dump) {
Expand Down
17 changes: 9 additions & 8 deletions src/runtime/internal/database.server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,34 +39,35 @@ export default function loadDatabaseAdapter(config: RuntimeConfig) {
}

const checkDatabaseIntegrity = {} as Record<string, boolean>
let integrityCheckPromise: Promise<void> | null = null
const integrityCheckPromise = {} as Record<string, Promise<void> | null>
export async function checkAndImportDatabaseIntegrity(event: H3Event, collection: string, config: RuntimeConfig): Promise<void> {
if (checkDatabaseIntegrity[String(collection)] !== false) {
checkDatabaseIntegrity[String(collection)] = false
integrityCheckPromise = integrityCheckPromise || _checkAndImportDatabaseIntegrity(event, collection, checksums[String(collection)], config)
integrityCheckPromise[String(collection)] = integrityCheckPromise[String(collection)] || _checkAndImportDatabaseIntegrity(event, collection, checksums[String(collection)], config)
.then((isValid) => { checkDatabaseIntegrity[String(collection)] = !isValid })
.catch((error) => {
console.log('Database integrity check failed', error)
checkDatabaseIntegrity[String(collection)] = true
integrityCheckPromise = null
integrityCheckPromise[String(collection)] = null
})
}

if (integrityCheckPromise) {
await integrityCheckPromise
if (integrityCheckPromise[String(collection)]) {
await integrityCheckPromise[String(collection)]
}
}

async function _checkAndImportDatabaseIntegrity(event: H3Event, collection: string, integrityVersion: string, config: RuntimeConfig) {
const db = await loadDatabaseAdapter(config)

const before = await db.first<{ version: string }>(`select * from ${tables._info}`).catch(() => ({ version: '' }))
const before = await db.first<{ version: string }>(`select * from ${tables.info} where id = 'checksum_${collection}'`).catch(() => ({ version: '' }))

if (before?.version) {
if (before?.version === integrityVersion) {
return true
}
// Delete old version
await db.exec(`DELETE FROM ${tables._info} WHERE version = '${before.version}'`)
await db.exec(`DELETE FROM ${tables.info} WHERE id = 'checksum_${collection}'`)
}

const dump = await loadDatabaseDump(event, collection).then(decompressSQLDump)
Expand All @@ -80,7 +81,7 @@ async function _checkAndImportDatabaseIntegrity(event: H3Event, collection: stri
})
}, Promise.resolve())

const after = await db.first<{ version: string }>(`select * from ${tables._info}`).catch(() => ({ version: '' }))
const after = await db.first<{ version: string }>(`SELECT * FROM ${tables.info} WHERE id = 'checksum_${collection}'`).catch(() => ({ version: '' }))
return after?.version === integrityVersion
}

Expand Down
2 changes: 1 addition & 1 deletion src/types/collection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ export interface CollectionInfo {
}

export interface CollectionItemBase {
_id: string
id: string
stem: string
extension: string
meta: Record<string, unknown>
Expand Down
35 changes: 26 additions & 9 deletions src/utils/collection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@ import { logger } from './dev'

const JSON_FIELDS_TYPES = ['ZodObject', 'ZodArray', 'ZodRecord', 'ZodIntersection', 'ZodUnion', 'ZodAny']

function getTableName(name: string) {
return `content_${name}`
export function getTableName(name: string) {
return `_content_${name}`
}

export function defineCollection<T extends ZodRawShape>(collection: Collection<T>): DefinedCollection {
Expand Down Expand Up @@ -45,17 +45,24 @@ export function resolveCollection(name: string, collection: DefinedCollection):
name,
type: collection.type || 'page',
tableName: getTableName(name),
private: name === '_info',
private: name === 'info',
}
}

export function resolveCollections(collections: Record<string, DefinedCollection>): ResolvedCollection[] {
collections._info = defineCollection({
collections.info = {
type: 'data',
source: undefined,
schema: z.object({
version: z.string(),
id: z.string(),
value: z.string(),
}),
})
extendedSchema: z.object({
id: z.string(),
value: z.string(),
}),
jsonFields: [],
}

return Object.entries(collections)
.map(([name, collection]) => resolveCollection(name, collection))
Expand Down Expand Up @@ -89,7 +96,7 @@ function resolveSource(source: string | CollectionSource | undefined): ResolvedC
export function generateCollectionInsert(collection: ResolvedCollection, data: Record<string, unknown>) {
const fields: string[] = []
const values: Array<string | number | boolean> = []
const sortedKeys = Object.keys((collection.extendedSchema).shape).sort()
const sortedKeys = getOrderedColumns((collection.extendedSchema).shape)

sortedKeys.forEach((key) => {
const value = (collection.extendedSchema).shape[key]
Expand Down Expand Up @@ -128,12 +135,12 @@ export function generateCollectionInsert(collection: ResolvedCollection, data: R

// Convert a collection with Zod schema to SQL table definition
export function generateCollectionTableDefinition(collection: ResolvedCollection, opts: { drop?: boolean } = {}) {
const sortedKeys = Object.keys((collection.extendedSchema).shape).sort()
const sortedKeys = getOrderedColumns((collection.extendedSchema).shape)
const sqlFields = sortedKeys.map((key) => {
const type = (collection.extendedSchema).shape[key]!
const underlyingType = getUnderlyingType(type)

if (key === '_id') return `${key} TEXT PRIMARY KEY`
if (key === 'id') return `${key} TEXT PRIMARY KEY`

let sqlType: string = ZodToSqlFieldTypes[underlyingType.constructor.name as ZodFieldType]

Expand Down Expand Up @@ -180,3 +187,13 @@ export function generateCollectionTableDefinition(collection: ResolvedCollection

return definition
}

function getOrderedColumns(shape: ZodRawShape) {
const keys = new Set([
shape.id ? 'id' : undefined,
shape.title ? 'title' : undefined,
...Object.keys(shape).sort(),
].filter(Boolean))

return Array.from(keys) as string[]
}
4 changes: 2 additions & 2 deletions src/utils/content/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,8 +88,8 @@ export async function parseContent(key: string, content: string, collection: Res
},
})

const { id: _id, ...parsedContentFields } = parsedContent
const result = { _id } as typeof collection.extendedSchema._type
const { id: id, ...parsedContentFields } = parsedContent
const result = { id } as typeof collection.extendedSchema._type
const meta = {} as Record<string, unknown>

const collectionKeys = Object.keys(collection.extendedSchema.shape)
Expand Down
4 changes: 2 additions & 2 deletions src/utils/dev.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ export async function watchContents(nuxt: Nuxt, options: ModuleOptions, manifest
}

async function broadcast(collection: ResolvedCollection, key: string, insertQuery?: string) {
const removeQuery = `DELETE FROM ${collection.tableName} WHERE _id = '${key}'`
const removeQuery = `DELETE FROM ${collection.tableName} WHERE id = '${key}'`
await db.exec(removeQuery)
if (insertQuery) {
await db.exec(insertQuery)
Expand Down Expand Up @@ -96,7 +96,7 @@ export async function watchContents(nuxt: Nuxt, options: ModuleOptions, manifest
const localCache = db.fetchDevelopmentCacheForKey(keyInCollection)

if (localCache && localCache.checksum === checksum) {
db.exec(`DELETE FROM ${collection.tableName} WHERE _id = '${keyInCollection}'`)
db.exec(`DELETE FROM ${collection.tableName} WHERE id = '${keyInCollection}'`)
db.exec(generateCollectionInsert(collection, JSON.parse(localCache.parsedContent)))
return
}
Expand Down
2 changes: 1 addition & 1 deletion src/utils/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { ContentFileExtension } from '../types/content'
import { getEnumValues } from './zod'

export const metaSchema = z.object({
_id: z.string(),
id: z.string(),
stem: z.string(),
extension: z.enum(getEnumValues(ContentFileExtension)),
meta: z.record(z.string(), z.any()),
Expand Down
9 changes: 5 additions & 4 deletions test/base.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@ import { setup, $fetch } from '@nuxt/test-utils'
import { afterAll, describe, expect, test } from 'vitest'
import { loadContentConfig } from '../src/utils/config'
import { decompressSQLDump } from '../src/runtime/internal/dump'
import { localDatabase, getTableName } from './utils/database'
import { getTableName } from '../src/utils/collection'
import { localDatabase } from './utils/database'

async function cleanup() {
await fs.rm(fileURLToPath(new URL('./fixtures/empty/node_modules', import.meta.url)), { recursive: true, force: true })
Expand Down Expand Up @@ -34,7 +35,7 @@ describe('empty', async () => {
const rootDir = fileURLToPath(new URL('./fixtures/empty', import.meta.url))
const config = await loadContentConfig(rootDir, { defaultFallback: true })

// Pages collection + _info collection
// Pages collection + info collection
expect(config.collections.length).toBe(2)
expect(config.collections.map(c => c.name)).toContain('content')

Expand Down Expand Up @@ -79,7 +80,7 @@ describe('empty', async () => {

expect(parsedDump.filter(item => item.startsWith('DROP TABLE IF EXISTS'))).toHaveLength(1)
expect(parsedDump.filter(item => item.startsWith('CREATE TABLE IF NOT EXISTS'))).toHaveLength(2)
// Only _info collection is inserted
// Only info collection is inserted
expect(parsedDump.filter(item => item.startsWith('INSERT INTO'))).toHaveLength(1)
})

Expand All @@ -91,7 +92,7 @@ describe('empty', async () => {

expect(parsedDump.filter(item => item.startsWith('DROP TABLE IF EXISTS'))).toHaveLength(1)
expect(parsedDump.filter(item => item.startsWith('CREATE TABLE IF NOT EXISTS'))).toHaveLength(2)
// Only _info collection is inserted
// Only info collection is inserted
expect(parsedDump.filter(item => item.startsWith('INSERT INTO'))).toHaveLength(1)
})
})
Expand Down
2 changes: 1 addition & 1 deletion test/unit/defineCollection.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { describe, expect, test } from 'vitest'
import { z } from 'zod'
import { defineCollection } from '../../src/utils/collection'

const metaFields = ['_id', 'stem', 'meta', 'extension']
const metaFields = ['id', 'stem', 'meta', 'extension']
const pageFields = ['path', 'title', 'description', 'seo', 'body', 'navigation']

function expectProperties(shape: z.ZodRawShape, fields: string[]) {
Expand Down
7 changes: 3 additions & 4 deletions test/unit/generateCollectionInsert.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import { describe, expect, test } from 'vitest'
import { z } from 'zod'
import { generateCollectionInsert, defineCollection, resolveCollection } from '../../src/utils/collection'
import { getTableName } from '../utils/database'
import { generateCollectionInsert, defineCollection, resolveCollection, getTableName } from '../../src/utils/collection'

describe('generateCollectionInsert', () => {
test('Respect Schema\'s default values', () => {
Expand All @@ -16,7 +15,7 @@ describe('generateCollectionInsert', () => {
}),
}))!
const sql = generateCollectionInsert(collection, {
_id: 'foo.md',
id: 'foo.md',
stem: 'foo',
extension: 'md',
meta: {},
Expand All @@ -41,7 +40,7 @@ describe('generateCollectionInsert', () => {
}),
}))!
const sql = generateCollectionInsert(collection, {
_id: 'foo.md',
id: 'foo.md',
stem: 'foo',
extension: 'md',
meta: {},
Expand Down
Loading

0 comments on commit 893252b

Please sign in to comment.