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
63 changes: 63 additions & 0 deletions src/adapters/normalized-adapter.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import assert from 'node:assert/strict'
import test from 'node:test'

import type { Adapter } from 'lowdb'

import { DEFAULT_SCHEMA_PATH, NormalizedAdapter } from './adapters/normalized-adapter.ts'
import type { RawData } from './adapters/normalized-adapter.ts'
import type { Data } from './service.ts'

class StubAdapter implements Adapter<RawData> {
#data: RawData | null

constructor(data: RawData | null) {
this.#data = data
}

async read(): Promise<RawData | null> {
return this.#data === null ? null : structuredClone(this.#data)
}

async write(data: RawData): Promise<void> {
this.#data = structuredClone(data)
}

get data(): RawData | null {
return this.#data
}
}

await test('read removes $schema and normalizes ids', async () => {
const adapter = new StubAdapter({
$schema: './custom/schema.json',
posts: [{ id: 1 }, { title: 'missing id' }],
profile: { name: 'x' },
})

const normalized = await new NormalizedAdapter(adapter).read()
assert.notEqual(normalized, null)

if (normalized === null) {
return
}

assert.equal(normalized['$schema'], undefined)
assert.deepEqual(normalized['profile'], { name: 'x' })

const posts = normalized['posts']
assert.ok(Array.isArray(posts))
assert.equal(posts[0]?.['id'], '1')
assert.equal(typeof posts[1]?.['id'], 'string')
assert.notEqual(posts[1]?.['id'], '')
})

await test('write always overwrites $schema', async () => {
const adapter = new StubAdapter(null)
const normalizedAdapter = new NormalizedAdapter(adapter)

await normalizedAdapter.write({ posts: [{ id: '1' }] } satisfies Data)

const data = adapter.data
assert.notEqual(data, null)
assert.equal(data?.['$schema'], DEFAULT_SCHEMA_PATH)
})
47 changes: 47 additions & 0 deletions src/adapters/normalized-adapter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import type { Adapter } from 'lowdb'

import { randomId } from '../random-id.ts'
import type { Data, Item } from '../service.ts'

export const DEFAULT_SCHEMA_PATH = './node_modules/json-server/schema.json'
export type RawData = Record<string, Item[] | Item | string | undefined> & {
$schema?: string
}

export class NormalizedAdapter implements Adapter<Data> {
#adapter: Adapter<RawData>

constructor(adapter: Adapter<RawData>) {
this.#adapter = adapter
}

async read(): Promise<Data | null> {
const data = await this.#adapter.read()

if (data === null) {
return null
}

delete data['$schema']

for (const value of Object.values(data)) {
if (Array.isArray(value)) {
for (const item of value) {
if (typeof item['id'] === 'number') {
item['id'] = item['id'].toString()
}

if (item['id'] === undefined) {
item['id'] = randomId()
}
}
}
}

return data as Data
}

async write(data: Data): Promise<void> {
await this.#adapter.write({ ...data, $schema: DEFAULT_SCHEMA_PATH })
}
}
File renamed without changes.
12 changes: 7 additions & 5 deletions src/bin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,10 @@ import { DataFile, JSONFile } from "lowdb/node";
import type { PackageJson } from "type-fest";

import { fileURLToPath } from "node:url";
import { NormalizedAdapter } from "./adapters/normalized-adapter.ts";
import type { RawData } from "./adapters/normalized-adapter.ts";
import { Observer } from "./adapters/observer.ts";
import { createApp } from "./app.ts";
import { Observer } from "./observer.ts";
import type { Data } from "./service.ts";

function help() {
Expand Down Expand Up @@ -123,16 +125,16 @@ if (readFileSync(file, "utf-8").trim() === "") {
}

// Set up database
let adapter: Adapter<Data>;
let adapter: Adapter<RawData>;
if (extname(file) === ".json5") {
adapter = new DataFile<Data>(file, {
adapter = new DataFile<RawData>(file, {
parse: JSON5.parse,
stringify: JSON5.stringify,
});
} else {
adapter = new JSONFile<Data>(file);
adapter = new JSONFile<RawData>(file);
}
const observer = new Observer(adapter);
const observer = new Observer(new NormalizedAdapter(adapter));

const db = new Low<Data>(observer, {});
await db.read();
Expand Down
5 changes: 5 additions & 0 deletions src/random-id.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { randomBytes } from 'node:crypto'

export function randomId(): string {
return randomBytes(2).toString('hex')
}
18 changes: 0 additions & 18 deletions src/service.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,24 +56,6 @@ beforeEach(() => {
})
})

await test('constructor', () => {
const defaultData = { posts: [{ id: '1' }, {}], object: {} } satisfies Data
const db = new Low<Data>(adapter, defaultData)
new Service(db)
if (Array.isArray(db.data['posts'])) {
const id0 = db.data['posts'][0]['id']
const id1 = db.data['posts'][1]['id']
assert.ok(
typeof id0 === 'string' && id0 === '1',
`id should not change if already set but was: ${id0}`,
)
assert.ok(
typeof id1 === 'string' && id1.length > 0,
`id should be a non empty string but was: ${id1}`,
)
}
})

await test('findById', () => {
const cases: [[string, string, { _embed?: string[] | string }], unknown][] = [
[[POSTS, '1', {}], db.data?.[POSTS]?.[0]],
Expand Down
39 changes: 1 addition & 38 deletions src/service.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
import { randomBytes } from 'node:crypto'

import inflection from 'inflection'
import { Low } from 'lowdb'
import sortOn from 'sort-on'
import type { JsonObject } from 'type-fest'

import { matchesWhere } from './matches-where.ts'
import { paginate, type PaginationResult } from './paginate.ts'
import { randomId } from './random-id.ts'
export type Item = Record<string, unknown>

export type Data = Record<string, Item[] | Item>
Expand All @@ -15,17 +14,6 @@ export function isItem(obj: unknown): obj is Item {
return typeof obj === 'object' && obj !== null && !Array.isArray(obj)
}

export function isData(obj: unknown): obj is Data {
if (typeof obj !== 'object' || obj === null) {
return false
}

const data = obj as Record<string, unknown>
return Object.values(data).every((value) =>
Array.isArray(value) ? value.every(isItem) : isItem(value),
)
}

export type PaginatedItems = PaginationResult<Item>

function ensureArray(arg: string | string[] = []): string[] {
Expand Down Expand Up @@ -90,35 +78,10 @@ function deleteDependents(db: Low<Data>, name: string, dependents: string[]) {
})
}

function randomId(): string {
return randomBytes(2).toString('hex')
}

function fixItemsIds(items: Item[]) {
items.forEach((item) => {
if (typeof item['id'] === 'number') {
item['id'] = item['id'].toString()
}
if (item['id'] === undefined) {
item['id'] = randomId()
}
})
}

// Ensure all items have an id
function fixAllItemsIds(data: Data) {
Object.values(data).forEach((value) => {
if (Array.isArray(value)) {
fixItemsIds(value)
}
})
}

export class Service {
#db: Low<Data>

constructor(db: Low<Data>) {
fixAllItemsIds(db.data)
this.#db = db
}

Expand Down