UNPKG

alinea

Version:

[![npm](https://img.shields.io/npm/v/alinea.svg)](https://npmjs.org/package/alinea) [![install size](https://packagephobia.com/badge?p=alinea)](https://packagephobia.com/result?p=alinea)

513 lines (511 loc) 16.1 kB
import { count, iif, match, withRecursive } from "../../chunks/chunk-TY7XOXM3.js"; import { BinOpType, Expr, ExprData, OrderDirection, ParamData, Query, QueryData, UnOpType } from "../../chunks/chunk-4JLFL6LD.js"; import "../../chunks/chunk-U5RRZUYZ.js"; // src/backend/resolver/EntryResolver.ts import { Field, Schema, Type, unreachable } from "alinea/core"; import { EntryPhase, EntryRow } from "alinea/core/EntryRow"; import { EntrySearch } from "alinea/core/EntrySearch"; import { SourceType } from "alinea/core/pages/Cursor"; import { BinaryOp, UnaryOp } from "alinea/core/pages/ExprData"; import { Realm } from "alinea/core/pages/Realm"; import { entries, fromEntries, keys } from "alinea/core/util/Objects"; import { Database } from "../Database.js"; import { LinkResolver } from "./LinkResolver.js"; import { ResolveContext } from "./ResolveContext.js"; var unOps = { [UnaryOp.Not]: UnOpType.Not, [UnaryOp.IsNull]: UnOpType.IsNull }; var binOps = { [BinaryOp.Add]: BinOpType.Add, [BinaryOp.Subt]: BinOpType.Subt, [BinaryOp.Mult]: BinOpType.Mult, [BinaryOp.Mod]: BinOpType.Mod, [BinaryOp.Div]: BinOpType.Div, [BinaryOp.Greater]: BinOpType.Greater, [BinaryOp.GreaterOrEqual]: BinOpType.GreaterOrEqual, [BinaryOp.Less]: BinOpType.Less, [BinaryOp.LessOrEqual]: BinOpType.LessOrEqual, [BinaryOp.Equals]: BinOpType.Equals, [BinaryOp.NotEquals]: BinOpType.NotEquals, [BinaryOp.And]: BinOpType.And, [BinaryOp.Or]: BinOpType.Or, [BinaryOp.Like]: BinOpType.Like, [BinaryOp.In]: BinOpType.In, [BinaryOp.NotIn]: BinOpType.NotIn, [BinaryOp.Concat]: BinOpType.Concat }; var MAX_DEPTH = 999; var pageFields = keys(EntryRow); var EntryResolver = class { constructor(db, schema, parsePreview, defaults) { this.db = db; this.schema = schema; this.parsePreview = parsePreview; this.defaults = defaults; this.targets = Schema.targets(schema); } targets; fieldOf(ctx, target, field) { const { name } = target; if (!name) { const fields = ctx.Table; if (field in fields) return fields[field][Expr.Data]; throw new Error(`Selecting unknown field: "${field}"`); } const type = this.schema[name]; if (!type) throw new Error(`Selecting "${field}" from unknown type: "${name}"`); return ctx.Table.data.get(field)[Expr.Data]; } pageFields(ctx) { return pageFields.map((key) => [key, this.fieldOf(ctx, {}, key)]); } selectFieldsOf(ctx, target) { const { name } = target; if (!name) return this.pageFields(ctx); const type = this.schema[name]; if (!type) throw new Error(`Selecting from unknown type: "${name}"`); return keys(type).map((key) => { return [key, this.fieldOf(ctx, target, key)]; }); } exprUnOp(ctx, { op, expr }) { return new ExprData.UnOp(unOps[op], this.expr(ctx, expr)); } exprBinOp(ctx, { op, a, b }) { return new ExprData.BinOp(binOps[op], this.expr(ctx, a), this.expr(ctx, b)); } exprField(ctx, { target, field }) { return this.fieldOf(ctx, target, field); } exprAccess(ctx, { expr, field }) { return new ExprData.Field(this.expr(ctx.access, expr), field); } exprValue(ctx, { value }) { return new ExprData.Param(new ParamData.Value(value)); } exprRecord(ctx, { fields }) { return new ExprData.Record( fromEntries( entries(fields).map(([key, expr]) => { return [key, this.expr(ctx, expr)]; }) ) ); } exprCase(ctx, { expr, cases, defaultCase }) { const subject = new Expr(this.expr(ctx, expr)); let res = new Expr( defaultCase ? this.select(ctx, defaultCase) : Expr.NULL[Expr.Data] ); for (const [condition, value] of cases) res = iif( subject.is(new Expr(this.expr(ctx, condition))), new Expr(this.select(ctx, value)), res ); return res[Expr.Data]; } expr(ctx, expr) { switch (expr.type) { case "unop": return this.exprUnOp(ctx, expr); case "binop": return this.exprBinOp(ctx, expr); case "field": return this.exprField(ctx, expr); case "access": return this.exprAccess(ctx, expr); case "value": return this.exprValue(ctx, expr); case "record": return this.exprRecord(ctx, expr); case "case": return this.exprCase(ctx, expr); } } selectRecord(ctx, { fields }) { return new ExprData.Record( fromEntries( fields.flatMap((field) => { switch (field.length) { case 1: const [target] = field; return this.selectFieldsOf(ctx.select, target); case 2: const [key, selection] = field; return [[key, this.select(ctx, selection)]]; } }) ) ); } selectRow(ctx, { target }) { return new ExprData.Record( fromEntries(this.selectFieldsOf(ctx.select, target)) ); } selectCursor(ctx, selection) { return new ExprData.Query(this.queryCursor(ctx, selection)); } selectExpr(ctx, { expr, fromParent }) { ctx = fromParent ? ctx.decreaseDepth() : ctx; return this.expr(ctx.select, expr); } selectCount() { return count()[Expr.Data]; } selectAll(ctx, target) { const fields = this.selectFieldsOf(ctx.select, target); return new ExprData.Record(fromEntries(fields)); } select(ctx, selection) { switch (selection.type) { case "cursor": return this.selectCursor(ctx, selection); case "row": return this.selectRow(ctx, selection); case "record": return this.selectRecord(ctx, selection); case "expr": return this.selectExpr(ctx, selection); case "count": return this.selectCount(); } } queryRecord(ctx, selection) { const expr = this.selectRecord(ctx.select, selection); return new QueryData.Select({ selection: expr, singleResult: true }); } querySource(ctx, source, hasSearch) { const cursor = hasSearch ? EntrySearch().innerJoin( ctx.Table, ctx.Table().get("rowid").is(EntrySearch().get("rowid")) ).select(ctx.Table) : ctx.Table(); if (!source) return cursor.orderBy(ctx.Table.index.asc()); const from = EntryRow().as(`E${ctx.depth - 1}`); switch (source.type) { case SourceType.Parent: return cursor.where(ctx.Table.entryId.is(from.parent)).take(1); case SourceType.Next: return cursor.where(ctx.Table.parent.is(from.parent)).where(ctx.Table.index.isGreater(from.index)).take(1); case SourceType.Previous: return cursor.where(ctx.Table.parent.is(from.parent)).where(ctx.Table.index.isLess(from.index)).take(1); case SourceType.Siblings: return cursor.where(ctx.Table.parent.is(from.parent)).where(ctx.Table.entryId.isNot(from.entryId)); case SourceType.Translations: return cursor.where(ctx.Table.i18nId.is(from.i18nId)).where(ctx.Table.entryId.isNot(from.entryId)); case SourceType.Children: const Child = EntryRow().as("Child"); const children = withRecursive( Child({ entryId: from.entryId }).where( this.conditionRealm(Child, ctx.realm), this.conditionLocale(Child, ctx.locale) ).select({ entryId: Child.entryId, parent: Child.parent, level: 0 }) ).unionAll( () => Child().select({ entryId: Child.entryId, parent: Child.parent, level: children.level.add(1) }).innerJoin(children({ entryId: Child.parent })).where( this.conditionRealm(Child, ctx.realm), this.conditionLocale(Child, ctx.locale), children.level.isLess( Math.min(source.depth ?? MAX_DEPTH, MAX_DEPTH) ) ) ); const childrenIds = children().select(children.entryId).skip(1); return cursor.where(ctx.Table.entryId.isIn(childrenIds)).orderBy(ctx.Table.index.asc()); case SourceType.Parents: const Parent = EntryRow().as("Parent"); const parents = withRecursive( Parent({ entryId: from.entryId }).where( this.conditionRealm(Parent, ctx.realm), this.conditionLocale(Parent, ctx.locale) ).select({ entryId: Parent.entryId, parent: Parent.parent, level: 0 }) ).unionAll( () => Parent().select({ entryId: Parent.entryId, parent: Parent.parent, level: parents.level.add(1) }).innerJoin(parents({ parent: Parent.entryId })).where( this.conditionRealm(Parent, ctx.realm), this.conditionLocale(Parent, ctx.locale), parents.level.isLess( Math.min(source.depth ?? MAX_DEPTH, MAX_DEPTH) ) ) ); const parentIds = parents().select(parents.entryId).skip(1); return cursor.where(ctx.Table.entryId.isIn(parentIds)).orderBy(ctx.Table.level.asc()); default: throw unreachable(source.type); } } orderBy(ctx, orderBy) { return orderBy.map(({ expr, order }) => { const exprData = this.expr(ctx, expr); return { expr: exprData, order: order === "Desc" ? OrderDirection.Desc : OrderDirection.Asc }; }); } conditionLocale(Table2, locale) { if (!locale) return Expr.value(true); return Table2.locale.is(locale); } conditionRealm(Table2, realm) { switch (realm) { case Realm.Published: return Table2.phase.is(EntryPhase.Published); case Realm.Draft: return Table2.phase.is(EntryPhase.Draft); case Realm.Archived: return Table2.phase.is(EntryPhase.Archived); case Realm.PreferDraft: return Table2.active; case Realm.PreferPublished: return Table2.main; case Realm.All: return Expr.value(true); } } conditionLocation(Table2, location) { switch (location.length) { case 1: return Table2.workspace.is(location[0]); case 2: return Table2.workspace.is(location[0]).and(Table2.root.is(location[1])); case 3: const condition = Table2.workspace.is(location[0]).and(Table2.root.is(location[1])).and(Table2.parentDir.like(`/${location[2]}%`)); return condition; default: return Expr.value(true); } } conditionSearch(Table2, searchTerms) { if (!searchTerms?.length) return Expr.value(true); const terms = searchTerms.map((term) => `"${term.replaceAll('"', "")}"*`).join(" + "); return match(EntrySearch, terms); } queryCursor(ctx, { cursor }) { const { target, where, skip, take, orderBy, groupBy, select, first, source, searchTerms } = cursor; ctx = ctx.increaseDepth().none; const { name } = target || {}; const hasSearch = Boolean(searchTerms?.length); let query = this.querySource(ctx, source, hasSearch); let preCondition = query[Query.Data].where; let condition = Expr.and( preCondition ? new Expr(preCondition) : Expr.value(true), name ? ctx.Table.type.is(name) : Expr.value(true), this.conditionLocation(ctx.Table, ctx.location), this.conditionRealm(ctx.Table, ctx.realm), this.conditionLocale(ctx.Table, ctx.locale), this.conditionSearch(ctx.Table, searchTerms) ); if (skip) query = query.skip(skip); if (take) query = query.take(take); const extra = {}; extra.where = (where ? condition.and(new Expr(this.expr(ctx.condition, where))) : condition)[Expr.Data]; extra.selection = select ? this.select(ctx.select, select) : this.selectAll(ctx, { name }); if (first) extra.singleResult = true; if (groupBy) extra.groupBy = groupBy.map((expr) => this.expr(ctx, expr)); if (orderBy) extra.orderBy = this.orderBy(ctx, orderBy); return query[Query.Data].with(extra); } query(ctx, selection) { switch (selection.type) { case "cursor": return this.queryCursor(ctx, selection); case "record": return this.queryRecord(ctx, selection); case "row": case "count": case "expr": throw new Error(`Cannot select ${selection.type} at root level`); } } async postRow(ctx, interim, { target }) { await this.postFieldsOf(ctx, interim, target); } async postCursor(ctx, interim, { cursor }) { const { target = {}, select, first } = cursor; if (select) { if (first) await this.post(ctx, interim, select); else await Promise.all( interim.map((row) => this.post(ctx, row, select)) ); } else { if (first) await this.postFieldsOf(ctx, interim, target); else await Promise.all( interim.map((row) => this.postFieldsOf(ctx, row, target)) ); } } async postField(ctx, interim, { target, field }) { const { name } = target; if (!name) return; const type = this.schema[name]; if (!type) return; const shape = Field.shape(Type.field(type, field)); await shape.applyLinks(interim, ctx.linkResolver); } async postExpr(ctx, interim, { expr }) { if (expr.type === "field") await this.postField(ctx, interim, expr); } async postFieldsOf(ctx, interim, target) { if (!interim) return; const { name } = target; if (!name) return; const type = this.schema[name]; if (!type) return; await Promise.all( keys(type).map((field) => { return this.postField(ctx, interim[field], { type: "field", target, field }); }) ); } async postRecord(ctx, interim, { fields }) { if (!interim) return; const tasks = []; for (const field of fields) { switch (field.length) { case 1: const [target] = field; tasks.push(this.postFieldsOf(ctx, interim, target)); continue; case 2: const [key, selection] = field; tasks.push(this.post(ctx, interim[key], selection)); continue; } } await Promise.all(tasks); } post(ctx, interim, selection) { switch (selection.type) { case "row": return this.postRow(ctx, interim, selection); case "cursor": return this.postCursor(ctx, interim, selection); case "record": return this.postRecord(ctx, interim, selection); case "expr": return this.postExpr(ctx, interim, selection); case "count": return Promise.resolve(); } } resolve = async ({ selection, location, locale, realm = this.defaults?.realm ?? Realm.Published, preview = this.defaults?.preview }) => { const ctx = new ResolveContext({ realm, location, locale }); const queryData = this.query(ctx, selection); const query = new Query(queryData); if (preview) { const updated = await this.parsePreview?.(preview); if (updated) try { await this.db.store.transaction(async (tx) => { const current = EntryRow({ entryId: preview.entryId, active: true }); await tx(current.delete()); await tx(EntryRow().insert(updated)); await Database.index(tx); const result2 = await tx(query); const linkResolver2 = new LinkResolver(this, tx, ctx.realm); if (result2) await this.post({ linkResolver: linkResolver2 }, result2, selection); throw { result: result2 }; }); } catch (err) { if (err.result) return err.result; } } const result = await this.db.store(query); const linkResolver = new LinkResolver(this, this.db.store, ctx.realm); if (result) await this.post({ linkResolver }, result, selection); return result; }; }; export { EntryResolver };