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
43 changes: 38 additions & 5 deletions builder/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,8 @@ class DBType {

let current = schema
for (let i = 0; i < components.length; i++) {
// key fields of a versioned schema resolve through its latest version
if (current.isVersioned) current = current.versions[current.versions.length - 1].type
const field = current.fieldsByName.get(components[i])
if (!field) throw new Error('Could not resolve path: ' + path)
current = field.type
Expand Down Expand Up @@ -116,6 +118,31 @@ class Collection extends DBType {
const fields = []
const type = '/hyperdb#' + this.id

// derive a value variant per version so stored values keep their version dispatch and maps
if (schema.isVersioned && !parents.has(schema)) {
parents.add(schema)
let external = false
const versions = []
for (const v of schema.versions) {
const derived = this._deriveValueSchema(
v.type,
prefix,
primaryKeySet,
new Set([...parents])
)
if (derived.external) external = true
versions.push({ version: v.version, type: derived.fqn, map: v.map })
}
if (!external) return { external: false, fqn: getFQN(schema.namespace, schema.name) }
this.builder.schema.register({
derived: true,
namespace: schema.namespace,
name: schema.name + type,
versions
})
return { external: true, fqn: getFQN(schema.namespace, schema.name + type) }
}

if (!schema.isStruct || parents.has(schema)) return { external: false, fqn: schema.name }

parents.add(schema)
Expand Down Expand Up @@ -420,7 +447,9 @@ class Builder {
const dbJsonPath = p.join(p.resolve(dbDir), DB_JSON_FILE_NAME)
const codePath = p.join(p.resolve(dbDir), CODE_FILE_NAME)

fs.writeFileSync(messagesPath, hyperdb.schema.toCode({ esm }), { encoding: 'utf-8' })
fs.writeFileSync(messagesPath, hyperdb.schema.toCode({ esm, filename: messagesPath }), {
encoding: 'utf-8'
})
fs.writeFileSync(dbJsonPath, JSON.stringify(hyperdb.toJSON(), null, 2), { encoding: 'utf-8' })
fs.writeFileSync(codePath, generateCode(hyperdb, { directory: dbDir, esm }), {
encoding: 'utf-8'
Expand Down Expand Up @@ -456,10 +485,14 @@ function getFQN(namespace, name) {
function resolvePathToType(name, schema) {
const parts = name.split('.')

let field = schema.fieldsByName.get(parts[0])

for (let i = 1; i < parts.length && field; i++) {
field = field.type.fieldsByName.get(parts[i])
let field = null
let current = schema
for (let i = 0; i < parts.length; i++) {
// key fields of a versioned schema resolve through its latest version
if (current.isVersioned) current = current.versions[current.versions.length - 1].type
field = current.fieldsByName.get(parts[i])
if (!field) return null
current = field.type
}

return field
Expand Down
51 changes: 51 additions & 0 deletions test/basic.js
Original file line number Diff line number Diff line change
Expand Up @@ -513,6 +513,57 @@ test('nested keys', { bee2: false }, async function ({ create, bee }, t) {
await db.close()
})

test('collection - register - missing nested keys', async function ({ build }, t) {
t.plan(1)
const db = await build(createDB)
await db.close()

function createDB(HyperDB, Hyperschema, paths) {
const schema = Hyperschema.from(paths.schema)
const example = schema.namespace('db')

example.register({
name: 'bar',
fields: [
{
name: 'baz',
type: 'string',
required: true
}
]
})

example.register({
name: 'foo',
fields: [
{
name: 'bar',
type: '@db/bar',
required: true
}
]
})

Hyperschema.toDisk(schema)

const db = HyperDB.from(paths.schema, paths.db)
const exampleDB = db.namespace('db')

t.exception(
() =>
exampleDB.collections.register({
name: 'nonexistents',
schema: '@db/foo',
key: ['bar.nonexistent']
}),
/Field not found: bar\.nonexistent/,
'Throws when given bad key'
)

HyperDB.toDisk(db)
}
})

test('undo mutation without deletion', async function ({ create }, t) {
const db = await create()

Expand Down
5 changes: 5 additions & 0 deletions test/helpers/helpers.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,3 +25,8 @@ exports.mapMiddleAge = (record, context) => {
if (record.age > 40 && record.age < 60) return [record.age]
return []
}

exports.thingV1ToV2 = (record) => {
const { title, ...rest } = record
return { ...rest, version: 2, name: title }
}
172 changes: 172 additions & 0 deletions test/versions.js
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,36 @@ test.bee('define versionField on collection', async function ({ build }, t) {
await dbVersions.close()
})

test.bee('versioned collection schema maps old rows on read', async function ({ build }, t) {
const dir = await tmp(t, { dir: path.join(__dirname, 'fixtures/tmp') })

const db = await build(createVersionedDB, { dir })
await db.insert('@example/things', { version: 1, id: 'a', title: 'hello' })
await db.flush()
await db.close()

// build() re-requires the generated index.js but not messages.js - refresh it by hand
delete require.cache[require.resolve(path.join(dir, 'hyperdb/messages.js'))]

const db2 = await build(createVersionedDBWithV2, { dir })
const row = await db2.get('@example/things', { id: 'a' })
await db2.close()

t.alike(row, { version: 2, id: 'a', name: 'hello' })
})

test.bee('key path through a nested versioned field resolves', async function ({ build }, t) {
const dir = await tmp(t, { dir: path.join(__dirname, 'fixtures/tmp') })

const db = await build(createNestedVersionedDB, { dir })
await db.insert('@example/wrappers', { thing: { version: 1, id: 'a', title: 'hello' } })
await db.flush()
const row = await db.get('@example/wrappers', { thing: { id: 'a' } })
await db.close()

t.alike(row, { thing: { version: 1, id: 'a', title: 'hello' } })
})

function createExampleDB(HyperDB, Hyperschema, paths) {
const schema = Hyperschema.from(paths.schema)
const example = schema.namespace('example')
Expand Down Expand Up @@ -141,3 +171,145 @@ function createExampleDBWithVersions(HyperDB, Hyperschema, paths) {

HyperDB.toDisk(db)
}

function registerThingV1(example) {
example.register({
name: 'thing-v1',
fields: [
{
name: 'id',
type: 'string',
required: true
},
{
name: 'title',
type: 'string',
required: true
}
]
})
}

function createVersionedDB(HyperDB, Hyperschema, paths) {
const schema = Hyperschema.from(paths.schema)
const example = schema.namespace('example')

registerThingV1(example)

example.register({
name: 'thing',
versions: [
{
version: 1,
type: '@example/thing-v1'
}
]
})

Hyperschema.toDisk(schema)

const db = HyperDB.from(paths.schema, paths.db)
const exampleDB = db.namespace('example')

exampleDB.collections.register({
name: 'things',
schema: '@example/thing',
key: ['id']
})

HyperDB.toDisk(db)
}

function createVersionedDBWithV2(HyperDB, Hyperschema, paths) {
const schema = Hyperschema.from(paths.schema)
const example = schema.namespace('example')

example.require(paths.helpers)

registerThingV1(example)

example.register({
name: 'thing-v2',
fields: [
{
name: 'id',
type: 'string',
required: true
},
{
name: 'name',
type: 'string',
required: true
}
]
})

example.register({
name: 'thing',
versions: [
{
version: 1,
type: '@example/thing-v1',
map: 'thingV1ToV2'
},
{
version: 2,
type: '@example/thing-v2'
}
]
})

Hyperschema.toDisk(schema)

const db = HyperDB.from(paths.schema, paths.db)
const exampleDB = db.namespace('example')

exampleDB.collections.register({
name: 'things',
schema: '@example/thing',
key: ['id']
})

HyperDB.toDisk(db)
}

function createNestedVersionedDB(HyperDB, Hyperschema, paths) {
const schema = Hyperschema.from(paths.schema)
const example = schema.namespace('example')

registerThingV1(example)

example.register({
name: 'thing',
versions: [
{
version: 1,
type: '@example/thing-v1'
}
]
})

example.register({
name: 'wrapper',
fields: [
{
name: 'thing',
type: '@example/thing',
required: true
}
]
})

Hyperschema.toDisk(schema)

const db = HyperDB.from(paths.schema, paths.db)
const exampleDB = db.namespace('example')

exampleDB.collections.register({
name: 'wrappers',
schema: '@example/wrapper',
key: ['thing.id']
})

HyperDB.toDisk(db)
}