From 0c9788a1828fb72a466c45f78d3397a28a47d5f6 Mon Sep 17 00:00:00 2001 From: racct <elias@adornis.de> Date: Fri, 14 Feb 2025 10:08:18 +0000 Subject: [PATCH 1/2] feat: use single nodes for every (sub)operation --- lab/introspect/api/get-introspect-node.ts | 52 +++ .../client/x-introspect-viewer-node.ts | 289 +++++++++++++ lab/introspect/client/x-introspect-viewer.ts | 394 +++++------------- .../entities/introspect-node.entity.ts | 88 ++++ lab/introspect/introspect-node.ts | 83 ++-- lab/introspect/server/introspect.ts | 53 +-- lab/introspect/server/introspectInjector.ts | 146 ++----- .../server/introspection-options.ts | 62 +++ lab/introspect/server/track-operation.ts | 88 ++++ lab/introspect/startup.test.ts | 12 + 10 files changed, 789 insertions(+), 478 deletions(-) create mode 100644 lab/introspect/api/get-introspect-node.ts create mode 100644 lab/introspect/client/x-introspect-viewer-node.ts create mode 100644 lab/introspect/entities/introspect-node.entity.ts create mode 100644 lab/introspect/server/introspection-options.ts create mode 100644 lab/introspect/server/track-operation.ts create mode 100644 lab/introspect/startup.test.ts diff --git a/lab/introspect/api/get-introspect-node.ts b/lab/introspect/api/get-introspect-node.ts new file mode 100644 index 0000000000..7303e87754 --- /dev/null +++ b/lab/introspect/api/get-introspect-node.ts @@ -0,0 +1,52 @@ +import { ID } from '@adornis/baseql/baseqlTypes.js'; +import type { EntityData } from '@adornis/baseql/entities/types.js'; +import { registerQuery } from '@adornis/baseql/metadata/register.js'; +import { getRawCollection } from '@adornis/baseql/server/collections.js'; +import type { BaseQLSelectionSet } from '@adornis/baseql/utils/queryGeneration.js'; +import { IntrospectNode } from '../entities/introspect-node.entity.js'; +import { getIntrospectCollectionName } from '../server/introspection-options.js'; + +export const getIntrospectNodeByID = registerQuery({ + operationName: 'getIntrospectNodeByID', + type: () => IntrospectNode, + params: [{ name: 'nodeID', type: () => ID }], + resolve: (nodeID: string) => async (gqlFields: BaseQLSelectionSet<IntrospectNode>) => { + const collection = await getRawCollection<EntityData<IntrospectNode>>(getIntrospectCollectionName()); + const node = await collection.findOne({ _id: nodeID }); + return node as IntrospectNode | null; + }, +}); + +export const getLastIntrospectNodeByOperation = registerQuery({ + operationName: 'getLastIntrospectNodeByOperation', + type: () => IntrospectNode, + params: [{ name: 'operationName', type: () => String }], + resolve: (operationName: string) => async (gqlFields: BaseQLSelectionSet<IntrospectNode>) => { + const collection = await getRawCollection<EntityData<IntrospectNode>>(getIntrospectCollectionName()); + const node = await collection.find({ operation: operationName }).sort({ startedAt: -1 }).limit(1).next(); + return node as IntrospectNode | null; + }, +}); + +export const getLastIntrospectNodeByOperationAndParam = registerQuery({ + operationName: 'getLastIntrospectNodeByOperationAndParam', + type: () => IntrospectNode, + params: [ + { name: 'operationName', type: () => String }, + { name: 'paramsStringified', type: () => String }, + ], + resolve: + (operationName: string, paramsStringified: string) => async (gqlFields: BaseQLSelectionSet<IntrospectNode>) => { + const params = JSON.parse(paramsStringified); + const collection = await getRawCollection<EntityData<IntrospectNode>>(getIntrospectCollectionName()); + const node = await collection + .find({ + operation: operationName, + ...Object.fromEntries(Object.entries(params).map(([key, value]) => [`args.${key}`, value])), + }) + .sort({ startedAt: -1 }) + .limit(1) + .next(); + return node as IntrospectNode | null; + }, +}); diff --git a/lab/introspect/client/x-introspect-viewer-node.ts b/lab/introspect/client/x-introspect-viewer-node.ts new file mode 100644 index 0000000000..c87df27daa --- /dev/null +++ b/lab/introspect/client/x-introspect-viewer-node.ts @@ -0,0 +1,289 @@ +import type { Maybe } from '@adornis/base/utilTypes.js'; +import { AdornisEntity } from '@adornis/baseql/entities/adornisEntity.js'; +import { constructValue } from '@adornis/baseql/entities/construct.js'; +import { selectionSet } from '@adornis/baseql/utils/queryGeneration.js'; +import { acss } from '@adornis/chemistry/directives/acss.js'; +import { css } from '@adornis/chemistry/directives/css.js'; +import '@adornis/chemistry/elements/components/x-loader.js'; +import { DesignSystem } from '@adornis/chemistry/elements/theming/design.js'; +import { stateful, useObservable } from '@adornis/functional-lit/stateful.js'; +import { useQuery } from '@adornis/functional-lit/use-query.js'; +import { GeneratedForm } from '@adornis/generated-form/generated-form.js'; +import { useGeneratedForm } from '@adornis/generated-form/use-generic-form.js'; +import { html } from 'lit'; +import { repeat } from 'lit/directives/repeat.js'; +import { html as htmlStatic, unsafeStatic } from 'lit/static-html.js'; +import { of } from 'rxjs'; +import { getIntrospectNodeByID } from '../api/get-introspect-node.js'; +import { IntrospectNode } from '../entities/introspect-node.entity.js'; +import type { IIntrospectNode } from '../introspect-node.js'; + +const cellBorder = { + borderBottom: '1px solid rgba(0,0,0,0.1)', + borderRight: '1px solid rgba(0,0,0,0.1)', + borderLeft: '1px solid rgba(0,0,0,0.1)', +}; + +const isGeneratedForm = (data: any): 'array' | 'object' | 'values' | false => { + if (Array.isArray(data)) return 'array'; + if (data?._class) return 'object'; + if (data) { + const values = Object.values(data) as any[]; + for (let i = 0; i < values.length; i++) { + if (values[i]?._class) { + return 'values'; + } + } + } + return false; +}; + +const genBlacklist = (data: Maybe<AdornisEntity>) => { + if (!data) { + return []; + } + return Object.keys((data as any).docMeta) + .map(key => { + if (!data.has(key as any)) { + return key; + } + return null; + }) + .filter(Boolean) as string[]; +}; + +export const IntrospectViewerNodeID = stateful(({ nodeID }: { nodeID?: string }) => { + const introspectionNode = useQuery( + nodeID ? getIntrospectNodeByID(nodeID)(selectionSet(() => IntrospectNode, 1)) : of(null), + [nodeID], + ); + + if (!nodeID) + return html` + <x-empty-placeholder .img=${'close'} .text=${'Keine Instrospection node übergeben.'}></x-empty-placeholder> + `; + + return IntrospectViewerNode(introspectionNode); +}); + +export const IntrospectViewerNode = stateful( + (introspectionNode: { value: IntrospectNode | null; loading: boolean; error: Error | null }) => { + const design = useObservable(() => DesignSystem.currentTheme, []); + + if (introspectionNode.loading) + return html` + <x-flex + flex + center + crossaxis-center + padding="md" + ${css({ + background: 'white', + minWidth: '200px', + maxWidth: '350px', + borderRadius: '0.375rem', + border: '1px solid rgba(0,0,0,0.1)', + })} + > + <x-loader></x-loader> + </x-flex> + `; + if (introspectionNode.error) + return html` + <x-empty-placeholder .img=${'close'} .text=${introspectionNode.error.message}></x-empty-placeholder> + `; + if (!introspectionNode.value) + return html` + <x-empty-placeholder .img=${'close'} .text=${'Keine Instrospection node gefunden.'}></x-empty-placeholder> + `; + + const dbNode = introspectionNode.value; + const node: IIntrospectNode & { childIDs: string[] } = { + ...introspectionNode.value.toObject(), + meta: dbNode.meta ? JSON.parse(dbNode.meta) : null, + result: dbNode.result ? JSON.parse(dbNode.result) : null, + errors: dbNode.errors ? JSON.parse(dbNode.errors) : null, + args: dbNode.args ? JSON.parse(dbNode.args) : null, + } as unknown as IIntrospectNode & { childIDs: string[] }; + + const nodeDataContent = (data: any, path: string[] = []) => { + const genForm = isGeneratedForm(data); + if (genForm) { + switch (genForm) { + case 'array': + return repeat(data, (res: any, i) => nodeDataContent(res, [...path, i.toString()])); + case 'object': + return stateful(() => { + const entity = fillEmptyFields(constructValue(data)); + const { form } = useGeneratedForm(entity, { + disabledFields: entity ? selectionSet(() => entity.constructor as typeof AdornisEntity, 1) : {}, + }); + return html` + <x-text tone="subtle"> ${data._class} </x-text> + ${GeneratedForm({ form, blacklist: genBlacklist(form.document) })} + `; + })(); + case 'values': + return repeat(Object.entries(data), ([key, val]) => { + return nodeDataContent(val, [...path, key]); + }); + } + } + + return html` + <x-flex space="sm"> + <x-text tone="subtle"> ${path.join(' > ')} </x-text> + <x-text ${css({ fontFamily: 'monospace', fontSize: '0.8rem', whiteSpace: 'pre-wrap' })} + >${JSON.stringify(data, null, 2)}</x-text + > + </x-flex> + `; + }; + + const customComp = node.meta['component']; + + return html` + <x-flex + ${css({ + display: 'inline-flex', + background: 'white', + minWidth: '250px', + maxWidth: '450px', + flexShrink: '0', + borderRadius: design?.sizes.borderRadiusSecondary, + })} + > + <x-flex + padding="sm" + space-between + horizontal + crossaxis-center + ${css({ + background: design?.colors.accent, + color: 'white', + borderRadius: `${design?.sizes.borderRadiusSecondary} ${design?.sizes.borderRadiusSecondary} 0 0`, + })} + ${acss({ + cursor: 'pointer', + '&:hover': { + opacity: 0.5, + }, + })} + @click=${() => { + // this._closed[path.join('.')] = !this._closed[path.join('.')]; + // if (this._closed[path.join('.')]) { + // // close of child nodes + // this._closeAllChildNodes(node, path); + // } + // this.requestUpdate(); + }} + > + <x-text>${node.operation}</x-text> + <x-icon> ${closed ? 'unfold_more' : 'unfold_less'} </x-icon> + </x-flex> + ${closed + ? html` + <x-flex ${css({ border: '1px solid rgba(0,0,0,0.1)', flexGrow: 1 })} padding="sm"> + <x-text ${css({ opacity: 0.5 })}>Expand to see more...</x-text> + </x-flex> + ` + : html` + <x-flex + flex + ${css({ + background: 'white', + minWidth: '200px', + maxWidth: '350px', + borderRadius: '0.375rem', + borderTopRightRadius: '0', + borderTopLeftRadius: '0', + border: '1px solid rgba(0,0,0,0.1)', + })} + > + <x-flex + padding="sm" + space-between + horizontal + crossaxis-center + ${css({ + background: 'var(--color-accent)', + color: 'white', + borderRadius: '0.375rem 0.375rem 0 0', + })} + > + <x-text>${node.operation}</x-text> + </x-flex> + + ${customComp + ? htmlStatic`<x-flex padding="sm" space="sm" ${css(cellBorder)}> + <${unsafeStatic(customComp)} .value=${node}></${unsafeStatic(customComp)}> + </x-flex>` + : html` + <x-flex padding="sm" space="sm" ${css(cellBorder)}> + <x-text ${css({ padding: '0.25rem', background: 'var(--color-accent-50)' })}>Arguments</x-text> + ${nodeDataContent(node.args, [])} + </x-flex> + + <x-flex padding="sm" space="sm" ${css(cellBorder)}> + <x-text ${css({ padding: '0.25rem', background: 'var(--color-accent-50)' })}>Result</x-text> + ${nodeDataContent(node.result, [])} + </x-flex> + `} + ${errors(node)} ${metaInfo(node)} ${timeStats(node)} + </x-flex> + `} + </x-flex> + `; + }, +); + +const errors = (node: IIntrospectNode) => html` + <x-flex padding="sm" space="sm" ${css(cellBorder)}> + <x-text ${css({ padding: '0.25rem', background: 'var(--color-accent-50)' })}>Errors</x-text> + <x-text ${css({ fontFamily: 'monospace', fontSize: '0.8rem', whiteSpace: 'pre-wrap' })} + >${JSON.stringify(node.errors, null, 2)}</x-text + > + </x-flex> +`; + +const metaInfo = (node: IIntrospectNode) => html` + <x-flex padding="sm" space="sm" ${css(cellBorder)}> + <x-text ${css({ padding: '0.25rem', background: 'var(--color-accent-50)' })}>Meta</x-text> + <x-text ${css({ fontFamily: 'monospace', fontSize: '0.8rem', whiteSpace: 'pre-wrap' })} + >${JSON.stringify(node.meta, null, 2)}</x-text + > + </x-flex> +`; + +const timeStats = (node: IIntrospectNode) => { + const startedAt = node.startedAt ? new Date(node.startedAt).toLocaleString() : 'No Start Time'; + const finishedAt = node.finishedAt ? new Date(node.finishedAt).toLocaleString() : 'No Finish Time'; + const elapsed = node.startedAt && node.finishedAt ? node.finishedAt.getTime() - node.startedAt.getTime() : 0; + + return html` + <x-flex ${css({ ...cellBorder, flexGrow: 1 })}> + <x-grid columns="1fr 1fr 1fr" padding="sm" space="sm"> + <x-text>Started</x-text> + <x-text>Finished</x-text> + <x-text>Elapsed</x-text> + <x-text ${css({ fontFamily: 'monospace', fontSize: '0.8rem', whiteSpace: 'pre-wrap' })}> ${startedAt} </x-text> + <x-text ${css({ fontFamily: 'monospace', fontSize: '0.8rem', whiteSpace: 'pre-wrap' })}> ${finishedAt} </x-text> + <x-text ${css({ fontFamily: 'monospace', fontSize: '0.8rem', whiteSpace: 'pre-wrap' })}> ${elapsed}ms </x-text> + </x-grid> + </x-flex> + `; +}; + +// TODO dirty hack for accessing properties that were not requested +const fillEmptyFields = (data: Maybe<AdornisEntity>) => { + if (!data) return data; + + Object.values(data.fields).forEach(field => { + if (!data.has(field.name as any)) { + data[field.name] = null; + return; + } + if (data[field.name] instanceof AdornisEntity) fillEmptyFields(data[field.name]); + }); + return data; +}; diff --git a/lab/introspect/client/x-introspect-viewer.ts b/lab/introspect/client/x-introspect-viewer.ts index f0ede5b462..1a0e306f31 100644 --- a/lab/introspect/client/x-introspect-viewer.ts +++ b/lab/introspect/client/x-introspect-viewer.ts @@ -1,47 +1,48 @@ -import type { Maybe } from '@adornis/base/utilTypes.js'; -import { AdornisEntity } from '@adornis/baseql/entities/adornisEntity.js'; -import { constructValue } from '@adornis/baseql/entities/construct.js'; +import { selectionSet } from '@adornis/baseql/utils/queryGeneration.js'; import { ChemistryLitElement } from '@adornis/chemistry/chemistry-lit-element.js'; -import { acss } from '@adornis/chemistry/directives/acss.js'; +import { RXController } from '@adornis/chemistry/controllers/RXController.js'; import { css } from '@adornis/chemistry/directives/css.js'; import '@adornis/cms/client/generated-form.js'; -import { GenericFieldFormController } from '@adornis/cms/client/generic-field-formcontroller.js'; -import { FormController } from '@adornis/forms/x-form-controller.js'; -import { html, nothing, type PropertyValues } from 'lit'; +import { stateful } from '@adornis/functional-lit/stateful.js'; +import { useQuery } from '@adornis/functional-lit/use-query.js'; +import { html, nothing } from 'lit'; import { customElement, property, state } from 'lit/decorators.js'; import { createRef, ref, type Ref } from 'lit/directives/ref.js'; import { repeat } from 'lit/directives/repeat.js'; -import { html as htmlStatic, unsafeStatic } from 'lit/static-html.js'; -import type { IntrospectNode } from '../introspect-node.js'; - -const cellBorder = { - borderBottom: '1px solid rgba(0,0,0,0.1)', - borderRight: '1px solid rgba(0,0,0,0.1)', - borderLeft: '1px solid rgba(0,0,0,0.1)', -}; +import { of, switchMap } from 'rxjs'; +import { getIntrospectNodeByID } from '../api/get-introspect-node.js'; +import { IntrospectNode } from '../entities/introspect-node.entity.js'; +import { IntrospectViewerNode } from './x-introspect-viewer-node.js'; @customElement('x-introspect-viewer') export class XIntrospectViewer extends ChemistryLitElement { - @property({ attribute: false }) node: Maybe<IntrospectNode & { _id?: string }> = null; - - @state() private _closed: Record<string, boolean> = {}; - @state() private _forms: Record<string, FormController<AdornisEntity>> = {}; - - override updated(changedProperties: PropertyValues): void { - if (changedProperties.has('node') && this.node) - this.node.children?.forEach((childNode, i) => { - this._closed['root.' + i] = true; - return this._closeAllChildNodes(childNode, ['root', i.toString()]); - }); - - super.updated(changedProperties); - } + @property({ attribute: false }) nodeID = new RXController<string | null>(this, null); + + @state() private _node = new RXController( + this, + this.nodeID.observable.pipe( + switchMap(nodeID => { + if (!nodeID) return of(null); + return getIntrospectNodeByID(nodeID)(selectionSet(() => IntrospectNode, 1)); + }), + ), + ); + + // override updated(changedProperties: PropertyValues): void { + // if (changedProperties.has('node') && this._node) + // this._node.children?.forEach((childNode, i) => { + // this._closed['root.' + i] = true; + // return this._closeAllChildNodes(childNode, ['root', i.toString()]); + // }); + + // super.updated(changedProperties); + // } private _header() { const values = { - _id: this.node?._id, - userID: this.node?.userID ?? 'No User ID', - sessionID: this.node?.sessionID ?? 'No Session ID', + _id: this._node.value?._id, + userID: this._node.value?.userID ?? 'No User ID', + sessionID: this._node.value?.sessionID ?? 'No Session ID', }; return html` @@ -86,256 +87,72 @@ export class XIntrospectViewer extends ChemistryLitElement { bgRef: Ref<HTMLElement> = createRef(); - private _isGeneratedForm(data: any): 'array' | 'object' | 'values' | false { - if (Array.isArray(data)) { - if (data?.[0]?._class) { - return 'array'; - } - } - if (data?._class) { - return 'object'; - } - if (data) { - const values = Object.values(data) as any[]; - for (let i = 0; i < values.length; i++) { - if (values[i]?._class) { - return 'values'; - } - } - } - return false; - } - - private _genBlacklist(data: Maybe<AdornisEntity>) { - if (!data) { - return []; - } - return Object.keys((data as any).docMeta) - .map(key => { - if (!data.has(key as any)) { - return key; - } - return null; - }) - .filter(Boolean) as string[]; - } - - // TODO dirty hack for accessing properties that were not requested - private _fillEmptyFields(data: Maybe<AdornisEntity>) { - if (!data) return data; - - Object.values(data.fields).forEach(field => { - if (!data.has(field.name as any)) { - data[field.name] = null; - return; - } - if (data[field.name] instanceof AdornisEntity) this._fillEmptyFields(data[field.name]); - }); - return data; - } - - private _nodeDataContent(data: any, path: string[] = []) { - const genForm = this._isGeneratedForm(data); - if (genForm) { - switch (genForm) { - case 'array': - return repeat(data, (res: any, i) => { - const id = path.join('.') + '#' + i; - this._forms[id] ??= new GenericFieldFormController(this, this._fillEmptyFields(constructValue(res)), { - enabledFields: {}, - }); - return html` - <x-text tone="subtle"> ${res._class} #${i + 1} </x-text> - <generated-form - .formController=${this._forms[id]!} - .blacklist=${this._genBlacklist(this._forms[id]?.document)} - ></generated-form> - `; - }); - case 'object': - const id = path.join('.'); - this._forms[id] ??= new GenericFieldFormController(this, this._fillEmptyFields(constructValue(data)), { - enabledFields: {}, - }); - return html` - <x-text tone="subtle"> ${data._class} </x-text> - <generated-form - .formController=${this._forms[id]!} - .blacklist=${this._genBlacklist(this._forms[id]?.document)} - ></generated-form> - `; - case 'values': - return repeat(Object.entries(data), ([key, val]) => { - return this._nodeDataContent(val, [...path, key]); - // const id = path.join('.') + '#' + key; - // this._forms[id] ??= new GenericFieldFormController(this, this._fillEmptyFields(constructValue(val)), { - // enabledFields: {}, - // }); - // return html` - // <x-text> ${key} </x-text> - // <generated-form - // .formController=${this._forms[id]!} - // .blacklist=${this._genBlacklist(this._forms[id]?.document)} - // ></generated-form> - // `; - }); - } - } - - return html` - <x-flex space="sm"> - <x-text tone="subtle"> ${path.join(' > ')} </x-text> - <x-text ${css({ fontFamily: 'monospace', fontSize: '0.8rem', whiteSpace: 'pre-wrap' })}> - ${JSON.stringify(data, null, 2)} - </x-text> - </x-flex> - `; - } - - private _nodeContent(node: IntrospectNode, parent: boolean, path: string[] = []) { - const customComp = node.meta['component']; - if (customComp) { - return htmlStatic`<x-flex padding="sm" space="sm" ${css(cellBorder)}> - <${unsafeStatic(customComp)} .value=${node}></${unsafeStatic(customComp)}> - </x-flex>`; - } - - const startedAt = node.startedAt ? new Date(node.startedAt).toLocaleString() : 'No Start Time'; - const finishedAt = node.finishedAt ? new Date(node.finishedAt).toLocaleString() : 'No Finish Time'; - const elapsed = node.startedAt && node.finishedAt ? node.finishedAt.getTime() - node.startedAt.getTime() : 0; - - return html` <x-flex padding="sm" space="sm" ${css(cellBorder)}> - <x-text ${css({ padding: this.spacing.xs, background: this.colors.accent.shade(50) })}>Arguments</x-text> - ${this._nodeDataContent(node.args, path)} - </x-flex> - <x-flex padding="sm" space="sm" ${css(cellBorder)}> - <x-text ${css({ padding: this.spacing.xs, background: this.colors.accent.shade(50) })}>Result</x-text> - ${this._nodeDataContent(node.result, path)} - </x-flex> - <x-flex padding="sm" space="sm" ${css(cellBorder)}> - <x-text ${css({ padding: this.spacing.xs, background: this.colors.accent.shade(50) })}>Errors</x-text> - <x-text ${css({ fontFamily: 'monospace', fontSize: '0.8rem', whiteSpace: 'pre-wrap' })} - >${JSON.stringify(node.errors, null, 2)}</x-text - > - </x-flex> - <x-flex padding="sm" space="sm" ${css(cellBorder)}> - <x-text ${css({ padding: this.spacing.xs, background: this.colors.accent.shade(50) })}>Meta</x-text> - <x-text ${css({ fontFamily: 'monospace', fontSize: '0.8rem', whiteSpace: 'pre-wrap' })} - >${JSON.stringify(node.meta, null, 2)}</x-text - > - </x-flex> - <x-flex - ${css({ - ...cellBorder, - flexGrow: 1, - })} - > - <x-grid columns="1fr 1fr 1fr" padding="sm" space="sm"> - <x-text>Started</x-text> - <x-text>Finished</x-text> - <x-text>Elapsed</x-text> - <x-text ${css({ fontFamily: 'monospace', fontSize: '0.8rem', whiteSpace: 'pre-wrap' })}>${startedAt}</x-text> - <x-text ${css({ fontFamily: 'monospace', fontSize: '0.8rem', whiteSpace: 'pre-wrap' })}>${finishedAt}</x-text> - <x-text ${css({ fontFamily: 'monospace', fontSize: '0.8rem', whiteSpace: 'pre-wrap' })}>${elapsed}ms</x-text> - </x-grid> - </x-flex>`; - } - - private _node(node: IntrospectNode, parent: boolean, path: string[] = []) { - const closed = !!this._closed[path.join('.')]; - + private _nodeView( + node: { value: IntrospectNode | null; loading: boolean; error: Error | null }, + parent: boolean, + path: string[] = [], + ) { return html` <x-flex ${css({ paddingBottom: parent ? '50px' : '0' })} horizontal> - <x-flex - ${css({ - display: 'inline-flex', - background: 'white', - minWidth: '250px', - maxWidth: '450px', - flexShrink: '0', - borderRadius: this.sizes.borderRadiusSecondary, - })} - > - <x-flex - padding="sm" - space-between - horizontal - crossaxis-center - ${css({ - background: this.colors.accent, - color: 'white', - borderRadius: `${this.sizes.borderRadiusSecondary} ${this.sizes.borderRadiusSecondary} 0 0`, - })} - ${acss({ - cursor: 'pointer', - '&:hover': { - opacity: 0.5, - }, - })} - @click=${() => { - this._closed[path.join('.')] = !this._closed[path.join('.')]; - if (this._closed[path.join('.')]) { - // close of child nodes - - this._closeAllChildNodes(node, path); - } - this.requestUpdate(); - }} - > - <x-text>${node.operation}</x-text> - <x-icon> ${closed ? 'unfold_more' : 'unfold_less'} </x-icon> - </x-flex> - ${closed - ? html`<x-flex ${css({ border: '1px solid rgba(0,0,0,0.1)', flexGrow: 1 })} padding="sm"> - <x-text ${css({ opacity: 0.5 })}>Expand to see more...</x-text> - </x-flex>` - : html` ${this._nodeContent(node, parent, path)} `} - </x-flex> - - ${(node.children ?? []).length > 0 + ${IntrospectViewerNode(node)} + ${(node.value?.childIDs ?? []).length > 0 ? html` <x-flex ${css({ padding: `${this.spacing.sm} 0` })} space="md"> - ${repeat(node.children ?? [], (child, index) => { - return html`<x-flex horizontal> - <x-flex space-between ${css({ padding: `${this.spacing.sm} ${this.spacing.md}` })}> - <svg - aria-hidden="true" - xmlns="http://www.w3.org/2000/svg" - width="24" - height="24" - fill="none" - viewBox="0 0 24 24" - > - <path - stroke="currentColor" - stroke-linecap="round" - stroke-linejoin="round" - stroke-width="2" - d="M19 12H5m14 0-4 4m4-4-4-4" - /> - </svg> - - <svg - class="w-6 h-6 text-gray-800 dark:text-white" - aria-hidden="true" - xmlns="http://www.w3.org/2000/svg" - width="24" - height="24" - fill="none" - viewBox="0 0 24 24" - > - <path - stroke="currentColor" - stroke-linecap="round" - stroke-linejoin="round" - stroke-width="2" - d="M5 12h14M5 12l4-4m-4 4 4 4" - /> - </svg> - </x-flex> - ${this._node(child, false, [...path, index.toString()])} - </x-flex>`; - })} + ${repeat( + node.value?.childIDs ?? [], + (childID, index) => + html` + ${stateful(() => { + const introspectionNode = useQuery( + childID ? getIntrospectNodeByID(childID)(selectionSet(() => IntrospectNode, 1)) : of(null), + [childID], + ); + + return html` + <x-flex horizontal> + <x-flex space-between ${css({ padding: `${this.spacing.sm} ${this.spacing.md}` })}> + <svg + aria-hidden="true" + xmlns="http://www.w3.org/2000/svg" + width="24" + height="24" + fill="none" + viewBox="0 0 24 24" + > + <path + stroke="currentColor" + stroke-linecap="round" + stroke-linejoin="round" + stroke-width="2" + d="M19 12H5m14 0-4 4m4-4-4-4" + /> + </svg> + + <svg + class="w-6 h-6 text-gray-800 dark:text-white" + aria-hidden="true" + xmlns="http://www.w3.org/2000/svg" + width="24" + height="24" + fill="none" + viewBox="0 0 24 24" + > + <path + stroke="currentColor" + stroke-linecap="round" + stroke-linejoin="round" + stroke-width="2" + d="M5 12h14M5 12l4-4m-4 4 4 4" + /> + </svg> + </x-flex> + + ${this._nodeView(introspectionNode, false, [...path, index.toString()])} + </x-flex> + `; + })()} + `, + )} </x-flex> ` : nothing} @@ -344,7 +161,8 @@ export class XIntrospectViewer extends ChemistryLitElement { } override render() { - if (!this.node) { + if (this._node.isLoading) return html` <x-loading-state no-wrapper></x-loading-state> `; + if (!this._node.value) { return html` <x-empty-placeholder no-wrapper .text=${'Keine Daten gefunden'} .img=${'close'}></x-empty-placeholder> `; @@ -377,17 +195,19 @@ export class XIntrospectViewer extends ChemistryLitElement { }} ${css({ display: 'block', overflow: 'auto' })} > - ${this._node(this.node, true, ['root'])}</x-flex + ${this._nodeView({ value: this._node.value, loading: this._node.isLoading, error: null }, true, [ + 'root', + ])}</x-flex > </x-flex> `; } - private _closeAllChildNodes(node: IntrospectNode | undefined, path: string[]) { - if (!node) return; - node.children?.forEach((child, index) => { - this._closed[path.join('.') + '.' + index] = true; - this._closeAllChildNodes(child, [...path, index.toString()]); - }); - } + // private _closeAllChildNodes(node: IntrospectNode | undefined, path: string[]) { + // if (!node) return; + // node.children?.forEach((child, index) => { + // this._closed[path.join('.') + '.' + index] = true; + // this._closeAllChildNodes(child, [...path, index.toString()]); + // }); + // } } diff --git a/lab/introspect/entities/introspect-node.entity.ts b/lab/introspect/entities/introspect-node.entity.ts new file mode 100644 index 0000000000..6e09c91ea0 --- /dev/null +++ b/lab/introspect/entities/introspect-node.entity.ts @@ -0,0 +1,88 @@ +import { A } from '@adornis/base/env-info.js'; +import type { Maybe } from '@adornis/base/utilTypes.js'; +import { ID } from '@adornis/baseql/baseqlTypes.js'; +import { Entity, Field } from '@adornis/baseql/decorators.js'; +import { AdornisEntity } from '@adornis/baseql/entities/adornisEntity.js'; +import { getRawCollection } from '@adornis/baseql/server/collections.js'; +import { getIntrospectCollectionName } from '../server/introspection-options.js'; + +if (A.isServer) { + const collectionName = getIntrospectCollectionName(); + const collection = await getRawCollection<any>(collectionName); + + await collection.createIndex({ + operation: 1, + }); + await collection.createIndex({ + operation: 1, + startedAt: -1, + }); +} + +/** Helper function to stringify a value if it's not already a string */ +function stringifyIfNeeded(value: unknown): string { + return JSON.stringify(value); +} + +@Entity() +export class IntrospectNode extends AdornisEntity { + static override _class = 'IntrospectNode'; + + @Field(() => ID) + _id!: string; + + /** The name of the operation */ + @Field(() => String) + operation: Maybe<string>; + + /** The time the operation started */ + @Field(() => Date) + startedAt: Maybe<Date>; + + /** The time the operation finished */ + @Field(() => Date) + finishedAt: Maybe<Date>; + + /** The result of the operation */ + @Field(() => String, { + resolve: function (this: IntrospectNode) { + return () => stringifyIfNeeded(this.result); + }, + }) + result: Maybe<string>; + + /** Any errors of the operation */ + @Field(() => String, { + resolve: function (this: IntrospectNode) { + return () => stringifyIfNeeded(this.errors); + }, + }) + errors: Maybe<string>; + + /** The input arguments of the operation */ + @Field(() => String, { + resolve: function (this: IntrospectNode) { + return () => stringifyIfNeeded(this.args); + }, + }) + args: Maybe<string>; + + /** Session ID of who started the operation */ + @Field(() => String) + sessionID: Maybe<string>; + + /** User ID of who started the operation */ + @Field(() => String) + userID: Maybe<string>; + + @Field(() => [ID]) + childIDs: Maybe<string[]>; + + /** Any additional meta data */ + @Field(() => String, { + resolve: function (this: IntrospectNode) { + return () => stringifyIfNeeded(this.meta); + }, + }) + meta: Maybe<string>; +} diff --git a/lab/introspect/introspect-node.ts b/lab/introspect/introspect-node.ts index 10aedbc5a6..66b97d6edb 100644 --- a/lab/introspect/introspect-node.ts +++ b/lab/introspect/introspect-node.ts @@ -1,45 +1,66 @@ -export interface IntrospectNode { - /** Reference to the parent */ - parent?: IntrospectNode; +import { A } from '@adornis/base/env-info.js'; +import { IntrospectNode as IntrospectNodeEntity } from './entities/introspect-node.entity.js'; - /** The name of the operation */ +export interface IIntrospectNode { operation: string; - - /** The time the operation started */ - startedAt?: Date; - - /** The time the operation finished */ + startedAt: Date; finishedAt?: Date; - - /** The result of the operation */ result?: any; - - /** Any errors of the operation */ errors?: any; - - /** The input arguments of the operation */ args?: Record<string, any>; + sessionID: string | null; + userID: string | null; + meta: Record<string, any>; +} - /** Session ID of who started the operation */ - sessionID?: string; - - /** User ID of who started the operation */ - userID?: string; - +export interface IIntrospectNodeWithChildren extends IIntrospectNode { /** Any operations that were called inside the operation */ - children?: IntrospectNode[]; + children?: IIntrospectNodeWithChildren[]; +} - /** Any additional meta data */ - meta: Record<string, any>; +export interface IIntrospectDBNode extends IIntrospectNodeWithChildren { + _id: string; + _class: string; + childIDs?: string[]; } /** - * Removes all parent references to avoid circular references - * @param node The node to clean - * @returns The cleaned node + * Converts an IIntrospectNode object into IntrospectNode entity with _id references + * @param node The node to convert + * @returns The converted entity and a map of all created entities */ -export function cleanParents(node: IntrospectNode) { - delete node.parent; - node.children = node.children?.map(child => cleanParents(child)); - return node; +export function convertIntrospectNodeObjectToDBObject(node: IIntrospectNodeWithChildren): { + rootEntity: IIntrospectDBNode; + allEntities: IIntrospectDBNode[]; +} { + const allEntities = new Map<string, IIntrospectDBNode>(); + + function convertNode(node: IIntrospectNodeWithChildren): IIntrospectDBNode { + const entity: IIntrospectDBNode = { + _id: A.getGloballyUniqueID(), + _class: IntrospectNodeEntity._class, + operation: node.operation, + startedAt: node.startedAt, + finishedAt: node.finishedAt, + result: node.result ?? null, + errors: node.errors ?? null, + args: node.args, + sessionID: node.sessionID, + userID: node.userID, + meta: node.meta, + childIDs: [], + }; + + allEntities.set(entity._id, entity); + + if (node.children?.length) { + const childEntities = node.children.map(child => convertNode(child)); + entity.childIDs = childEntities.map(child => child._id); + } + + return entity; + } + + const rootEntity = convertNode(node); + return { rootEntity, allEntities: Array.from(allEntities.values()) }; } diff --git a/lab/introspect/server/introspect.ts b/lab/introspect/server/introspect.ts index 74ccb7ec39..1aef213465 100644 --- a/lab/introspect/server/introspect.ts +++ b/lab/introspect/server/introspect.ts @@ -1,23 +1,10 @@ -import { AdornisEntity } from '@adornis/baseql/entities/adornisEntity.js'; import { AsyncLocalStorage } from 'async_hooks'; -import type { IntrospectNode } from '../introspect-node.js'; +import type { IIntrospectNodeWithChildren } from '../introspect-node.js'; +import { trackOperation } from './track-operation.js'; -export const introspectAsyncStore = AsyncLocalStorage ? new AsyncLocalStorage<IntrospectNode>() : undefined; - -/** - * Helper to strip some unnecessary data from the result - * @param data The data to clean - * @returns The cleaned data - */ -export function cleanData(data: any) { - if (Array.isArray(data)) { - return data.map(item => cleanData(item)); - } - if (data instanceof AdornisEntity) { - return data.toObject(); - } - return data; -} +export const introspectAsyncStore = AsyncLocalStorage + ? new AsyncLocalStorage<IIntrospectNodeWithChildren>() + : undefined; /** * Helper to wrap any function and store introspection information @@ -26,34 +13,8 @@ export function cleanData(data: any) { * @param args The arguments to the function */ export function wrapIntrospect<TFn extends (...args: any[]) => any>(opName: string, fn: TFn) { - return async (...args: Parameters<TFn>): Promise<Awaited<ReturnType<TFn>>> => { - const store = introspectAsyncStore?.getStore(); - - // if we are not in an introspection session then just run the function - if (!introspectAsyncStore || !store) return fn(...args); - - // otherwise we construct a node - const node: IntrospectNode = { - operation: opName, - startedAt: new Date(), - meta: {}, - args: { - args, - }, - }; - node.parent = store; - if (!store.children) store.children = []; - store.children.push(node); - - // run the original function - const res = await introspectAsyncStore?.run(node, () => fn(...args)); - - // store the result - node.result = cleanData(res); - node.finishedAt = new Date(); - - return res; - }; + return async (...args: Parameters<TFn>): Promise<Awaited<ReturnType<TFn>>> => + trackOperation(opName, fn, args) as Promise<Awaited<ReturnType<TFn>>>; } /** diff --git a/lab/introspect/server/introspectInjector.ts b/lab/introspect/server/introspectInjector.ts index 36ee4602aa..16b6e8bea1 100644 --- a/lab/introspect/server/introspectInjector.ts +++ b/lab/introspect/server/introspectInjector.ts @@ -1,30 +1,17 @@ -import { A } from '@adornis/base/env-info.js'; -import { logger } from '@adornis/base/logging.js'; -import type { Maybe, OptionalPromise } from '@adornis/base/utilTypes.js'; import type { BaseQLFieldDefinition } from '@adornis/baseql/metadata/types.js'; import type { BaseQLWebsocketMessageGQLStart } from '@adornis/baseql/protocol.js'; import { PassthroughHandler, type RequestHandler } from '@adornis/baseql/requestHandler.js'; -import { getRawCollection } from '@adornis/baseql/server/collections.js'; -import { context } from '@adornis/baseql/server/context.js'; -import type { ExecutionResult, GraphQLError } from 'graphql'; -import type { ObjMap } from 'graphql/jsutils/ObjMap.js'; +import type { ExecutionResult } from 'graphql'; import assert from 'node:assert'; -import { cleanParents, type IntrospectNode } from '../introspect-node.js'; -import { cleanData, introspectAsyncStore } from './introspect.js'; - -interface IntrospectInjectorOptions { - operationFilter?: (op: BaseQLFieldDefinition) => boolean; - saveHandler?: (node: IntrospectNode) => Promise<void>; - onCollected?: (node: IntrospectNode) => OptionalPromise<void>; - collectionName?: string; -} +import { introspectAsyncStore } from './introspect.js'; +import { setIntrospectOptions, type IntrospectInjectorOptions } from './introspection-options.js'; +import { trackOperation } from './track-operation.js'; export class IntrospectInjector extends PassthroughHandler<ExecutionResult> { - private _opts: Maybe<IntrospectInjectorOptions> = null; - constructor(upstream: RequestHandler<ExecutionResult>, opts?: IntrospectInjectorOptions) { super(upstream); - this._opts = opts; + + setIntrospectOptions(opts); } override subscription(op: BaseQLFieldDefinition, payload: BaseQLWebsocketMessageGQLStart['payload']) { @@ -34,106 +21,37 @@ export class IntrospectInjector extends PassthroughHandler<ExecutionResult> { override async query(op: BaseQLFieldDefinition, payload: BaseQLWebsocketMessageGQLStart['payload']) { assert(introspectAsyncStore); - // this has to be casted because the typing of AsyncLocalStorage does not infer the return value out of the callback anymore - return introspectAsyncStore.run(this.getStore(op, payload), async () => { - const res = await this.upstream.query(op, payload); - - // * we ignore internal operations - if (op.name.indexOf('__') === 0) return res as unknown as Promise<ExecutionResult>; - // * we check if the parent is allowed to be tracked, if so all children are also allowed - if (!introspectAsyncStore?.getStore()?.parent && this._opts?.operationFilter && !this._opts.operationFilter(op)) - return res as unknown as Promise<ExecutionResult>; + let baseqlOperationReturnValue: ExecutionResult; - await this._track(op, payload, res.data, res.errors as Maybe<GraphQLError[]>); + await trackOperation( + op.name, + async () => { + baseqlOperationReturnValue = await this.upstream.query(op, payload); + if (baseqlOperationReturnValue.errors?.length) throw baseqlOperationReturnValue.errors; + return baseqlOperationReturnValue.data?.[op.name]; + }, + [payload.variables], + ); - return res as unknown as Promise<ExecutionResult>; - }); + // @ts-expect-error waiting on _track call makes sure thie variable is set properly + return baseqlOperationReturnValue; } override async mutation(op: BaseQLFieldDefinition, payload: BaseQLWebsocketMessageGQLStart['payload']) { - assert(introspectAsyncStore); - // this has to be casted because the typing of AsyncLocalStorage does not infer the return value out of the callback anymore - return introspectAsyncStore.run(this.getStore(op, payload), async () => { - const res = await this.upstream.mutation(op, payload); - - // * we ignore internal operations - if (op.name.indexOf('__') === 0) return res as unknown as Promise<ExecutionResult>; - - // * we check if the parent is allowed to be tracked, if so all children are also allowed - if (!introspectAsyncStore?.getStore()?.parent && this._opts?.operationFilter && !this._opts.operationFilter(op)) - return res as unknown as Promise<ExecutionResult>; - - await this._track(op, payload, res.data, res.errors as Maybe<GraphQLError[]>); - - return res as unknown as Promise<ExecutionResult>; - }); - } - - private async _track( - op: BaseQLFieldDefinition, - payload: BaseQLWebsocketMessageGQLStart['payload'], - data: Maybe<ObjMap<unknown>>, - errors: Maybe<GraphQLError[]>, - ) { - const store = introspectAsyncStore?.getStore(); - if (store) { - store.args = payload.variables; - store.result = cleanData(data?.[op.name]); - store.errors = errors; - store.finishedAt = new Date(); - } - - // * if no parent then we are back at the root - if (store && !store.parent) { - store.sessionID = context.sessionID; - if (!context.serverContext) { - store.userID = context.userID; - } - - if (this._opts?.saveHandler) { - await this._opts.saveHandler(store); - } else { - const col = await getRawCollection(this._opts?.collectionName ?? 'introspect'); - - // * we don't really need to wait for the insert to finish. Just start it. - col - .insertOne({ - _id: A.getGloballyUniqueID() as any, - ...cleanParents(store), - }) - // use lambda function instead of just passing the function to avoid this context issues - .catch(err => - logger.error({ err, store: cleanParents(store) }, 'There was an error inserting the introspection data'), - ); - } - - if (this._opts?.onCollected) { - this._opts.onCollected(store); - } - } - } - - getStore(op: BaseQLFieldDefinition, payload: BaseQLWebsocketMessageGQLStart['payload']): IntrospectNode { - assert(introspectAsyncStore); - - const node: IntrospectNode = { - operation: op.name ?? 'No Name', - startedAt: new Date(), - meta: {}, - }; - - if (op.name == 'getEntityByID' && payload.variables?.entityClass) { - node.meta['entityClass'] = payload.variables?.entityClass; - } - - const parent = introspectAsyncStore.getStore(); - if (parent) { - parent.children ??= []; - parent.children.push(node); - node.parent = parent; - } - - return node; + let baseqlOperationReturnValue: ExecutionResult; + + await trackOperation( + op.name, + async () => { + baseqlOperationReturnValue = await this.upstream.query(op, payload); + if (baseqlOperationReturnValue.errors?.length) throw baseqlOperationReturnValue.errors; + return baseqlOperationReturnValue.data?.[op.name]; + }, + [payload.variables], + ); + + // @ts-expect-error waiting on _track call makes sure thie variable is set properly + return baseqlOperationReturnValue; } } diff --git a/lab/introspect/server/introspection-options.ts b/lab/introspect/server/introspection-options.ts new file mode 100644 index 0000000000..10eb161215 --- /dev/null +++ b/lab/introspect/server/introspection-options.ts @@ -0,0 +1,62 @@ +import { logger } from '@adornis/base/logging.js'; +import type { OptionalPromise } from '@adornis/base/utilTypes.js'; +import { getRawCollection } from '@adornis/baseql/server/collections.js'; +import { type IIntrospectNodeWithChildren, convertIntrospectNodeObjectToDBObject } from '../introspect-node.js'; + +export interface IntrospectInjectorOptions { + operationFilter?: (op: { name?: string }) => boolean; + saveHandler?: (node: IIntrospectNodeWithChildren) => Promise<void>; + onCollected?: (node: IIntrospectNodeWithChildren) => OptionalPromise<void>; + collectionName?: string; +} + +let introspectOptionsAreSet: boolean = false; +let operationFilter: IntrospectInjectorOptions['operationFilter']; +let saveHandler: IntrospectInjectorOptions['saveHandler']; +let onCollected: IntrospectInjectorOptions['onCollected']; +let collectionName: IntrospectInjectorOptions['collectionName']; + +export function setIntrospectOptions(opts?: IntrospectInjectorOptions) { + if (introspectOptionsAreSet) return; + + operationFilter = opts?.operationFilter; + saveHandler = opts?.saveHandler; + onCollected = opts?.onCollected; + collectionName = opts?.collectionName; + introspectOptionsAreSet = true; +} + +export function getIntrospectOperationFilter() { + return operationFilter; +} + +export function getIntrospectSaveHandler() { + return ( + saveHandler ?? + (async (node: IIntrospectNodeWithChildren) => { + const collectionName = getIntrospectCollectionName(); + const col = await getRawCollection<any>(collectionName); + + const { rootEntity, allEntities } = convertIntrospectNodeObjectToDBObject(node); + + // * we don't really need to wait for the insert to finish. Just start it. + col + .insertMany(allEntities) + // use lambda function instead of just passing the function to avoid this context issues + .catch(err => + logger.error( + { err, operationName: rootEntity.operation }, + 'There was an error inserting the introspection data', + ), + ); + }) + ); +} + +export function getIntrospectOnCollected() { + return onCollected; +} + +export function getIntrospectCollectionName() { + return collectionName ?? 'introspect2'; +} diff --git a/lab/introspect/server/track-operation.ts b/lab/introspect/server/track-operation.ts new file mode 100644 index 0000000000..bdc797a920 --- /dev/null +++ b/lab/introspect/server/track-operation.ts @@ -0,0 +1,88 @@ +import { AdornisEntity } from '@adornis/baseql/entities/adornisEntity.js'; +import { context } from '@adornis/baseql/server/context.js'; +import assert from 'node:assert'; +import type { IIntrospectNodeWithChildren } from '../introspect-node.js'; +import { introspectAsyncStore } from './introspect.js'; +import { + getIntrospectOnCollected, + getIntrospectOperationFilter, + getIntrospectSaveHandler, +} from './introspection-options.js'; + +export async function trackOperation(operationName: string, op: (...args: any[]) => any, variables: any[] | undefined) { + // if operation should not be tracked dont spawn a new async context + const operationFilter = getIntrospectOperationFilter(); + if (!introspectAsyncStore?.getStore() && operationFilter && !operationFilter({ name: operationName })) + return op(...(variables ?? [])); + + // spawn new async context and run function + assert(introspectAsyncStore); + return introspectAsyncStore.run(getStore({ name: operationName }), async () => { + let data: any; + let errors: Error[] | undefined; + + try { + data = await op(...(variables ?? [])); + } catch (err) { + errors = Array.isArray(err) ? err : [err]; + } + + const store = introspectAsyncStore?.getStore(); + if (store) { + store.args = variables; + store.result = cleanData(data); + store.errors = errors; + store.finishedAt = new Date(); + } + + // * if no parent then we are back at the root + if (store && !store.parent) { + store.sessionID = context.sessionID; + if (!context.serverContext) { + store.userID = context.userID; + } + + await getIntrospectSaveHandler()(store); + + getIntrospectOnCollected()?.(store); + } + + return data; + }); +} + +function getStore(op: { name?: string }): IIntrospectNodeWithChildren { + assert(introspectAsyncStore); + + const node: IIntrospectNodeWithChildren = { + operation: op.name ?? 'No Name', + startedAt: new Date(), + meta: {}, + // @ts-expect-error could be a user context + userID: context.userID, + sessionID: context.sessionID, + }; + + const parent = introspectAsyncStore.getStore(); + if (parent) { + parent.children ??= []; + parent.children.push(node); + } + + return node; +} + +/** + * Helper to strip some unnecessary data from the result + * @param data The data to clean + * @returns The cleaned data + */ +function cleanData(data: any) { + if (Array.isArray(data)) { + return data.map(item => cleanData(item)); + } + if (data instanceof AdornisEntity) { + return data.toObject(); + } + return data; +} diff --git a/lab/introspect/startup.test.ts b/lab/introspect/startup.test.ts new file mode 100644 index 0000000000..784bfa16db --- /dev/null +++ b/lab/introspect/startup.test.ts @@ -0,0 +1,12 @@ +// TODO + +/** + * query/mutation entrypoint no sub methods + * query/mutation entrypoint with sub methods + * query/mutation entrypoint with tracked sub methods + * query/mutation entrypoint with sub querys/mutations + * tracked method entrypoint with no sub methods + * tracked method entrypoint with sub methods + * tracked method entrypoint with tracked sub methods + * tracked method entrypoint with sub querys/mutations + */ -- GitLab From 7eedd78581e4199be78e114c4fb27e43321bcf18 Mon Sep 17 00:00:00 2001 From: racct <elias@adornis.de> Date: Fri, 14 Feb 2025 10:17:36 +0000 Subject: [PATCH 2/2] chore: fix typing issues --- lab/introspect/client/x-introspect-viewer-node.ts | 7 +++++-- lab/introspect/introspect-node.ts | 1 + lab/introspect/package.json | 2 ++ pnpm-lock.yaml | 6 ++++++ 4 files changed, 14 insertions(+), 2 deletions(-) diff --git a/lab/introspect/client/x-introspect-viewer-node.ts b/lab/introspect/client/x-introspect-viewer-node.ts index c87df27daa..cffe305ffd 100644 --- a/lab/introspect/client/x-introspect-viewer-node.ts +++ b/lab/introspect/client/x-introspect-viewer-node.ts @@ -115,12 +115,15 @@ export const IntrospectViewerNode = stateful( case 'object': return stateful(() => { const entity = fillEmptyFields(constructValue(data)); - const { form } = useGeneratedForm(entity, { + const form = useGeneratedForm(entity, { disabledFields: entity ? selectionSet(() => entity.constructor as typeof AdornisEntity, 1) : {}, }); return html` <x-text tone="subtle"> ${data._class} </x-text> - ${GeneratedForm({ form, blacklist: genBlacklist(form.document) })} + ${ + // @ts-expect-error useGeneratedForm needs correct typing + GeneratedForm({ form, blacklist: genBlacklist(form.document) }) + } `; })(); case 'values': diff --git a/lab/introspect/introspect-node.ts b/lab/introspect/introspect-node.ts index 66b97d6edb..3911a31058 100644 --- a/lab/introspect/introspect-node.ts +++ b/lab/introspect/introspect-node.ts @@ -16,6 +16,7 @@ export interface IIntrospectNode { export interface IIntrospectNodeWithChildren extends IIntrospectNode { /** Any operations that were called inside the operation */ children?: IIntrospectNodeWithChildren[]; + parent?: IIntrospectNodeWithChildren; } export interface IIntrospectDBNode extends IIntrospectNodeWithChildren { diff --git a/lab/introspect/package.json b/lab/introspect/package.json index d9464f63f1..3defdc2029 100644 --- a/lab/introspect/package.json +++ b/lab/introspect/package.json @@ -15,6 +15,8 @@ "@adornis/chemistry": "workspace:^", "@adornis/cms": "workspace:^", "@adornis/forms": "workspace:^", + "@adornis/functional-lit": "workspace:^", + "@adornis/generated-form": "workspace:^", "@adornis/users": "workspace:^", "graphql": "^16.8.0", "lit": "^3.1.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 46cfb37b72..f66e2b0bbe 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -776,6 +776,12 @@ importers: '@adornis/forms': specifier: workspace:^ version: link:../../modules/forms + '@adornis/functional-lit': + specifier: workspace:^ + version: link:../functional-lit + '@adornis/generated-form': + specifier: workspace:^ + version: link:../../modules/generated-form '@adornis/users': specifier: workspace:^ version: link:../../modules/users -- GitLab