mirror of
https://github.com/typicode/json-server.git
synced 2026-03-13 09:35:37 +08:00
* 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>
223 lines
5.9 KiB
JavaScript
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
|
|
}
|
|
}
|