@softize/opus 25.1.0 → 25.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +66 -0
- package/bin/cli.mjs +5 -0
- package/bin/lib/assistant-schema.d.mts +55 -0
- package/bin/lib/assistant-schema.mjs +286 -0
- package/bin/lib/check.mjs +173 -0
- package/bin/lib/copy.mjs +2 -0
- package/bin/lib/gen-manifest.mjs +4 -0
- package/bin/lib/gen-runner.mjs +31 -2
- package/bin/lib/gen-stubs.mjs +3 -0
- package/bin/lib/mcp.mjs +1 -1
- package/docs/adr/0053-read-action-declares-the-resource-assistant.md +443 -0
- package/docs/adr/0054-action-list-grouping-is-a-declared-server-cut.md +82 -0
- package/docs/protocol.md +59 -1
- package/package.json +5 -1
- package/registry/skills/create-opus-action/SKILL.md +8 -3
- package/registry/skills/create-opus-action/references/contract-and-binding.md +24 -1
- package/registry/skills/create-opus-action/references/evaluations.md +3 -0
- package/registry/skills/test-opus-action/SKILL.md +4 -1
- package/registry/skills/test-opus-action/references/evaluations.md +2 -0
- package/registry/skills/test-opus-action/references/harness.md +14 -0
- package/src/ai/index.ts +3 -0
- package/src/ai/resource-context.ts +113 -0
- package/src/core/actions.ts +5 -2
- package/src/core/assistant.ts +545 -0
- package/src/core/contracts.ts +2 -0
- package/src/core/index.ts +23 -0
- package/src/core/list-grouping.ts +45 -0
- package/src/core/runtime.ts +138 -0
- package/src/core/types.ts +39 -0
- package/src/mcp/index.ts +3 -0
- package/src/ui/components/patterns/grouped-list.ts +88 -0
- package/src/ui/components/patterns/list.tsx +395 -61
- package/src/ui/docs/content/action-list.md +42 -9
- package/src/ui/docs/content/actions.md +97 -1
- package/src/ui/docs/content/ai.md +4 -2
- package/src/ui/docs/content/cli.md +2 -1
- package/src/ui/docs/content/mcp.md +3 -1
- package/src/ui/drivers/context.tsx +50 -0
- package/src/ui/drivers/react.tsx +9 -69
- package/src/ui/meta.ts +1 -1
package/src/core/runtime.ts
CHANGED
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
|
|
14
14
|
import { isReaction } from './reactions.ts'
|
|
15
15
|
import { isBackgroundAction } from './actions.ts'
|
|
16
|
+
import { assertValidListGrouping } from './list-grouping.ts'
|
|
16
17
|
import { isSchedule } from './schedules.ts'
|
|
17
18
|
import {
|
|
18
19
|
collectDomainWarnings,
|
|
@@ -25,6 +26,17 @@ import { createPurgeHandler, entityTableName, type PurgeHandler } from './retent
|
|
|
25
26
|
import type { DataProductConfig } from './data-product.ts'
|
|
26
27
|
import { AuditEmitter } from './audit.ts'
|
|
27
28
|
import { countReturnedRecords, hasInvalidEffect, resolveActionEffect } from './action-nature.ts'
|
|
29
|
+
import {
|
|
30
|
+
assertAssistantDeclaration,
|
|
31
|
+
projectAssistantContext,
|
|
32
|
+
publicAssistant,
|
|
33
|
+
resolveAssistantTitle,
|
|
34
|
+
type AssistantOmission,
|
|
35
|
+
type ReadResourceOptions,
|
|
36
|
+
type ResourceContext,
|
|
37
|
+
type RegisteredAssistant,
|
|
38
|
+
type ResourceReadResult,
|
|
39
|
+
} from './assistant.ts'
|
|
28
40
|
import { error, isActionError, normalizeError } from './errors.ts'
|
|
29
41
|
import { normalizeTraceContext } from './trace.ts'
|
|
30
42
|
import { isSystemActor, markSystemActor } from './system-actor.ts'
|
|
@@ -78,6 +90,7 @@ import type {
|
|
|
78
90
|
BoundAi,
|
|
79
91
|
AiTool,
|
|
80
92
|
AIConfig,
|
|
93
|
+
ActionAssistant,
|
|
81
94
|
User,
|
|
82
95
|
TraceContext,
|
|
83
96
|
} from './types.ts'
|
|
@@ -321,6 +334,8 @@ export class Runtime {
|
|
|
321
334
|
private aiToolsCache: AiTool[] | null = null
|
|
322
335
|
|
|
323
336
|
private readonly actions = new Map<string, ActionDef>()
|
|
337
|
+
/** A action dona de cada tipo de recurso com assistente (ADR 0053). */
|
|
338
|
+
private readonly assistantOwners = new Map<string, ActionDef & { assistant: ActionAssistant }>()
|
|
324
339
|
private readonly reactions = new Map<string, ReactionDef<any>>()
|
|
325
340
|
private readonly schedules = new Map<string, ScheduleDef>()
|
|
326
341
|
private readonly dataProducts = new Map<string, DataProductConfig>()
|
|
@@ -532,11 +547,34 @@ export class Runtime {
|
|
|
532
547
|
'Use simple ou form para escrever (ADR 0041).',
|
|
533
548
|
})
|
|
534
549
|
}
|
|
550
|
+
this.claimAssistant(action)
|
|
551
|
+
if (action.kind === 'list') assertValidListGrouping(action)
|
|
535
552
|
this.actions.set(action.name, action)
|
|
536
553
|
if (this.config.dev) this.warnIgnoredExecutionFields(action)
|
|
537
554
|
}
|
|
538
555
|
}
|
|
539
556
|
|
|
557
|
+
/**
|
|
558
|
+
* Valida a declaração do assistente e reserva o tipo de recurso para a action (ADR 0053). Duas
|
|
559
|
+
* actions donas do mesmo recurso são recusadas com `runtime.assistant_duplicate_resource`.
|
|
560
|
+
*/
|
|
561
|
+
private claimAssistant(action: ActionDef): void {
|
|
562
|
+
assertAssistantDeclaration(action)
|
|
563
|
+
const assistant = (action as { assistant?: ActionAssistant }).assistant
|
|
564
|
+
if (assistant === undefined) return
|
|
565
|
+
const owner = this.assistantOwners.get(assistant.resource)
|
|
566
|
+
if (owner !== undefined) {
|
|
567
|
+
throw error({
|
|
568
|
+
code: 'runtime.assistant_duplicate_resource',
|
|
569
|
+
category: 'internal',
|
|
570
|
+
message:
|
|
571
|
+
`O recurso "${assistant.resource}" já tem a action dona "${owner.name}"; ` +
|
|
572
|
+
`"${action.name}" não pode declarar o mesmo resource (ADR 0053).`,
|
|
573
|
+
})
|
|
574
|
+
}
|
|
575
|
+
this.assistantOwners.set(assistant.resource, action as ActionDef & { assistant: ActionAssistant })
|
|
576
|
+
}
|
|
577
|
+
|
|
540
578
|
/**
|
|
541
579
|
* `emits` e `background` pertencem ao binding de qualquer kind pelo tipo, mas o runtime e o
|
|
542
580
|
* manifest só os aplicam em `simple` e `form`. Restringir o tipo quebraria bindings
|
|
@@ -726,6 +764,103 @@ export class Runtime {
|
|
|
726
764
|
return this.executeAction(action, input, ctxBase, 'request')
|
|
727
765
|
}
|
|
728
766
|
|
|
767
|
+
/**
|
|
768
|
+
* Lê um recurso pela action dona do tipo (ADR 0053). Executa a action com `{ id }` pelo mesmo
|
|
769
|
+
* `execute` de qualquer chamada: autorização, loaders, auditoria, marca de dado pessoal e span são
|
|
770
|
+
* os da action. Em caso de sucesso, devolve o descritor e, com `{ context: true }`, a projeção
|
|
771
|
+
* limitada que o host pode entregar ao modelo. Em caso de falha, devolve o erro da action. Um tipo
|
|
772
|
+
* sem action dona devolve `runtime.assistant_resource_unknown`, sem executar nada.
|
|
773
|
+
*/
|
|
774
|
+
async readResource(
|
|
775
|
+
resource: string,
|
|
776
|
+
id: string,
|
|
777
|
+
ctxBase: ContextBase,
|
|
778
|
+
options: ReadResourceOptions = {},
|
|
779
|
+
): Promise<ResourceReadResult> {
|
|
780
|
+
const owner = this.assistantOwners.get(resource)
|
|
781
|
+
if (owner === undefined) {
|
|
782
|
+
return {
|
|
783
|
+
ok: false,
|
|
784
|
+
error: error({
|
|
785
|
+
code: 'runtime.assistant_resource_unknown',
|
|
786
|
+
category: 'validation',
|
|
787
|
+
// O tipo veio de fora: a mensagem é fixa, e o valor recebido fica em `meta`.
|
|
788
|
+
message: 'O tipo de recurso informado não tem action dona.',
|
|
789
|
+
meta: { resource },
|
|
790
|
+
}),
|
|
791
|
+
}
|
|
792
|
+
}
|
|
793
|
+
const result = await this.execute(owner.name, { id }, ctxBase)
|
|
794
|
+
if (!result.ok) return { ok: false, error: result.error, meta: result.meta }
|
|
795
|
+
|
|
796
|
+
const { assistant } = owner
|
|
797
|
+
let title: string | null
|
|
798
|
+
let omitted: AssistantOmission[]
|
|
799
|
+
let context: ResourceContext | undefined
|
|
800
|
+
try {
|
|
801
|
+
if (options.context === true) {
|
|
802
|
+
// A projeção já resolve o título; sem contexto pedido, basta o título, sem pagar o corte.
|
|
803
|
+
const projection = projectAssistantContext(assistant, result.data)
|
|
804
|
+
title = projection.title
|
|
805
|
+
omitted = projection.omitted
|
|
806
|
+
context = {
|
|
807
|
+
data: projection.data,
|
|
808
|
+
truncated: projection.truncated,
|
|
809
|
+
omitted: projection.omitted,
|
|
810
|
+
personalData: owner.personalData === true,
|
|
811
|
+
}
|
|
812
|
+
} else {
|
|
813
|
+
;({ title, omitted } = resolveAssistantTitle(assistant, result.data))
|
|
814
|
+
}
|
|
815
|
+
} catch (thrown) {
|
|
816
|
+
// A action executou e foi auditada; o que falhou foi transformar a saída em dado ao modelo
|
|
817
|
+
// (por exemplo, `BigInt` ou referência circular, que o JSON não representa).
|
|
818
|
+
this.log.error('A saída da action dona não pôde ser projetada para o assistente.', {
|
|
819
|
+
code: 'runtime.assistant_projection_failed',
|
|
820
|
+
action: owner.name,
|
|
821
|
+
resource,
|
|
822
|
+
error: thrown instanceof Error ? thrown.message : String(thrown),
|
|
823
|
+
})
|
|
824
|
+
return {
|
|
825
|
+
ok: false,
|
|
826
|
+
error: error({
|
|
827
|
+
code: 'runtime.assistant_projection_failed',
|
|
828
|
+
category: 'internal',
|
|
829
|
+
message: 'Não foi possível preparar os dados deste recurso para o assistente.',
|
|
830
|
+
cause: thrown,
|
|
831
|
+
}),
|
|
832
|
+
meta: result.meta,
|
|
833
|
+
}
|
|
834
|
+
}
|
|
835
|
+
const missing = omitted.filter((entry) => entry.reason === 'missing').map((entry) => entry.path)
|
|
836
|
+
if (missing.length > 0) {
|
|
837
|
+
this.log.warn(
|
|
838
|
+
`A saída de "${owner.name}" não trouxe ${missing.map((path) => `\`${path}\``).join(', ')}, ` +
|
|
839
|
+
'declarado no assistente, como texto ou valor esperado; o recurso foi lido sem esses campos.',
|
|
840
|
+
{ code: 'runtime.assistant_path_missing', action: owner.name, resource, paths: missing },
|
|
841
|
+
)
|
|
842
|
+
}
|
|
843
|
+
return {
|
|
844
|
+
ok: true,
|
|
845
|
+
resource: { kind: resource, id, title, label: assistant.label, skill: assistant.skill },
|
|
846
|
+
...(context !== undefined ? { context } : {}),
|
|
847
|
+
meta: result.meta,
|
|
848
|
+
}
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
/**
|
|
852
|
+
* As declarações de assistente registradas, com a action dona de cada uma (ADR 0053). Serve ao
|
|
853
|
+
* consumidor para montar rotas e telas a partir da declaração e para conferir as chaves de
|
|
854
|
+
* procedimento contra o registro que ele mantém.
|
|
855
|
+
*/
|
|
856
|
+
assistants(): RegisteredAssistant[] {
|
|
857
|
+
return [...this.assistantOwners.values()].map((owner) => ({
|
|
858
|
+
action: owner.name,
|
|
859
|
+
...publicAssistant(owner.assistant),
|
|
860
|
+
personalData: owner.personalData === true,
|
|
861
|
+
}))
|
|
862
|
+
}
|
|
863
|
+
|
|
729
864
|
/**
|
|
730
865
|
* Executa no worker um envelope criado por `execute()`. O consumer fornece
|
|
731
866
|
* auth/can reidratados; identidade, provenance e trace vêm do envelope.
|
|
@@ -1026,6 +1161,9 @@ export class Runtime {
|
|
|
1026
1161
|
metadata: {
|
|
1027
1162
|
...(effect !== undefined ? { effect } : {}),
|
|
1028
1163
|
...(action.personalData !== undefined ? { personalData: action.personalData } : {}),
|
|
1164
|
+
...(action.kind === 'view' && action.assistant !== undefined
|
|
1165
|
+
? { assistant: publicAssistant(action.assistant) }
|
|
1166
|
+
: {}),
|
|
1029
1167
|
dataProducts,
|
|
1030
1168
|
dataProductLabels: Object.fromEntries(
|
|
1031
1169
|
dataProducts.map((id) => [id, this.dataProducts.get(id)?.label ?? id]),
|
package/src/core/types.ts
CHANGED
|
@@ -21,6 +21,7 @@
|
|
|
21
21
|
* 13. RuntimeConfig (§14)
|
|
22
22
|
*/
|
|
23
23
|
|
|
24
|
+
import type { AssistantDeclaration } from './assistant.ts'
|
|
24
25
|
import type { SoftDeleteEntity } from './retention.ts'
|
|
25
26
|
|
|
26
27
|
// =============================================================================
|
|
@@ -548,6 +549,30 @@ export interface AIConfig {
|
|
|
548
549
|
requiresConfirmation?: boolean
|
|
549
550
|
}
|
|
550
551
|
|
|
552
|
+
/**
|
|
553
|
+
* O assistente do recurso que uma action `view` lê (ADR 0053). A action que o declara é a action
|
|
554
|
+
* dona do tipo de recurso: é por ela que o recurso é lido e que o acesso a ele é conferido. A
|
|
555
|
+
* declaração é serializável e não carrega função, consulta nem regra de acesso.
|
|
556
|
+
*/
|
|
557
|
+
export interface ActionAssistant {
|
|
558
|
+
/** O tipo de recurso de que a action é dona, em camelCase e único no runtime (`customer`). */
|
|
559
|
+
resource: string
|
|
560
|
+
/** A chave do procedimento que a IA segue ao tratar do recurso, em minúsculas com hífen. */
|
|
561
|
+
skill: string
|
|
562
|
+
/** O nome do assistente para pessoas (`Assistente do cliente`). */
|
|
563
|
+
label: string
|
|
564
|
+
/** O caminho, na saída, do campo que nomeia o recurso para pessoas (`name`). */
|
|
565
|
+
title: string
|
|
566
|
+
/**
|
|
567
|
+
* Os caminhos, na saída, dos campos que o host pode entregar ao modelo como dado do recurso. O
|
|
568
|
+
* ponto desce em objetos (`leads.totalCount`); um caminho que passa por uma lista se aplica a cada
|
|
569
|
+
* item.
|
|
570
|
+
*/
|
|
571
|
+
context: readonly string[]
|
|
572
|
+
/** Teto, em caracteres, do JSON de `context`. Padrão de 4.000, mínimo de 1.000, máximo de 16.000. */
|
|
573
|
+
maxContextChars?: number
|
|
574
|
+
}
|
|
575
|
+
|
|
551
576
|
export interface Example {
|
|
552
577
|
name: string
|
|
553
578
|
description?: string
|
|
@@ -899,6 +924,9 @@ export interface SearchFilters {
|
|
|
899
924
|
paginate?: 'cursor' | 'offset' | false
|
|
900
925
|
text?: { fields: string[]; placeholder?: I18nRef }
|
|
901
926
|
periods?: PeriodSpec[]
|
|
927
|
+
/** Dimensões fechadas que a ActionList pode usar para agrupar a tabela. Cada nome
|
|
928
|
+
* referencia um filtro select com opções static ou dictionary. */
|
|
929
|
+
grouping?: { fields: string[] }
|
|
902
930
|
/**
|
|
903
931
|
* Cláusula DSL aplicada **sempre** pelo repository do consumer (ex:
|
|
904
932
|
* `'NOT archived'`). Runtime NÃO executa SQL — quem consome a search
|
|
@@ -931,6 +959,8 @@ export interface ListAction<In, Out, ParsedIn = In>
|
|
|
931
959
|
paginate?: 'cursor' | 'offset' | false
|
|
932
960
|
text?: { fields: string[]; placeholder?: I18nRef }
|
|
933
961
|
periods?: PeriodSpec[]
|
|
962
|
+
/** Dimensões fechadas que a ActionList pode usar para agrupar a tabela. */
|
|
963
|
+
grouping?: { fields: string[] }
|
|
934
964
|
/**
|
|
935
965
|
* Ver `SearchFilters.where`. Baseline DSL aplicado pelo repository
|
|
936
966
|
* via `compileWhere` (do `@softize/opus/dsl`). Runtime não executa SQL.
|
|
@@ -943,6 +973,12 @@ export interface ViewAction<In, Out, ParsedIn = In> extends ActionBase<In, Out,
|
|
|
943
973
|
/** Entidades relacionadas projetáveis. View simples não projeta nada — omita. */
|
|
944
974
|
projection?: string[]
|
|
945
975
|
expand?: Record<string, ExpandSpec>
|
|
976
|
+
/**
|
|
977
|
+
* Torna esta action a dona de um tipo de recurso e declara o assistente dele (ADR 0053). Exige
|
|
978
|
+
* `ai` habilitado, `personalData` declarado e uma entrada em que `id`, do tipo texto, é o único
|
|
979
|
+
* campo obrigatório. O runtime lê o recurso por `runtime.readResource`.
|
|
980
|
+
*/
|
|
981
|
+
assistant?: ActionAssistant
|
|
946
982
|
}
|
|
947
983
|
|
|
948
984
|
/**
|
|
@@ -1341,6 +1377,9 @@ export interface AiTool {
|
|
|
1341
1377
|
effect?: ActionEffect
|
|
1342
1378
|
/** A action declara devolver dado pessoal; ausente quando não declarou. */
|
|
1343
1379
|
personalData?: boolean
|
|
1380
|
+
/** O assistente do recurso de que a action é dona (ADR 0053), com o teto resolvido; ausente
|
|
1381
|
+
* quando não declara. */
|
|
1382
|
+
assistant?: AssistantDeclaration
|
|
1344
1383
|
}
|
|
1345
1384
|
}
|
|
1346
1385
|
|
package/src/mcp/index.ts
CHANGED
|
@@ -58,6 +58,9 @@ export function createOpusMcpServer(runtime: Runtime, opts: OpusMcpOptions = {})
|
|
|
58
58
|
...(t.metadata?.personalData !== undefined
|
|
59
59
|
? { 'com.softize.opus/personal-data': t.metadata.personalData }
|
|
60
60
|
: {}),
|
|
61
|
+
// O assistente do recurso de que a action é dona (ADR 0053): o host conhece o assistente
|
|
62
|
+
// pelo catálogo que já consulta.
|
|
63
|
+
...(t.metadata?.assistant !== undefined ? { 'com.softize.opus/assistant': t.metadata.assistant } : {}),
|
|
61
64
|
},
|
|
62
65
|
})),
|
|
63
66
|
}))
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { useQueries } from '@tanstack/react-query'
|
|
2
|
+
import { useRef } from 'react'
|
|
3
|
+
import type { ActionDef, ActionError, Paginated } from '../../../core/index.ts'
|
|
4
|
+
import { useClient } from '../../drivers/context.tsx'
|
|
5
|
+
|
|
6
|
+
export interface ListActionGroup<TItem> {
|
|
7
|
+
value: string
|
|
8
|
+
items: TItem[]
|
|
9
|
+
total: number | undefined
|
|
10
|
+
error: ActionError | undefined
|
|
11
|
+
isLoading: boolean
|
|
12
|
+
isFetching: boolean
|
|
13
|
+
isError: boolean
|
|
14
|
+
refetch: () => Promise<void>
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** Implementação interna do ActionList para executar um recorte paginado por grupo. */
|
|
18
|
+
export function useGroupedListAction<TItem = unknown, TInput extends Record<string, unknown> = Record<string, unknown>>(
|
|
19
|
+
action: { name: string; kind: 'list' },
|
|
20
|
+
input: TInput,
|
|
21
|
+
field: string | null,
|
|
22
|
+
values: string[],
|
|
23
|
+
multiple: boolean,
|
|
24
|
+
pages: Record<string, number>,
|
|
25
|
+
pageSize: number,
|
|
26
|
+
): ListActionGroup<TItem>[] {
|
|
27
|
+
const client = useClient('ActionList grouping')
|
|
28
|
+
const previous = useRef<{
|
|
29
|
+
field: string | null
|
|
30
|
+
groups: Map<string, { items: TItem[]; total: number }>
|
|
31
|
+
}>({ field, groups: new Map() })
|
|
32
|
+
if (previous.current.field !== field) {
|
|
33
|
+
previous.current = { field, groups: new Map() }
|
|
34
|
+
}
|
|
35
|
+
const groupField = field ?? '__disabled_group__'
|
|
36
|
+
const requests =
|
|
37
|
+
field === null
|
|
38
|
+
? []
|
|
39
|
+
: values.flatMap((value) =>
|
|
40
|
+
Array.from({ length: pages[value] ?? 1 }, (_, index) => ({
|
|
41
|
+
value,
|
|
42
|
+
page: index + 1,
|
|
43
|
+
})),
|
|
44
|
+
)
|
|
45
|
+
const queries = useQueries({
|
|
46
|
+
queries: requests.map(({ value, page }) => {
|
|
47
|
+
const groupInput = {
|
|
48
|
+
...input,
|
|
49
|
+
[groupField]: multiple ? [value] : value,
|
|
50
|
+
page,
|
|
51
|
+
limit: pageSize,
|
|
52
|
+
}
|
|
53
|
+
return {
|
|
54
|
+
queryKey: [action.name, groupInput],
|
|
55
|
+
queryFn: async (): Promise<Paginated<TItem>> => {
|
|
56
|
+
const result = await client.run<Paginated<TItem>>(action as unknown as ActionDef, groupInput)
|
|
57
|
+
if (!result.ok) throw result.error
|
|
58
|
+
return result.data
|
|
59
|
+
},
|
|
60
|
+
}
|
|
61
|
+
}),
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
return values.map((value) => {
|
|
65
|
+
const groupQueries = queries.filter((_, index) => requests[index]?.value === value)
|
|
66
|
+
const items = groupQueries.flatMap((query) => query.data?.items ?? [])
|
|
67
|
+
const total = groupQueries.find((query) => query.data?.total !== undefined)?.data?.total
|
|
68
|
+
const isError = groupQueries.some((query) => query.isError)
|
|
69
|
+
const hasCompleteData = groupQueries.length > 0 && groupQueries.every((query) => query.data !== undefined)
|
|
70
|
+
if (hasCompleteData && !isError && total !== undefined) {
|
|
71
|
+
previous.current.groups.set(value, { items, total })
|
|
72
|
+
}
|
|
73
|
+
const fallback = previous.current.groups.get(value)
|
|
74
|
+
const hasCurrentData = groupQueries.some((query) => query.data !== undefined)
|
|
75
|
+
return {
|
|
76
|
+
value,
|
|
77
|
+
items: hasCurrentData ? items : (fallback?.items ?? []),
|
|
78
|
+
total: hasCurrentData ? total : fallback?.total,
|
|
79
|
+
error: (groupQueries.find((query) => query.error !== null)?.error as unknown as ActionError | null) ?? undefined,
|
|
80
|
+
isLoading: (groupQueries[0]?.isPending ?? true) && fallback === undefined,
|
|
81
|
+
isFetching: groupQueries.some((query) => query.isFetching),
|
|
82
|
+
isError,
|
|
83
|
+
refetch: async () => {
|
|
84
|
+
await Promise.all(groupQueries.map((query) => query.refetch()))
|
|
85
|
+
},
|
|
86
|
+
}
|
|
87
|
+
})
|
|
88
|
+
}
|