Files
json-server/src/service.ts
Copilot 037609cc1d feat: auto-insert $schema when missing from DB file on startup (#1717)
* Initial plan

* Add auto-fix for missing $schema in JSON/JSON5 DB files

Co-authored-by: typicode <5502029+typicode@users.noreply.github.com>

* Delete package-lock.json

* Normalize DB adapter reads/writes

* Move adapters under src/adapters and remove service auto-fixes

* update

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: typicode <5502029+typicode@users.noreply.github.com>
Co-authored-by: typicode <typicode@gmail.com>
2026-02-28 01:41:54 +01:00

223 lines
5.9 KiB
JavaScript

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>
export function isItem(obj: unknown): obj is Item {
return typeof obj === 'object' && obj !== null && !Array.isArray(obj)
}
export type PaginatedItems = PaginationResult<Item>
function ensureArray(arg: string | string[] = []): string[] {
return Array.isArray(arg) ? arg : [arg]
}
function embed(db: Low<Data>, name: string, item: Item, related: string): Item {
if (inflection.singularize(related) === related) {
const relatedData = db.data[inflection.pluralize(related)] as Item[]
if (!relatedData) {
return item
}
const foreignKey = `${related}Id`
const relatedItem = relatedData.find((relatedItem: Item) => {
return relatedItem['id'] === item[foreignKey]
})
return { ...item, [related]: relatedItem }
}
const relatedData: Item[] = db.data[related] as Item[]
if (!relatedData) {
return item
}
const foreignKey = `${inflection.singularize(name)}Id`
const relatedItems = relatedData.filter(
(relatedItem: Item) => relatedItem[foreignKey] === item['id'],
)
return { ...item, [related]: relatedItems }
}
function nullifyForeignKey(db: Low<Data>, name: string, id: string) {
const foreignKey = `${inflection.singularize(name)}Id`
Object.entries(db.data).forEach(([key, items]) => {
// Skip
if (key === name) return
// Nullify
if (Array.isArray(items)) {
items.forEach((item) => {
if (item[foreignKey] === id) {
item[foreignKey] = null
}
})
}
})
}
function deleteDependents(db: Low<Data>, name: string, dependents: string[]) {
const foreignKey = `${inflection.singularize(name)}Id`
Object.entries(db.data).forEach(([key, items]) => {
// Skip
if (key === name || !dependents.includes(key)) return
// Delete if foreign key is null
if (Array.isArray(items)) {
db.data[key] = items.filter((item) => item[foreignKey] !== null)
}
})
}
export class Service {
#db: Low<Data>
constructor(db: Low<Data>) {
this.#db = db
}
#get(name: string): Item[] | Item | undefined {
return this.#db.data[name]
}
has(name: string): boolean {
return Object.prototype.hasOwnProperty.call(this.#db?.data, name)
}
findById(name: string, id: string, query: { _embed?: string[] | string }): Item | undefined {
const value = this.#get(name)
if (Array.isArray(value)) {
let item = value.find((item) => item['id'] === id)
ensureArray(query._embed).forEach((related) => {
if (item !== undefined) item = embed(this.#db, name, item, related)
})
return item
}
return
}
find(
name: string,
opts: {
where: JsonObject
sort?: string
page?: number
perPage?: number
embed?: string | string[]
},
): Item[] | PaginatedItems | Item | undefined {
const items = this.#get(name)
if (!Array.isArray(items)) {
return items
}
let results = items
// Include
ensureArray(opts.embed).forEach((related) => {
results = results.map((item) => embed(this.#db, name, item, related))
})
results = results.filter((item) => matchesWhere(item as JsonObject, opts.where))
if (opts.sort) {
results = sortOn(results, opts.sort.split(','))
}
if (opts.page !== undefined) {
return paginate(results, opts.page, opts.perPage ?? 10)
}
return results
}
async create(name: string, data: Omit<Item, 'id'> = {}): Promise<Item | undefined> {
const items = this.#get(name)
if (items === undefined || !Array.isArray(items)) return
const item = { id: randomId(), ...data }
items.push(item)
await this.#db.write()
return item
}
async #updateOrPatch(name: string, body: Item = {}, isPatch: boolean): Promise<Item | undefined> {
const item = this.#get(name)
if (item === undefined || Array.isArray(item)) return
const nextItem = (this.#db.data[name] = isPatch ? { ...item, ...body } : body)
await this.#db.write()
return nextItem
}
async #updateOrPatchById(
name: string,
id: string,
body: Item = {},
isPatch: boolean,
): Promise<Item | undefined> {
const items = this.#get(name)
if (items === undefined || !Array.isArray(items)) return
const item = items.find((item) => item['id'] === id)
if (!item) return
const nextItem = isPatch ? { ...item, ...body, id } : { ...body, id }
const index = items.indexOf(item)
items.splice(index, 1, nextItem)
await this.#db.write()
return nextItem
}
async update(name: string, body: Item = {}): Promise<Item | undefined> {
return this.#updateOrPatch(name, body, false)
}
async patch(name: string, body: Item = {}): Promise<Item | undefined> {
return this.#updateOrPatch(name, body, true)
}
async updateById(name: string, id: string, body: Item = {}): Promise<Item | undefined> {
return this.#updateOrPatchById(name, id, body, false)
}
async patchById(name: string, id: string, body: Item = {}): Promise<Item | undefined> {
return this.#updateOrPatchById(name, id, body, true)
}
async destroyById(
name: string,
id: string,
dependent?: string | string[],
): Promise<Item | undefined> {
const items = this.#get(name)
if (items === undefined || !Array.isArray(items)) return
const item = items.find((item) => item['id'] === id)
if (item === undefined) return
const index = items.indexOf(item)
items.splice(index, 1)
nullifyForeignKey(this.#db, name, id)
const dependents = ensureArray(dependent)
deleteDependents(this.#db, name, dependents)
await this.#db.write()
return item
}
}