Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Query link edges #398

Merged
merged 3 commits into from
Jan 20, 2025
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
2 changes: 1 addition & 1 deletion apps/web/src/layout/Header.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import {cms} from '@/cms'
import styler, {Styler} from '@alinea/styler'
import {AnyLink} from 'alinea'
import {Link as AnyLink} from 'alinea'
import {Entry} from 'alinea/core/Entry'
import {HStack, Stack} from 'alinea/ui'
import {IcRoundClose} from 'alinea/ui/icons/IcRoundClose'
Expand Down
6 changes: 3 additions & 3 deletions apps/web/src/page/BlogPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {Link} from '@/layout/nav/Link'
import {BlogOverview} from '@/schema/BlogOverview'
import {BlogPost} from '@/schema/BlogPost'
import styler from '@alinea/styler'
import {Query} from 'alinea'
import {Entry} from 'alinea/core/Entry'
import {VStack} from 'alinea/ui'
import {MetadataRoute} from 'next'
Expand All @@ -18,8 +19,7 @@ export default async function BlogPage() {
type: BlogOverview,
select: {
title: BlogOverview.title,
posts: {
children: {},
posts: Query.children({
type: BlogPost,
select: {
...Entry,
Expand All @@ -28,7 +28,7 @@ export default async function BlogPage() {
author: BlogPost.author,
publishDate: BlogPost.publishDate
}
}
})
}
})
return (
Expand Down
3 changes: 1 addition & 2 deletions src/backend/Database.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -164,7 +164,7 @@ test('fetch translations', async () => {
location: example.workspaces.main.multiLanguage,
select: {
translations: {
translations: {},
edge: 'translations',
type: Page,
select: Entry.locale
}
Expand Down Expand Up @@ -395,7 +395,6 @@ test('remove field contents', async () => {

test('take/skip', async () => {
const example = createExample()
const {Page} = example.schema
const lastTwo = await example.find({
root: example.workspaces.main.pages,
skip: 1,
Expand Down
3 changes: 2 additions & 1 deletion src/backend/data/ChangeSet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -157,8 +157,9 @@ export class ChangeSetCreator {
select: {
workspace: Entry.workspace,
files: {
edge: 'children',
depth: 999,
type: MediaFile,
children: {depth: 999},
select: {location: MediaFile.location}
}
},
Expand Down
97 changes: 58 additions & 39 deletions src/backend/resolver/EntryResolver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,12 @@ import {Expr} from 'alinea/core/Expr'
import {Field} from 'alinea/core/Field'
import {Filter} from 'alinea/core/Filter'
import {
EdgeQuery,
GraphQuery,
Order,
Projection,
QuerySettings,
querySource,
RelatedQuery,
Status
} from 'alinea/core/Graph'
import {
Expand Down Expand Up @@ -58,7 +58,7 @@ import {Builder} from 'rado/core/Builder'
import {Functions} from 'rado/core/expr/Functions'
import {input} from 'rado/core/expr/Input'
import {jsonExpr} from 'rado/core/expr/Json'
import {getData, getTable, HasSql} from 'rado/core/Internal'
import {getData, getTable, HasSql, internalTarget} from 'rado/core/Internal'
import {bm25, snippet} from 'rado/sqlite'
import type {Database} from '../Database.js'
import {Store} from '../Store.js'
Expand Down Expand Up @@ -108,15 +108,19 @@ export class EntryResolver {
}
}

field(table: typeof EntryRow, field: Expr): HasSql<any> {
const name = this.scope.nameOf(field)
if (!name) throw new Error(`Expression has no name ${field}`)
const isEntryField = name === 'path' || name === 'type'
if (isEntryField) return table[name]
return (<any>table.data)[name]
}

expr(ctx: ResolveContext, expr: Expr): HasSql<any> {
const internal = getExpr(expr)
switch (internal.type) {
case 'field':
const name = this.scope.nameOf(expr)
if (!name) throw new Error(`Expression has no name ${expr}`)
const isEntryField = name === 'path' || name === 'type'
if (isEntryField) return ctx.Table[name]
return (<any>ctx.Table.data)[name]
return this.field(ctx.Table, expr)
case 'entryField':
return ctx.Table[internal.name as keyof EntryRow]
case 'call':
Expand Down Expand Up @@ -161,7 +165,7 @@ export class EntryResolver {
return [key, this.selectProjection(ctx, value as Projection)]
})
)
const related = value as RelatedQuery<Projection>
const related = value as object as EdgeQuery<Projection>
const isSingle = this.isSingleResult(related)
const query = this.query(ctx, related)
return isSingle ? include.one(query) : include(query)
Expand All @@ -176,7 +180,7 @@ export class EntryResolver {
return this.selectProjection(ctx, fromEntries(entries(fields)))
}

querySource(ctx: ResolveContext, query: RelatedQuery): Select<any> {
querySource(ctx: ResolveContext, query: EdgeQuery): Select<any> {
const hasSearch = Boolean(query.search?.length)
const {aliased} = getTable(ctx.Table)
const cursor = hasSearch
Expand All @@ -189,7 +193,7 @@ export class EntryResolver {
)
: builder.select().from(ctx.Table)
const from = alias(EntryRow, `E${ctx.depth - 1}`) // .as(source.id)
switch (querySource(query)) {
switch (query.edge) {
case 'parent':
return cursor.where(eq(ctx.Table.id, from.parentId)).limit(1)
case 'next':
Expand All @@ -209,14 +213,12 @@ export class EntryResolver {
case 'siblings':
return cursor.where(
eq(ctx.Table.parentId, from.parentId),
query.siblings?.includeSelf ? undefined : ne(ctx.Table.id, from.id)
query?.includeSelf ? undefined : ne(ctx.Table.id, from.id)
)
case 'translations':
return cursor.where(
eq(ctx.Table.id, from.id),
query.translations?.includeSelf
? undefined
: ne(ctx.Table.locale, from.locale)
query?.includeSelf ? undefined : ne(ctx.Table.locale, from.locale)
)
case 'children':
const Child = alias(EntryRow, 'Child')
Expand Down Expand Up @@ -245,10 +247,7 @@ export class EntryResolver {
.where(
is(Child.locale, from.locale),
this.conditionStatus(Child, ctx.status),
lt(
self.level,
Math.min(query.children?.depth ?? 1, MAX_DEPTH)
)
lt(self.level, Math.min(query?.depth ?? 1, MAX_DEPTH))
)
)
)
Expand Down Expand Up @@ -291,10 +290,7 @@ export class EntryResolver {
.where(
is(Parent.locale, from.locale),
this.conditionStatus(Parent, ctx.status),
lt(
self.level,
Math.min(query.parents?.depth ?? MAX_DEPTH, MAX_DEPTH)
)
lt(self.level, Math.min(query?.depth ?? MAX_DEPTH, MAX_DEPTH))
)
)
)
Expand All @@ -310,6 +306,19 @@ export class EntryResolver {
is(ctx.Table.locale, from.locale)
)
.orderBy(asc(ctx.Table.level))
case 'entryMultiple': {
const linkedField = this.field(from, query.field)
return cursor
.innerJoin(
{[internalTarget]: sql`json_each(${linkedField}) as lF`} as any,
eq(ctx.Table.id, sql`lF.value->>'_entry'`)
)
.orderBy(asc(sql`lF.id`))
}
case 'entrySingle': {
const linkedField = this.field(from, query.field)
return cursor.where(eq(ctx.Table.id, sql`${linkedField}->>'_entry'`))
}
default:
return cursor.orderBy(asc(ctx.Table.index))
}
Expand Down Expand Up @@ -388,7 +397,7 @@ export class EntryResolver {
query.root && typeof query.root === 'object' && hasRoot(query.root)
? this.scope.nameOf(query.root)
: query.root
return this.conditionFilter(ctx, this.getField.bind(this), {
return this.conditionFilter(ctx, this.filterField.bind(this), {
_id: query.id,
_parentId: query.parentId,
_path: query.path,
Expand Down Expand Up @@ -509,7 +518,7 @@ export class EntryResolver {
return and(...conditions)
}

getField(ctx: ResolveContext, name: string) {
filterField(ctx: ResolveContext, name: string) {
if (name.startsWith('_')) {
const entryProp = name.slice(1)
const key = entryProp as keyof EntryRow
Expand All @@ -519,10 +528,10 @@ export class EntryResolver {
return (<any>ctx.Table.data)[name]
}

query(ctx: ResolveContext, query: RelatedQuery<Projection>): Select<any> {
query(ctx: ResolveContext, query: GraphQuery<Projection>): Select<any> {
const {type, filter, skip, take, orderBy, groupBy, first, search} = query
ctx = ctx.increaseDepth().none
let q = this.querySource(ctx, query)
let q = this.querySource(ctx, query as EdgeQuery<Projection>)
if (skip) q = q.offset(skip)
if (take) q = q.limit(take)
const queryData = getData(q)
Expand All @@ -537,7 +546,7 @@ export class EntryResolver {
? undefined
: this.conditionLocale(ctx.Table, ctx.locale),
this.conditionSearch(ctx.Table, search),
filter && this.conditionFilter(ctx, this.getField.bind(this), filter)
filter && this.conditionFilter(ctx, this.filterField.bind(this), filter)
)
const toSelect = this.select(ctx.select, query)
let result = new Select({
Expand All @@ -559,9 +568,14 @@ export class EntryResolver {
return result
}

isSingleResult(query: RelatedQuery): boolean {
isSingleResult(query: EdgeQuery): boolean {
return Boolean(
query.first || query.get || query.parent || query.next || query.previous
query.first ||
query.get ||
query.count ||
query.edge === 'parent' ||
query.edge === 'next' ||
query.edge === 'previous'
)
}

Expand All @@ -586,26 +600,31 @@ export class EntryResolver {
ctx: PostContext,
interim: Interim,
query: GraphQuery<Projection>
) {
): Promise<void> {
if (!interim) return
const selected = this.projection(query)
if (selected && hasExpr(selected))
return this.postExpr(ctx, interim, selected)
if (hasExpr(selected)) return this.postExpr(ctx, interim, selected)
if (querySource(selected))
return this.post(ctx, interim, selected as EdgeQuery<Projection>)
await Promise.all(
entries(selected).map(([key, value]) => {
const source = querySource(value)
if (source)
return this.post(ctx, interim[key], value as RelatedQuery<Projection>)
return this.post(ctx, interim[key], value as EdgeQuery<Projection>)
return this.postExpr(ctx, interim[key], value as Expr)
})
)
}

post(ctx: PostContext, interim: Interim, input: RelatedQuery<Projection>) {
async post(
ctx: PostContext,
interim: Interim,
input: EdgeQuery<Projection>
): Promise<void> {
if (input.count === true) return
const isSingle = this.isSingleResult(input)
if (isSingle) return this.postRow(ctx, interim, input)
return Promise.all(interim.map((row: any) => this.postRow(ctx, row, input)))
await Promise.all(interim.map((row: any) => this.postRow(ctx, row, input)))
}

resolve = async <T>(query: GraphQuery): Promise<T> => {
Expand All @@ -616,14 +635,14 @@ export class EntryResolver {
...query,
location
})
const dbQuery = this.query(ctx, query as GraphQuery<Projection>)
const singleResult = this.isSingleResult(query)
const asEdge = query as EdgeQuery<Projection>
const dbQuery = this.query(ctx, asEdge)
const singleResult = this.isSingleResult(asEdge)
const transact = async (tx: Store): Promise<T> => {
const rows = await dbQuery.all(tx)
const linkResolver = new LinkResolver(this, tx, ctx)
const result = singleResult ? rows[0] ?? null : rows
if (result)
await this.post({linkResolver}, result, query as GraphQuery<Projection>)
if (result) await this.post({linkResolver}, result, asEdge)
return result as T
}
if (query.preview) {
Expand Down
Loading
Loading