@digimuza/system-core 0.2.0 → 0.3.0-rc.1
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/INTEGRATION.md +209 -3
- package/README.md +23 -0
- package/dist/ai/api.d.ts +120 -0
- package/dist/ai/api.d.ts.map +1 -0
- package/dist/ai/api.js +446 -0
- package/dist/ai/api.js.map +1 -0
- package/dist/ai/card.d.ts +27 -0
- package/dist/ai/card.d.ts.map +1 -0
- package/dist/ai/card.js +84 -0
- package/dist/ai/card.js.map +1 -0
- package/dist/ai/contract.d.ts +364 -0
- package/dist/ai/contract.d.ts.map +1 -0
- package/dist/ai/contract.js +101 -0
- package/dist/ai/contract.js.map +1 -0
- package/dist/ai/display.d.ts +20 -0
- package/dist/ai/display.d.ts.map +1 -0
- package/dist/ai/display.js +35 -0
- package/dist/ai/display.js.map +1 -0
- package/dist/ai/index.d.ts +7 -0
- package/dist/ai/index.d.ts.map +1 -0
- package/dist/ai/index.js +10 -0
- package/dist/ai/index.js.map +1 -0
- package/dist/ai/manifest.d.ts +25 -0
- package/dist/ai/manifest.d.ts.map +1 -0
- package/dist/ai/manifest.js +15 -0
- package/dist/ai/manifest.js.map +1 -0
- package/dist/ai/runner.d.ts +71 -0
- package/dist/ai/runner.d.ts.map +1 -0
- package/dist/ai/runner.js +246 -0
- package/dist/ai/runner.js.map +1 -0
- package/dist/ai/tools.d.ts +42 -0
- package/dist/ai/tools.d.ts.map +1 -0
- package/dist/ai/tools.js +117 -0
- package/dist/ai/tools.js.map +1 -0
- package/dist/audit/csv.d.ts +31 -0
- package/dist/audit/csv.d.ts.map +1 -0
- package/dist/audit/csv.js +39 -0
- package/dist/audit/csv.js.map +1 -0
- package/dist/audit/index.d.ts +5 -0
- package/dist/audit/index.d.ts.map +1 -0
- package/dist/audit/index.js +6 -0
- package/dist/audit/index.js.map +1 -0
- package/dist/convex/functions.d.ts +39 -0
- package/dist/convex/functions.d.ts.map +1 -1
- package/dist/convex/functions.js +38 -6
- package/dist/convex/functions.js.map +1 -1
- package/dist/convex/index.d.ts +1 -1
- package/dist/convex/index.d.ts.map +1 -1
- package/dist/convex/index.js.map +1 -1
- package/dist/core/access.d.ts +14 -0
- package/dist/core/access.d.ts.map +1 -1
- package/dist/core/access.js +62 -0
- package/dist/core/access.js.map +1 -1
- package/dist/core/audit-module.d.ts +5 -0
- package/dist/core/audit-module.d.ts.map +1 -0
- package/dist/core/audit-module.js +25 -0
- package/dist/core/audit-module.js.map +1 -0
- package/dist/core/audit.d.ts +31 -0
- package/dist/core/audit.d.ts.map +1 -1
- package/dist/core/audit.js +46 -2
- package/dist/core/audit.js.map +1 -1
- package/dist/core/catalog.d.ts +41 -0
- package/dist/core/catalog.d.ts.map +1 -1
- package/dist/core/catalog.js +45 -0
- package/dist/core/catalog.js.map +1 -1
- package/dist/core/context.d.ts +8 -2
- package/dist/core/context.d.ts.map +1 -1
- package/dist/core/context.js +4 -1
- package/dist/core/context.js.map +1 -1
- package/dist/core/index.d.ts +2 -0
- package/dist/core/index.d.ts.map +1 -1
- package/dist/core/index.js +2 -0
- package/dist/core/index.js.map +1 -1
- package/dist/core/notify.d.ts +37 -0
- package/dist/core/notify.d.ts.map +1 -0
- package/dist/core/notify.js +16 -0
- package/dist/core/notify.js.map +1 -0
- package/dist/core/roles.d.ts.map +1 -1
- package/dist/core/roles.js +5 -1
- package/dist/core/roles.js.map +1 -1
- package/dist/core/system-modules.d.ts +4 -0
- package/dist/core/system-modules.d.ts.map +1 -1
- package/dist/core/system-modules.js +11 -0
- package/dist/core/system-modules.js.map +1 -1
- package/dist/finance/ai-tools.d.ts +55 -0
- package/dist/finance/ai-tools.d.ts.map +1 -0
- package/dist/finance/ai-tools.js +65 -0
- package/dist/finance/ai-tools.js.map +1 -0
- package/dist/finance/index.d.ts +87 -1
- package/dist/finance/index.d.ts.map +1 -1
- package/dist/finance/index.js +92 -38
- package/dist/finance/index.js.map +1 -1
- package/dist/finance/search.d.ts +15 -0
- package/dist/finance/search.d.ts.map +1 -0
- package/dist/finance/search.js +51 -0
- package/dist/finance/search.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/modules/index.d.ts +1 -1
- package/dist/modules/index.d.ts.map +1 -1
- package/dist/modules/index.js +1 -1
- package/dist/modules/index.js.map +1 -1
- package/dist/notifications/api.d.ts +130 -0
- package/dist/notifications/api.d.ts.map +1 -0
- package/dist/notifications/api.js +384 -0
- package/dist/notifications/api.js.map +1 -0
- package/dist/notifications/contract.d.ts +132 -0
- package/dist/notifications/contract.d.ts.map +1 -0
- package/dist/notifications/contract.js +59 -0
- package/dist/notifications/contract.js.map +1 -0
- package/dist/notifications/index.d.ts +6 -0
- package/dist/notifications/index.d.ts.map +1 -0
- package/dist/notifications/index.js +9 -0
- package/dist/notifications/index.js.map +1 -0
- package/dist/search/index.d.ts +95 -0
- package/dist/search/index.d.ts.map +1 -0
- package/dist/search/index.js +151 -0
- package/dist/search/index.js.map +1 -0
- package/package.json +41 -2
- package/src/ai/api.ts +535 -0
- package/src/ai/card.ts +91 -0
- package/src/ai/contract.ts +245 -0
- package/src/ai/display.ts +38 -0
- package/src/ai/index.ts +9 -0
- package/src/ai/manifest.ts +16 -0
- package/src/ai/runner.ts +275 -0
- package/src/ai/tools.ts +128 -0
- package/src/audit/csv.ts +60 -0
- package/src/audit/index.ts +7 -0
- package/src/convex/functions.ts +54 -7
- package/src/convex/index.ts +1 -1
- package/src/core/access.ts +55 -1
- package/src/core/audit-module.ts +26 -0
- package/src/core/audit.ts +68 -2
- package/src/core/catalog.ts +63 -0
- package/src/core/context.ts +13 -2
- package/src/core/index.ts +2 -0
- package/src/core/notify.ts +52 -0
- package/src/core/roles.ts +5 -1
- package/src/core/system-modules.ts +12 -0
- package/src/finance/ai-tools.ts +68 -0
- package/src/finance/index.ts +119 -42
- package/src/finance/search.ts +58 -0
- package/src/index.ts +2 -0
- package/src/modules/index.ts +3 -0
- package/src/notifications/api.ts +477 -0
- package/src/notifications/contract.ts +142 -0
- package/src/notifications/index.ts +8 -0
- package/src/search/index.ts +198 -0
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
// `@digimuza/system-core/search` (DIG-72): one global search over every
|
|
2
|
+
// installed module. Each module contributes a provider guarded by one of its
|
|
3
|
+
// own permissions; the query only runs the providers the caller may use now.
|
|
4
|
+
//
|
|
5
|
+
// // convex/search.ts
|
|
6
|
+
// import { defineSearch } from "@digimuza/system-core/search"
|
|
7
|
+
// import { financeSearch } from "@digimuza/system-core/finance"
|
|
8
|
+
// export const { search } = defineSearch(system, { providers: [financeSearch] }).functions
|
|
9
|
+
import { queryGeneric } from "convex/server"
|
|
10
|
+
import { v } from "convex/values"
|
|
11
|
+
import { decide, isModuleEnabled, isSuperActor, requireHuman, type Actor } from "../core/access.js"
|
|
12
|
+
import { MODULE_ID, PERMISSION_ID } from "../core/catalog.js"
|
|
13
|
+
import type { SystemContext } from "../core/context.js"
|
|
14
|
+
import { validation } from "../core/errors.js"
|
|
15
|
+
import { toConvexError, type ConvexSystem } from "../convex/functions.js"
|
|
16
|
+
|
|
17
|
+
export const SEARCH_QUERY_MIN = 2
|
|
18
|
+
export const SEARCH_QUERY_MAX = 100
|
|
19
|
+
/** Hits per provider when the caller passes no `limit`. */
|
|
20
|
+
export const SEARCH_DEFAULT_LIMIT = 5
|
|
21
|
+
/** Largest `limit` (hits per provider). */
|
|
22
|
+
export const SEARCH_PROVIDER_MAX = 20
|
|
23
|
+
/** Hits across all providers. */
|
|
24
|
+
export const SEARCH_TOTAL_MAX = 50
|
|
25
|
+
|
|
26
|
+
/** `module.name`, e.g. `finance.invoices`. */
|
|
27
|
+
export const SEARCH_PROVIDER_ID = /^[a-z][a-z0-9-]*\.[a-z][a-z0-9_-]*$/
|
|
28
|
+
|
|
29
|
+
export interface SearchHit {
|
|
30
|
+
/** Set by the search to the provider's id. */
|
|
31
|
+
providerId: string
|
|
32
|
+
/** Stable id of the hit within the provider (e.g. the document id). */
|
|
33
|
+
id: string
|
|
34
|
+
title: string
|
|
35
|
+
subtitle?: string
|
|
36
|
+
/** App-relative link, `/app/...`. Hits with any other href are dropped. */
|
|
37
|
+
href: string
|
|
38
|
+
/** Optional kind for an icon (e.g. `invoice`). */
|
|
39
|
+
kind?: string
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** What a provider's `search` gets. Runs inside the search query (read-only). */
|
|
43
|
+
export interface SearchContext {
|
|
44
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
45
|
+
db: any
|
|
46
|
+
sys: SystemContext
|
|
47
|
+
actor: Actor
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export interface SearchProvider {
|
|
51
|
+
/** Stable `module.name`, starts with `${moduleId}.`. */
|
|
52
|
+
id: string
|
|
53
|
+
/** Owning module: the provider is skipped while it is disabled (except for an effective Super). */
|
|
54
|
+
moduleId: string
|
|
55
|
+
/** Fallback group label; the app translates by `id`. */
|
|
56
|
+
label: string
|
|
57
|
+
/** The owning module's permission the caller must hold now (checked with `decide`). */
|
|
58
|
+
permission: string
|
|
59
|
+
/** Trimmed, validated query; return at most `limit` hits, best first. */
|
|
60
|
+
search(ctx: SearchContext, input: { query: string; limit: number }): Promise<Omit<SearchHit, "providerId">[] | SearchHit[]>
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export interface SearchGroup {
|
|
64
|
+
providerId: string
|
|
65
|
+
moduleId: string
|
|
66
|
+
label: string
|
|
67
|
+
hits: SearchHit[]
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export interface SearchResult {
|
|
71
|
+
/** The trimmed query. */
|
|
72
|
+
query: string
|
|
73
|
+
/** Providers with at least one hit, in the order they were installed. */
|
|
74
|
+
groups: SearchGroup[]
|
|
75
|
+
/** Hits across all groups (≤ SEARCH_TOTAL_MAX). */
|
|
76
|
+
total: number
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** Declares a provider. Identity at runtime; validates ids and the permission format. */
|
|
80
|
+
export function defineSearchProvider<const P extends SearchProvider>(provider: P): P {
|
|
81
|
+
const id = typeof provider?.id === "string" ? provider.id : ""
|
|
82
|
+
if (!SEARCH_PROVIDER_ID.test(id)) throw new Error(`defineSearchProvider: invalid id "${id}" (expected module.name, e.g. finance.invoices)`)
|
|
83
|
+
if (typeof provider.moduleId !== "string" || !MODULE_ID.test(provider.moduleId)) throw new Error(`defineSearchProvider(${id}): invalid moduleId`)
|
|
84
|
+
if (!id.startsWith(`${provider.moduleId}.`)) throw new Error(`defineSearchProvider(${id}): id must start with "${provider.moduleId}."`)
|
|
85
|
+
if (typeof provider.permission !== "string" || !PERMISSION_ID.test(provider.permission)) throw new Error(`defineSearchProvider(${id}): invalid permission`)
|
|
86
|
+
if (typeof provider.label !== "string" || !provider.label.trim()) throw new Error(`defineSearchProvider(${id}): label is required`)
|
|
87
|
+
if (typeof provider.search !== "function") throw new Error(`defineSearchProvider(${id}): search must be a function`)
|
|
88
|
+
return provider
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const SAFE_HREF = /^\/(?!\/)[^\s\\]*$/
|
|
92
|
+
const clip = (s: string, n: number) => (s.length > n ? `${s.slice(0, n - 1)}…` : s)
|
|
93
|
+
|
|
94
|
+
/** Keeps well-formed hits with a safe, app-relative href. */
|
|
95
|
+
function cleanHit(providerId: string, raw: unknown): SearchHit | null {
|
|
96
|
+
if (!raw || typeof raw !== "object") return null
|
|
97
|
+
const h = raw as Record<string, unknown>
|
|
98
|
+
if (typeof h.id !== "string" || !h.id || typeof h.title !== "string" || !h.title.trim()) return null
|
|
99
|
+
if (typeof h.href !== "string" || h.href.length > 500 || !SAFE_HREF.test(h.href)) return null
|
|
100
|
+
const hit: SearchHit = { providerId, id: h.id, title: clip(h.title.trim(), 200), href: h.href }
|
|
101
|
+
if (typeof h.subtitle === "string" && h.subtitle.trim()) hit.subtitle = clip(h.subtitle.trim(), 300)
|
|
102
|
+
if (typeof h.kind === "string" && h.kind) hit.kind = clip(h.kind, 40)
|
|
103
|
+
return hit
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** Trims and checks the query; throws VALIDATION (`fields: ["query"]`). */
|
|
107
|
+
export function normalizeSearchQuery(query: string): string {
|
|
108
|
+
const q = query.replace(/\s+/g, " ").trim()
|
|
109
|
+
if (q.length < SEARCH_QUERY_MIN) throw validation("query_too_short", ["query"])
|
|
110
|
+
if (q.length > SEARCH_QUERY_MAX) throw validation("query_too_long", ["query"])
|
|
111
|
+
return q
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/** Runs the providers the actor may use. No audit: it is a read. */
|
|
115
|
+
export async function runSearch(
|
|
116
|
+
ctx: { db: unknown },
|
|
117
|
+
sys: SystemContext,
|
|
118
|
+
actor: Actor,
|
|
119
|
+
providers: readonly SearchProvider[],
|
|
120
|
+
input: { query: string; limit?: number | undefined },
|
|
121
|
+
): Promise<SearchResult> {
|
|
122
|
+
const query = normalizeSearchQuery(input.query)
|
|
123
|
+
const perProvider = Math.min(Math.max(1, Math.floor(input.limit ?? SEARCH_DEFAULT_LIMIT)), SEARCH_PROVIDER_MAX)
|
|
124
|
+
const isSuper = await isSuperActor(sys, actor)
|
|
125
|
+
const groups: SearchGroup[] = []
|
|
126
|
+
let total = 0
|
|
127
|
+
for (const provider of providers) {
|
|
128
|
+
if (total >= SEARCH_TOTAL_MAX) break
|
|
129
|
+
if (!isSuper && !isModuleEnabled(sys, await sys.store.get("modules", provider.moduleId))) continue
|
|
130
|
+
if (!(await decide(sys, actor, provider.permission)).allowed) continue
|
|
131
|
+
const limit = Math.min(perProvider, SEARCH_TOTAL_MAX - total)
|
|
132
|
+
let raw: unknown[]
|
|
133
|
+
try {
|
|
134
|
+
raw = (await provider.search({ db: ctx.db, sys, actor }, { query, limit })) ?? []
|
|
135
|
+
} catch (error) {
|
|
136
|
+
// One failing provider never fails the whole search. The query is not logged.
|
|
137
|
+
sys.logger.error("search_provider_failed", { providerId: provider.id, error })
|
|
138
|
+
continue
|
|
139
|
+
}
|
|
140
|
+
const hits: SearchHit[] = []
|
|
141
|
+
const seen = new Set<string>()
|
|
142
|
+
for (const r of Array.isArray(raw) ? raw : []) {
|
|
143
|
+
const hit = cleanHit(provider.id, r)
|
|
144
|
+
if (!hit || seen.has(hit.id)) continue
|
|
145
|
+
seen.add(hit.id)
|
|
146
|
+
hits.push(hit)
|
|
147
|
+
if (hits.length >= limit) break
|
|
148
|
+
}
|
|
149
|
+
if (!hits.length) continue
|
|
150
|
+
groups.push({ providerId: provider.id, moduleId: provider.moduleId, label: provider.label, hits })
|
|
151
|
+
total += hits.length
|
|
152
|
+
}
|
|
153
|
+
return { query, groups, total }
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
export interface SearchOptions {
|
|
157
|
+
providers: readonly SearchProvider[]
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* The search's Convex functions, bound to the app's system:
|
|
162
|
+
* export const { search } = defineSearch(system, { providers: [financeSearch] }).functions
|
|
163
|
+
*/
|
|
164
|
+
export function defineSearch(system: ConvexSystem, options: SearchOptions) {
|
|
165
|
+
const catalog = system.definition.catalog
|
|
166
|
+
const ids = new Set<string>()
|
|
167
|
+
for (const p of options.providers) {
|
|
168
|
+
if (ids.has(p.id)) throw new Error(`defineSearch: duplicate provider id "${p.id}"`)
|
|
169
|
+
ids.add(p.id)
|
|
170
|
+
if (!catalog.modules.has(p.moduleId)) throw new Error(`defineSearch(${p.id}): module "${p.moduleId}" is not installed in this system`)
|
|
171
|
+
if (!catalog.permissions.has(p.permission)) throw new Error(`defineSearch(${p.id}): unknown permission "${p.permission}"`)
|
|
172
|
+
}
|
|
173
|
+
const providers = [...options.providers]
|
|
174
|
+
|
|
175
|
+
return {
|
|
176
|
+
functions: {
|
|
177
|
+
/**
|
|
178
|
+
* `({ query, limit? })` → SearchResult. Human sessions only (API keys: FORBIDDEN).
|
|
179
|
+
* `limit` is hits per provider (default 5, max 20); at most 50 in total.
|
|
180
|
+
* VALIDATION (`fields: ["query"]`) unless 2–100 characters after trimming.
|
|
181
|
+
*/
|
|
182
|
+
search: queryGeneric({
|
|
183
|
+
args: { query: v.string(), limit: v.optional(v.number()), apiKey: v.optional(v.string()), operationId: v.optional(v.string()) },
|
|
184
|
+
handler: async (ctx, a): Promise<SearchResult> => {
|
|
185
|
+
const { sys, actor } = await system.context(ctx, { apiKey: a.apiKey, operationId: a.operationId })
|
|
186
|
+
try {
|
|
187
|
+
const human = requireHuman(sys, actor)
|
|
188
|
+
return await runSearch(ctx, sys, human, providers, { query: a.query, limit: a.limit })
|
|
189
|
+
} catch (e) {
|
|
190
|
+
throw toConvexError(e, sys.operationId)
|
|
191
|
+
}
|
|
192
|
+
},
|
|
193
|
+
}),
|
|
194
|
+
},
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
export type SearchFunctions = ReturnType<typeof defineSearch>["functions"]
|