alinea
Version:
[](https://npmjs.org/package/alinea) [](https://packagephobia.com/result?p=alinea)
513 lines (511 loc) • 16.1 kB
JavaScript
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
};