Skip to content

Commit

Permalink
WIP serialize database
Browse files Browse the repository at this point in the history
  • Loading branch information
kettanaito committed Apr 28, 2021
1 parent 2982ab9 commit 92468b1
Show file tree
Hide file tree
Showing 15 changed files with 686 additions and 303 deletions.
1 change: 1 addition & 0 deletions jest.config.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
export default {
preset: 'ts-jest',
testTimeout: 60000,
setupFilesAfterEnv: ['./jest.setup.ts'],
moduleNameMapper: {
'^@mswjs/data(.*)': '<rootDir>/$1',
},
Expand Down
22 changes: 22 additions & 0 deletions jest.setup.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import * as path from 'path'
import { CreateBrowserApi, createBrowser } from 'page-with'

let browser: CreateBrowserApi

beforeAll(async () => {
browser = await createBrowser({
serverOptions: {
webpackConfig: {
resolve: {
alias: {
'@mswjs/data': path.resolve(__dirname, '.'),
},
},
},
},
})
})

afterAll(async () => {
await browser.cleanup()
})
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@
"@types/node-fetch": "^2.5.10",
"debug": "^4.3.1",
"faker": "^5.5.3",
"jest": "^26.6.0",
"jest": "^26.6.3",
"msw": "^0.28.2",
"node-fetch": "^2.6.1",
"page-with": "^0.3.5",
Expand Down
81 changes: 80 additions & 1 deletion src/db/Database.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import md5 from 'md5'
import { StrictEventEmitter } from 'strict-event-emitter'
import {
Entity,
InternalEntity,
InternalEntityProperty,
ModelDictionary,
PrimaryKeyType,
RelationKind,
} from '../glossary'

type Models<Dictionary extends ModelDictionary> = Record<
Expand Down Expand Up @@ -69,9 +71,10 @@ export class Database<Dictionary extends ModelDictionary> {
customPrimaryKey ||
(entity[entity[InternalEntityProperty.primaryKey]] as string)

const createdEntity = this.getModel(modelName).set(primaryKey, entity)
this.events.emit('create', this.id, modelName, entity, customPrimaryKey)

return this.getModel(modelName).set(primaryKey, entity)
return createdEntity
}

update<ModelName extends string>(
Expand Down Expand Up @@ -116,4 +119,80 @@ export class Database<Dictionary extends ModelDictionary> {
): InternalEntity<Dictionary, ModelName>[] {
return Array.from(this.getModel(modelName).values())
}

/**
* Returns a JSON representation of the current database entities.
*/
toJson(): Record<string, any> {
console.log('input:', this.models)

return Object.entries(this.models).reduce<Record<string, any>>(
(json, [modelName, entities]) => {
const modelJson: [PrimaryKeyType, Entity<any, any>][] = []

for (const [primaryKey, entity] of entities.entries()) {
const descriptors = Object.getOwnPropertyDescriptors(entity)
const jsonEntity: Entity<any, any> = {} as any

for (const propertyName in descriptors) {
const node = descriptors[propertyName]
const isRelationalProperty =
!node.hasOwnProperty('value') && node.hasOwnProperty('get')

console.log('analyzing "%s.%s"', modelName, propertyName)

if (isRelationalProperty) {
console.log(
'found a relational property "%s" on "%s"',
propertyName,
modelName,
)

/**
* @todo Handle `manyOf` relation: this variable will be a list
* of relations in that case.
*/
const resolvedRelationNode = node.get?.()

console.log('value', node)
console.log('resolved relation:', node.get?.())

jsonEntity[propertyName] = {
kind: RelationKind.OneOf,
modelName: resolvedRelationNode.__type,
unique: false,
refs: [
{
__type: resolvedRelationNode.__type,
__primaryKey: resolvedRelationNode.__primaryKey,
__nodeId:
resolvedRelationNode[resolvedRelationNode.__primaryKey],
},
],
}
} else {
console.log('property "%s" is not relational', propertyName)
jsonEntity[propertyName] = node.value
}
}

console.log({ jsonEntity })

/**
* @todo How to persist relational properties?
* Need to write down pointers, kinda like they work internally.
*/
modelJson.push([primaryKey, jsonEntity])
}

json[modelName] = modelJson
return json
},
{},
)
}

hydrate(state: Dictionary) {
// this.models = {}
}
}
39 changes: 39 additions & 0 deletions src/extensions/persist.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { Database } from '../db/Database'
import { isBrowser, supports } from '../utils/env'

const STORAGE_KEY_PREFIX = 'mswjs-data'

/**
* Persists database state into `sessionStorage` and
* hydrates from it on the initial page load.
*/
export function persist(db: Database<any>) {
if (!isBrowser() || !supports.sessionStorage()) {
return
}

const key = `${STORAGE_KEY_PREFIX}/${db.id}`

function persistState() {
const json = db.toJson()
console.log('persists state to storage...', json)
sessionStorage.setItem(key, JSON.stringify(json))
}

db.events.addListener('create', persistState)
db.events.addListener('update', persistState)
db.events.addListener('delete', persistState)

function hydrateState() {
const initialState = sessionStorage.getItem(key)

if (!initialState) {
return
}

console.log('should hydrate from "%s"', key, initialState)
db.hydrate(JSON.parse(initialState))
}

window.addEventListener('load', hydrateState)
}
2 changes: 1 addition & 1 deletion src/extensions/sync.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { isBrowser, supports } from 'src/utils/env'
import { Database, DatabaseEventsMap } from '../db/Database'
import { isBrowser, supports } from '../utils/env'

interface DatabaseMessageEventData<
OperationType extends keyof DatabaseEventsMap
Expand Down
2 changes: 2 additions & 0 deletions src/factory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import { generateRestHandlers } from './model/generateRestHandlers'
import { generateGraphQLHandlers } from './model/generateGraphQLHandlers'
import { sync } from './extensions/sync'
import { removeInternalProperties } from './utils/removeInternalProperties'
import { persist } from './extensions/persist'

/**
* Create a database with the given models.
Expand Down Expand Up @@ -45,6 +46,7 @@ function createModelApi<
const { primaryKey } = parsedModel

sync(db)
persist(db)

if (typeof primaryKey === 'undefined') {
throw new OperationError(
Expand Down
3 changes: 3 additions & 0 deletions src/utils/env.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@ export function isBrowser() {
}

export const supports = {
sessionStorage() {
return typeof sessionStorage !== 'undefined'
},
broadcastChannel() {
return typeof BroadcastChannel !== 'undefined'
},
Expand Down
153 changes: 153 additions & 0 deletions test/db/toJson.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
import { ModelDictionary } from 'lib/glossary'
import { parseModelDefinition } from 'src/model/parseModelDefinition'
import { oneOf, manyOf, primaryKey } from '../../src'
import { Database } from '../../src/db/Database'
import { RelationKind } from '../../src/glossary'
import { createModel } from '../../src/model/createModel'

test('serialized database models into JSON', () => {
const dictionary: ModelDictionary = {
user: {
id: primaryKey(String),
firstName: 'John',
role: oneOf('role'),
posts: manyOf('post'),
},
role: {
id: primaryKey(String),
name: String,
},
post: {
id: primaryKey(String),
title: String,
},
}

const db = new Database({
user: dictionary.user,
role: dictionary.role,
post: dictionary.post,
})

const role = createModel(
'role',
dictionary.role,
parseModelDefinition(dictionary, 'role'),
{ id: 'role-1', name: 'Reader' },
db,
)
const posts = [
createModel('post', 'id', { id: 'post-1', title: 'First' }, {}, db),
createModel('post', 'id', { id: 'post-2', title: 'Second' }, {}, db),
]

db.create('role', role)
posts.forEach((post) => {
db.create('post', post)
})

db.create(
'user',
createModel(
'user',
'id',
{
id: 'abc-123',
firstName: 'John',
} as any,
{
role: {
kind: RelationKind.OneOf,
modelName: 'role',
unique: false,
refs: [
{
__type: 'role',
__primaryKey: 'id',
__nodeId: 'role-1',
},
],
},
posts: {
kind: RelationKind.ManyOf,
modelName: 'post',
unique: false,
refs: [
{
__type: 'post',
__primaryKey: 'id',
__nodeId: 'post-1',
},
{
__type: 'post',
__primaryKey: 'id',
__nodeId: 'post-2',
},
],
},
},
db,
),
)

const json = db.toJson()
console.log(JSON.stringify(json, null, 2))

expect(json).toEqual({
user: [
[
'abc-123',
{
__type: 'user',
__primaryKey: 'id',
id: 'abc-123',
firstName: 'John',
role: {
kind: RelationKind.OneOf,
modelName: 'role',
unique: false,
refs: [
{
__type: 'role',
__nodeId: 'role-1',
__primaryKey: 'id',
},
],
},
posts: [],
},
],
],
role: [
[
'role-1',
{
__type: 'role',
__primaryKey: 'id',
id: 'role-1',
name: 'Reader',
},
],
],
post: [
[
'post-1',
{
__type: 'post',
__primaryKey: 'id',
id: 'post-1',
title: 'First',
},
],
[
'post-2',
{
__type: 'post',
__primaryKey: 'id',
id: 'post-2',
title: 'Second',
},
],
],
})
})
24 changes: 24 additions & 0 deletions test/extensions/persist/persist.runtime.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { factory, primaryKey, oneOf } from '@mswjs/data'

const db = factory({
user: {
id: primaryKey(String),
firstName: String,
role: oneOf('userRole'),
},
role: {
id: primaryKey(String),
name: String,
},
})

db.user.create({
id: 'abc-123',
firstName: 'John',
role: db.role.create({
id: 1,
name: 'Reader',
}),
})

window.db = db
Loading

0 comments on commit 92468b1

Please sign in to comment.