@fayz-ai/core 0.9.0 → 0.11.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/README.md +2 -2
- package/dist/access/index.js.map +1 -1
- package/dist/chunk-BEGXYHS4.js +566 -0
- package/dist/chunk-BEGXYHS4.js.map +1 -0
- package/dist/chunk-ED6NBH6U.js +348 -0
- package/dist/chunk-ED6NBH6U.js.map +1 -0
- package/dist/chunk-EWKX4VUM.js +429 -0
- package/dist/chunk-EWKX4VUM.js.map +1 -0
- package/dist/chunk-LARZWDHA.js +319 -0
- package/dist/chunk-LARZWDHA.js.map +1 -0
- package/dist/chunk-OXQGRJZT.js +125 -0
- package/dist/chunk-OXQGRJZT.js.map +1 -0
- package/dist/chunk-RRYFXUQX.js +1247 -0
- package/dist/chunk-RRYFXUQX.js.map +1 -0
- package/dist/{chunk-5RJVUC6J.js → chunk-TDI6F6PT.js} +373 -25
- package/dist/chunk-TDI6F6PT.js.map +1 -0
- package/dist/{chunk-V2MSAUHO.js → chunk-UVKH6RDX.js} +211 -335
- package/dist/chunk-UVKH6RDX.js.map +1 -0
- package/dist/chunk-V53GR4UT.js +289 -0
- package/dist/chunk-V53GR4UT.js.map +1 -0
- package/dist/connector-runtime/index.d.ts +15 -0
- package/dist/connector-runtime/index.d.ts.map +1 -0
- package/dist/connector-runtime/index.js +3 -0
- package/dist/connector-runtime/index.js.map +1 -0
- package/dist/data/archetype.d.ts.map +1 -1
- package/dist/data/backend-env.d.ts +38 -0
- package/dist/data/backend-env.d.ts.map +1 -0
- package/dist/data/bulk.d.ts +24 -0
- package/dist/data/bulk.d.ts.map +1 -1
- package/dist/data/cached.d.ts.map +1 -1
- package/dist/data/custom-fields.d.ts +5 -0
- package/dist/data/custom-fields.d.ts.map +1 -0
- package/dist/data/index.d.ts +2 -1
- package/dist/data/index.d.ts.map +1 -1
- package/dist/data/index.js +3 -1
- package/dist/data/mock.d.ts.map +1 -1
- package/dist/data/platform-api.d.ts.map +1 -1
- package/dist/data/resolve.d.ts.map +1 -1
- package/dist/data/supabase.d.ts.map +1 -1
- package/dist/data/types.d.ts +8 -0
- package/dist/data/types.d.ts.map +1 -1
- package/dist/entity/events.d.ts +14 -0
- package/dist/entity/events.d.ts.map +1 -0
- package/dist/events/envelope.d.ts +31 -0
- package/dist/events/envelope.d.ts.map +1 -0
- package/dist/events/index.d.ts +4 -0
- package/dist/events/index.d.ts.map +1 -1
- package/dist/events/log.d.ts +44 -0
- package/dist/events/log.d.ts.map +1 -0
- package/dist/events/types.d.ts +40 -0
- package/dist/events/types.d.ts.map +1 -0
- package/dist/fields/store.d.ts +45 -0
- package/dist/fields/store.d.ts.map +1 -0
- package/dist/i18n/index.d.ts +10 -0
- package/dist/i18n/index.d.ts.map +1 -1
- package/dist/i18n/index.js +1 -1
- package/dist/i18n/shell-translations.en.d.ts.map +1 -1
- package/dist/i18n/shell-translations.pt-br.d.ts.map +1 -1
- package/dist/index.d.ts +45 -21
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +543 -56
- package/dist/index.js.map +1 -1
- package/dist/integrations/events.d.ts +7 -0
- package/dist/integrations/events.d.ts.map +1 -0
- package/dist/integrations/index.d.ts +533 -29
- package/dist/integrations/index.d.ts.map +1 -1
- package/dist/integrations/index.js +3 -1
- package/dist/integrations/permissions.d.ts +132 -0
- package/dist/integrations/permissions.d.ts.map +1 -0
- package/dist/integrations/request.d.ts +170 -0
- package/dist/integrations/request.d.ts.map +1 -0
- package/dist/integrations/runtime-token.d.ts +33 -0
- package/dist/integrations/runtime-token.d.ts.map +1 -0
- package/dist/integrations/runtime.d.ts +398 -0
- package/dist/integrations/runtime.d.ts.map +1 -0
- package/dist/integrations/settings.d.ts +102 -0
- package/dist/integrations/settings.d.ts.map +1 -0
- package/dist/integrations/store.d.ts +145 -0
- package/dist/integrations/store.d.ts.map +1 -0
- package/dist/{address/index.d.ts → lib/address.d.ts} +1 -1
- package/dist/{address/index.d.ts.map → lib/address.d.ts.map} +1 -1
- package/dist/{phone/index.d.ts → lib/phone.d.ts} +1 -1
- package/dist/lib/phone.d.ts.map +1 -0
- package/dist/{seo/index.d.ts → lib/seo.d.ts} +18 -1
- package/dist/lib/seo.d.ts.map +1 -0
- package/dist/{tenant/index.d.ts → lib/tenant.d.ts} +1 -1
- package/dist/lib/tenant.d.ts.map +1 -0
- package/dist/manifest/index.d.ts +1 -1
- package/dist/manifest/index.d.ts.map +1 -1
- package/dist/messaging/index.d.ts +3 -0
- package/dist/messaging/index.d.ts.map +1 -0
- package/dist/messaging/registry.d.ts +35 -0
- package/dist/messaging/registry.d.ts.map +1 -0
- package/dist/messaging/types.d.ts +67 -0
- package/dist/messaging/types.d.ts.map +1 -0
- package/dist/plugin/index.js +3 -1
- package/dist/plugin/runtime.d.ts +17 -0
- package/dist/plugin/runtime.d.ts.map +1 -1
- package/dist/registry/block-catalog.d.ts +78 -0
- package/dist/registry/block-catalog.d.ts.map +1 -0
- package/dist/registry/block-constraints.d.ts +64 -0
- package/dist/registry/block-constraints.d.ts.map +1 -0
- package/dist/registry/block-schema.d.ts +69 -0
- package/dist/registry/block-schema.d.ts.map +1 -0
- package/dist/registry/blocks.d.ts +84 -0
- package/dist/registry/blocks.d.ts.map +1 -0
- package/dist/registry/entities.d.ts +179 -0
- package/dist/registry/entities.d.ts.map +1 -0
- package/dist/registry/extension-points.d.ts +117 -0
- package/dist/registry/extension-points.d.ts.map +1 -0
- package/dist/registry/handles.d.ts +41 -0
- package/dist/registry/handles.d.ts.map +1 -0
- package/dist/registry/index.d.ts +41 -41
- package/dist/registry/index.d.ts.map +1 -1
- package/dist/registry/loaders.d.ts +55 -0
- package/dist/registry/loaders.d.ts.map +1 -0
- package/dist/registry/registry-class.d.ts +157 -0
- package/dist/registry/registry-class.d.ts.map +1 -0
- package/dist/registry/schema-check.d.ts +14 -0
- package/dist/registry/schema-check.d.ts.map +1 -0
- package/dist/router/index.d.ts +78 -1
- package/dist/router/index.d.ts.map +1 -1
- package/dist/router/index.js +1 -1
- package/dist/{shell-translations.pt-br-2GWRQRRP.js → shell-translations.pt-br-BM5ZFSKJ.js} +72 -3
- package/dist/shell-translations.pt-br-BM5ZFSKJ.js.map +1 -0
- package/dist/telemetry/errors.d.ts +87 -0
- package/dist/telemetry/errors.d.ts.map +1 -0
- package/dist/testing/index.js +4 -1
- package/dist/{testing/index.d.ts → testing.d.ts} +5 -3
- package/dist/testing.d.ts.map +1 -0
- package/dist/types/analytics.d.ts +213 -0
- package/dist/types/analytics.d.ts.map +1 -0
- package/dist/types/crud.d.ts +37 -3
- package/dist/types/crud.d.ts.map +1 -1
- package/dist/types/plugins.d.ts +138 -1
- package/dist/types/plugins.d.ts.map +1 -1
- package/package.json +9 -17
- package/dist/blocks/index.d.ts +0 -25
- package/dist/blocks/index.d.ts.map +0 -1
- package/dist/chunk-5RJVUC6J.js.map +0 -1
- package/dist/chunk-L4TLMYGC.js +0 -42
- package/dist/chunk-L4TLMYGC.js.map +0 -1
- package/dist/chunk-V2MSAUHO.js.map +0 -1
- package/dist/chunk-X5THHG6T.js +0 -77
- package/dist/chunk-X5THHG6T.js.map +0 -1
- package/dist/chunk-Y2C2FBQP.js +0 -35
- package/dist/chunk-Y2C2FBQP.js.map +0 -1
- package/dist/chunk-YH25Y4FW.js +0 -311
- package/dist/chunk-YH25Y4FW.js.map +0 -1
- package/dist/phone/index.d.ts.map +0 -1
- package/dist/seo/index.d.ts.map +0 -1
- package/dist/shell-translations.pt-br-2GWRQRRP.js.map +0 -1
- package/dist/tenant/index.d.ts.map +0 -1
- package/dist/testing/index.d.ts.map +0 -1
- package/src/access/denial.ts +0 -33
- package/src/access/engine.test.ts +0 -48
- package/src/access/engine.ts +0 -136
- package/src/access/index.ts +0 -14
- package/src/access/limits.ts +0 -18
- package/src/address/index.ts +0 -188
- package/src/app/render.tsx +0 -69
- package/src/blocks/index.tsx +0 -68
- package/src/data/archetype.ts +0 -255
- package/src/data/bulk.ts +0 -35
- package/src/data/cached.ts +0 -55
- package/src/data/count.test.ts +0 -152
- package/src/data/count.ts +0 -131
- package/src/data/filters.test.ts +0 -70
- package/src/data/filters.ts +0 -90
- package/src/data/index.ts +0 -11
- package/src/data/mock.ts +0 -90
- package/src/data/platform-api.ts +0 -209
- package/src/data/refresh.ts +0 -115
- package/src/data/resolve.ts +0 -126
- package/src/data/supabase.ts +0 -230
- package/src/data/types.ts +0 -50
- package/src/entity/index.ts +0 -2
- package/src/entity/registry.ts +0 -49
- package/src/events/index.tsx +0 -82
- package/src/i18n/index.ts +0 -476
- package/src/i18n/shell-translations.en.ts +0 -875
- package/src/i18n/shell-translations.pt-br.ts +0 -873
- package/src/index.ts +0 -217
- package/src/integrations/index.ts +0 -237
- package/src/lib/cache.ts +0 -118
- package/src/lib/csv.ts +0 -54
- package/src/lib/format.ts +0 -58
- package/src/manifest/app-manifest.schema.json +0 -676
- package/src/manifest/index.ts +0 -518
- package/src/manifest/serialize.ts +0 -52
- package/src/phone/index.ts +0 -85
- package/src/plugin/createSafeDataProvider.ts +0 -30
- package/src/plugin/index.ts +0 -1
- package/src/plugin/runtime.ts +0 -331
- package/src/registry/index.ts +0 -163
- package/src/router/history.ts +0 -86
- package/src/router/index.ts +0 -29
- package/src/runtime/index.ts +0 -15
- package/src/runtime/oauth.ts +0 -219
- package/src/search/engine.ts +0 -559
- package/src/search/index.ts +0 -25
- package/src/search/text.ts +0 -206
- package/src/search/types.ts +0 -75
- package/src/seo/index.ts +0 -128
- package/src/tenant/index.ts +0 -38
- package/src/testing/index.ts +0 -116
- package/src/types/auth.ts +0 -41
- package/src/types/billing.ts +0 -39
- package/src/types/crud.ts +0 -184
- package/src/types/entities.ts +0 -142
- package/src/types/entitlements.ts +0 -45
- package/src/types/i18n.ts +0 -7
- package/src/types/index.ts +0 -11
- package/src/types/org.ts +0 -137
- package/src/types/payment.ts +0 -44
- package/src/types/permissions.ts +0 -32
- package/src/types/plugins.ts +0 -480
- package/src/types/theme.ts +0 -8
package/src/data/filters.test.ts
DELETED
|
@@ -1,70 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect } from 'vitest'
|
|
2
|
-
import { parseRangeFilter, isFilterObject, matchesRange, applyRangeToQuery } from './filters'
|
|
3
|
-
|
|
4
|
-
describe('parseRangeFilter', () => {
|
|
5
|
-
it('reads the {from,to} dialect', () => {
|
|
6
|
-
expect(parseRangeFilter({ from: 1, to: 5 })).toEqual({ gte: 1, lte: 5 })
|
|
7
|
-
})
|
|
8
|
-
|
|
9
|
-
it('reads the Mongo dialect the models actually emit', () => {
|
|
10
|
-
// The exact filter that answered "não há compromissos hoje" over a booked day.
|
|
11
|
-
expect(
|
|
12
|
-
parseRangeFilter({ $gte: '2026-07-27T00:00:00Z', $lt: '2026-07-28T00:00:00Z' }),
|
|
13
|
-
).toEqual({ gte: '2026-07-27T00:00:00Z', lt: '2026-07-28T00:00:00Z' })
|
|
14
|
-
})
|
|
15
|
-
|
|
16
|
-
it('reads bare comparison keys', () => {
|
|
17
|
-
expect(parseRangeFilter({ gte: 10, lt: 20 })).toEqual({ gte: 10, lt: 20 })
|
|
18
|
-
})
|
|
19
|
-
|
|
20
|
-
it('returns null for plain values, so they stay equality filters', () => {
|
|
21
|
-
expect(parseRangeFilter('open')).toBeNull()
|
|
22
|
-
expect(parseRangeFilter(3)).toBeNull()
|
|
23
|
-
expect(parseRangeFilter(null)).toBeNull()
|
|
24
|
-
})
|
|
25
|
-
|
|
26
|
-
it('returns null for an object with no recognised bound — never a silent match-nothing', () => {
|
|
27
|
-
expect(parseRangeFilter({ equals: 'open' })).toBeNull()
|
|
28
|
-
expect(isFilterObject({ equals: 'open' })).toBe(true)
|
|
29
|
-
})
|
|
30
|
-
})
|
|
31
|
-
|
|
32
|
-
describe('matchesRange', () => {
|
|
33
|
-
const day = { gte: '2026-07-27T00:00:00Z', lt: '2026-07-28T00:00:00Z' }
|
|
34
|
-
|
|
35
|
-
it('keeps a timestamp inside the window', () => {
|
|
36
|
-
expect(matchesRange('2026-07-27T18:00:00+00:00', day)).toBe(true)
|
|
37
|
-
})
|
|
38
|
-
|
|
39
|
-
it('drops the bounds it should', () => {
|
|
40
|
-
expect(matchesRange('2026-07-28T00:00:00Z', day)).toBe(false)
|
|
41
|
-
expect(matchesRange('2026-07-26T23:59:59Z', day)).toBe(false)
|
|
42
|
-
})
|
|
43
|
-
|
|
44
|
-
it('honours inclusive upper bounds', () => {
|
|
45
|
-
expect(matchesRange(5, { gte: 1, lte: 5 })).toBe(true)
|
|
46
|
-
expect(matchesRange(5, { gte: 1, lt: 5 })).toBe(false)
|
|
47
|
-
})
|
|
48
|
-
|
|
49
|
-
it('rejects values that are not comparable instead of letting them through', () => {
|
|
50
|
-
expect(matchesRange(null, day)).toBe(false)
|
|
51
|
-
expect(matchesRange('não é data', day)).toBe(false)
|
|
52
|
-
})
|
|
53
|
-
})
|
|
54
|
-
|
|
55
|
-
describe('applyRangeToQuery', () => {
|
|
56
|
-
it('emits one PostgREST call per bound', () => {
|
|
57
|
-
const calls: Array<[string, string, unknown]> = []
|
|
58
|
-
const builder = {
|
|
59
|
-
gte(c: string, v: unknown) { calls.push(['gte', c, v]); return this },
|
|
60
|
-
gt(c: string, v: unknown) { calls.push(['gt', c, v]); return this },
|
|
61
|
-
lte(c: string, v: unknown) { calls.push(['lte', c, v]); return this },
|
|
62
|
-
lt(c: string, v: unknown) { calls.push(['lt', c, v]); return this },
|
|
63
|
-
}
|
|
64
|
-
applyRangeToQuery(builder, 'starts_at', { gte: 'a', lt: 'b' })
|
|
65
|
-
expect(calls).toEqual([
|
|
66
|
-
['gte', 'starts_at', 'a'],
|
|
67
|
-
['lt', 'starts_at', 'b'],
|
|
68
|
-
])
|
|
69
|
-
})
|
|
70
|
-
})
|
package/src/data/filters.ts
DELETED
|
@@ -1,90 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* ONE filter grammar, shared by the providers and by the agent's data tools.
|
|
3
|
-
*
|
|
4
|
-
* Models express ranges in whatever dialect they were trained on — `{from,to}`,
|
|
5
|
-
* `{gte,lte}`, Mongo's `{$gte,$lt}`. When only some of those are recognised the
|
|
6
|
-
* unrecognised one falls through to an equality compare against
|
|
7
|
-
* "[object Object]", which matches nothing and reads as "there are no
|
|
8
|
-
* appointments today" instead of "I did not understand that filter".
|
|
9
|
-
*/
|
|
10
|
-
|
|
11
|
-
export interface RangeFilter {
|
|
12
|
-
gte?: unknown
|
|
13
|
-
gt?: unknown
|
|
14
|
-
lte?: unknown
|
|
15
|
-
lt?: unknown
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
const LOWER_INCLUSIVE = ['from', 'gte', 'start', 'after', 'since', 'min']
|
|
19
|
-
const LOWER_EXCLUSIVE = ['gt']
|
|
20
|
-
const UPPER_INCLUSIVE = ['to', 'lte', 'end', 'until', 'before', 'max']
|
|
21
|
-
const UPPER_EXCLUSIVE = ['lt']
|
|
22
|
-
|
|
23
|
-
function pick(source: Record<string, unknown>, names: string[]): unknown {
|
|
24
|
-
for (const name of names) {
|
|
25
|
-
if (source[name] !== undefined && source[name] !== null) return source[name]
|
|
26
|
-
const dollar = `$${name}`
|
|
27
|
-
if (source[dollar] !== undefined && source[dollar] !== null) return source[dollar]
|
|
28
|
-
}
|
|
29
|
-
return undefined
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
/**
|
|
33
|
-
* Parse a filter value into a range, or return null when it is a plain
|
|
34
|
-
* (equality) value. An object that looks like a filter but carries no
|
|
35
|
-
* recognised bound returns `null` too — callers treat that as "invalid",
|
|
36
|
-
* never as "matches nothing".
|
|
37
|
-
*/
|
|
38
|
-
export function parseRangeFilter(value: unknown): RangeFilter | null {
|
|
39
|
-
if (typeof value !== 'object' || value === null || Array.isArray(value)) return null
|
|
40
|
-
const source = value as Record<string, unknown>
|
|
41
|
-
const range: RangeFilter = {}
|
|
42
|
-
const gte = pick(source, LOWER_INCLUSIVE)
|
|
43
|
-
const gt = pick(source, LOWER_EXCLUSIVE)
|
|
44
|
-
const lte = pick(source, UPPER_INCLUSIVE)
|
|
45
|
-
const lt = pick(source, UPPER_EXCLUSIVE)
|
|
46
|
-
if (gte !== undefined) range.gte = gte
|
|
47
|
-
if (gt !== undefined) range.gt = gt
|
|
48
|
-
if (lte !== undefined) range.lte = lte
|
|
49
|
-
if (lt !== undefined) range.lt = lt
|
|
50
|
-
return Object.keys(range).length ? range : null
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
/** True for any object value — including ones no bound could be read from. */
|
|
54
|
-
export function isFilterObject(value: unknown): boolean {
|
|
55
|
-
return typeof value === 'object' && value !== null && !Array.isArray(value)
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
function comparable(value: unknown): number {
|
|
59
|
-
if (typeof value === 'number') return value
|
|
60
|
-
if (value instanceof Date) return value.getTime()
|
|
61
|
-
const ts = Date.parse(String(value))
|
|
62
|
-
if (!Number.isNaN(ts)) return ts
|
|
63
|
-
return Number(value)
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
/** Client-side counterpart of the range a provider would push to SQL. */
|
|
67
|
-
export function matchesRange(raw: unknown, range: RangeFilter): boolean {
|
|
68
|
-
const value = comparable(raw)
|
|
69
|
-
if (Number.isNaN(value)) return false
|
|
70
|
-
if (range.gte !== undefined && value < comparable(range.gte)) return false
|
|
71
|
-
if (range.gt !== undefined && value <= comparable(range.gt)) return false
|
|
72
|
-
if (range.lte !== undefined && value > comparable(range.lte)) return false
|
|
73
|
-
if (range.lt !== undefined && value >= comparable(range.lt)) return false
|
|
74
|
-
return true
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
/** Apply a range to a PostgREST query builder. */
|
|
78
|
-
export function applyRangeToQuery<Q>(
|
|
79
|
-
query: Q,
|
|
80
|
-
column: string,
|
|
81
|
-
range: RangeFilter,
|
|
82
|
-
): Q {
|
|
83
|
-
let q = query as Record<string, (c: string, v: unknown) => unknown>
|
|
84
|
-
for (const op of ['gte', 'gt', 'lte', 'lt'] as const) {
|
|
85
|
-
const bound = range[op]
|
|
86
|
-
if (bound === undefined) continue
|
|
87
|
-
q = q[op](column, bound) as Record<string, (c: string, v: unknown) => unknown>
|
|
88
|
-
}
|
|
89
|
-
return q as Q
|
|
90
|
-
}
|
package/src/data/index.ts
DELETED
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
export { createSupabaseProvider, setGlobalSupabaseClient, getSupabaseClientOptional } from './supabase'
|
|
2
|
-
export { countByTenant, invalidateCount } from './count'
|
|
3
|
-
export type { CountByTenantOptions } from './count'
|
|
4
|
-
export { createFayzApiProvider } from './platform-api'
|
|
5
|
-
export { createMockProvider } from './mock'
|
|
6
|
-
export { bulkCreate } from './bulk'
|
|
7
|
-
export { resolveDataProvider } from './resolve'
|
|
8
|
-
export type { DataProvider, CrudQuery, CrudResult, BulkWriteOptions, BulkWriteResult } from './types'
|
|
9
|
-
export type { SupabaseProviderConfig } from './supabase'
|
|
10
|
-
export type { FayzApiProviderConfig } from './platform-api'
|
|
11
|
-
export type { ResolveDataProviderOptions } from './resolve'
|
package/src/data/mock.ts
DELETED
|
@@ -1,90 +0,0 @@
|
|
|
1
|
-
import type { DataProvider, CrudQuery, CrudResult } from './types'
|
|
2
|
-
import type { EntityDef } from '../types/crud'
|
|
3
|
-
import { matchesRange, parseRangeFilter } from './filters'
|
|
4
|
-
|
|
5
|
-
export function createMockProvider<T extends { id: string; [key: string]: unknown }>(
|
|
6
|
-
entityDefOrSearchKeys: EntityDef<T> | string[],
|
|
7
|
-
initialData: T[] = [],
|
|
8
|
-
): DataProvider<T> {
|
|
9
|
-
let items = [...initialData]
|
|
10
|
-
|
|
11
|
-
const searchableKeys = Array.isArray(entityDefOrSearchKeys)
|
|
12
|
-
? entityDefOrSearchKeys
|
|
13
|
-
: entityDefOrSearchKeys.fields.filter((f) => f.searchable).map((f) => f.key)
|
|
14
|
-
|
|
15
|
-
const defaultSort = Array.isArray(entityDefOrSearchKeys) ? undefined : entityDefOrSearchKeys.defaultSort
|
|
16
|
-
const defaultSortDir = Array.isArray(entityDefOrSearchKeys) ? 'asc' : (entityDefOrSearchKeys.defaultSortDir ?? 'asc')
|
|
17
|
-
|
|
18
|
-
return {
|
|
19
|
-
async list(query: CrudQuery): Promise<CrudResult<T>> {
|
|
20
|
-
let filtered = [...items]
|
|
21
|
-
|
|
22
|
-
if (query.search && searchableKeys.length > 0) {
|
|
23
|
-
const term = query.search.toLowerCase()
|
|
24
|
-
filtered = filtered.filter((item) =>
|
|
25
|
-
searchableKeys.some((key) => {
|
|
26
|
-
const val = item[key]
|
|
27
|
-
return typeof val === 'string' && val.toLowerCase().includes(term)
|
|
28
|
-
}),
|
|
29
|
-
)
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
// Faceted / dynamic filters — same grammar the SQL providers speak, so a
|
|
33
|
-
// date window behaves identically on mock data instead of silently
|
|
34
|
-
// matching nothing (a range object is never `===` anything).
|
|
35
|
-
if (query.filters) {
|
|
36
|
-
for (const [key, val] of Object.entries(query.filters)) {
|
|
37
|
-
if (val == null || val === '') continue
|
|
38
|
-
const range = parseRangeFilter(val)
|
|
39
|
-
filtered = range
|
|
40
|
-
? filtered.filter((item) => matchesRange(item[key], range))
|
|
41
|
-
: filtered.filter((item) => item[key] === val)
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
const sortBy = query.sortBy ?? defaultSort
|
|
46
|
-
const sortDir = query.sortDir ?? defaultSortDir
|
|
47
|
-
if (sortBy) {
|
|
48
|
-
filtered.sort((a, b) => {
|
|
49
|
-
const av = a[sortBy] ?? ''
|
|
50
|
-
const bv = b[sortBy] ?? ''
|
|
51
|
-
const cmp = typeof av === 'number' && typeof bv === 'number'
|
|
52
|
-
? av - bv
|
|
53
|
-
: String(av).localeCompare(String(bv))
|
|
54
|
-
return sortDir === 'desc' ? -cmp : cmp
|
|
55
|
-
})
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
const total = filtered.length
|
|
59
|
-
const page = query.page ?? 1
|
|
60
|
-
const pageSize = query.pageSize ?? 50
|
|
61
|
-
const start = (page - 1) * pageSize
|
|
62
|
-
const data = filtered.slice(start, start + pageSize)
|
|
63
|
-
return { data, total }
|
|
64
|
-
},
|
|
65
|
-
|
|
66
|
-
async create(data) {
|
|
67
|
-
const now = new Date().toISOString()
|
|
68
|
-
const item = {
|
|
69
|
-
...data,
|
|
70
|
-
id: crypto.randomUUID(),
|
|
71
|
-
tenantId: 'mock-tenant',
|
|
72
|
-
createdAt: now,
|
|
73
|
-
updatedAt: now,
|
|
74
|
-
} as unknown as T
|
|
75
|
-
items.unshift(item)
|
|
76
|
-
return item
|
|
77
|
-
},
|
|
78
|
-
|
|
79
|
-
async update(id, data) {
|
|
80
|
-
const idx = items.findIndex((i) => i.id === id)
|
|
81
|
-
if (idx === -1) throw new Error(`[@fayz-ai/core] Mock provider: item not found: ${id}`)
|
|
82
|
-
items[idx] = { ...items[idx], ...data, updatedAt: new Date().toISOString() }
|
|
83
|
-
return items[idx]!
|
|
84
|
-
},
|
|
85
|
-
|
|
86
|
-
async remove(id) {
|
|
87
|
-
items = items.filter((i) => i.id !== id)
|
|
88
|
-
},
|
|
89
|
-
}
|
|
90
|
-
}
|
package/src/data/platform-api.ts
DELETED
|
@@ -1,209 +0,0 @@
|
|
|
1
|
-
import type { CrudQuery, CrudResult, DataProvider } from './types'
|
|
2
|
-
import { parseRangeFilter } from './filters'
|
|
3
|
-
|
|
4
|
-
type FayzApiFilterOperator = 'eq' | 'neq' | 'gt' | 'lt' | 'gte' | 'lte' | 'like' | 'ilike' | 'is_null' | 'is_not_null'
|
|
5
|
-
|
|
6
|
-
interface FayzApiTableFilter {
|
|
7
|
-
column: string
|
|
8
|
-
operator: FayzApiFilterOperator
|
|
9
|
-
value?: unknown
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
export interface FayzApiProviderConfig {
|
|
13
|
-
baseUrl?: string
|
|
14
|
-
projectId?: string
|
|
15
|
-
tenantId?: string | (() => string | undefined)
|
|
16
|
-
tenantIdColumn?: string | false
|
|
17
|
-
entityKey?: string
|
|
18
|
-
table?: string
|
|
19
|
-
schema?: string
|
|
20
|
-
idColumn?: string
|
|
21
|
-
searchColumns?: string[]
|
|
22
|
-
runtimeToken?: string | (() => string | undefined | Promise<string | undefined>)
|
|
23
|
-
headers?: () => HeadersInit | Promise<HeadersInit>
|
|
24
|
-
fetcher?: typeof fetch
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
function resolveTenantId(config?: FayzApiProviderConfig): string | undefined {
|
|
28
|
-
const tenantId = config?.tenantId
|
|
29
|
-
return typeof tenantId === 'function' ? tenantId() : tenantId
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
async function resolveRuntimeToken(config?: FayzApiProviderConfig): Promise<string | undefined> {
|
|
33
|
-
const runtimeToken = config?.runtimeToken
|
|
34
|
-
return typeof runtimeToken === 'function' ? runtimeToken() : runtimeToken
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
function trimTrailingSlash(value: string): string {
|
|
38
|
-
return value.replace(/\/+$/, '')
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
function appendQuery(
|
|
42
|
-
url: URL,
|
|
43
|
-
query: CrudQuery,
|
|
44
|
-
tenantId?: string,
|
|
45
|
-
tenantIdColumn = 'tenant_id',
|
|
46
|
-
searchColumns: string[] = [],
|
|
47
|
-
): void {
|
|
48
|
-
const filters = normalizeFilters(query.filters)
|
|
49
|
-
if (query.search && searchColumns[0]) {
|
|
50
|
-
filters.push({ column: searchColumns[0], operator: 'ilike', value: `%${query.search}%` })
|
|
51
|
-
}
|
|
52
|
-
if (query.sortBy) url.searchParams.set('sortColumn', query.sortBy)
|
|
53
|
-
if (query.sortDir) url.searchParams.set('sortDirection', query.sortDir)
|
|
54
|
-
if (query.page != null) url.searchParams.set('page', String(query.page))
|
|
55
|
-
if (query.pageSize != null) url.searchParams.set('limit', String(query.pageSize))
|
|
56
|
-
if (tenantId) filters.push({ column: tenantIdColumn, operator: 'eq', value: tenantId })
|
|
57
|
-
if (filters.length > 0) url.searchParams.set('filters', JSON.stringify(filters))
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
async function parseResponse<T>(response: Response): Promise<T> {
|
|
61
|
-
if (!response.ok) {
|
|
62
|
-
const text = await response.text().catch(() => '')
|
|
63
|
-
throw new Error(`[@fayz-ai/core] Fayz API request failed (${response.status}): ${text || response.statusText}`)
|
|
64
|
-
}
|
|
65
|
-
return response.json() as Promise<T>
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
function isFilterOperator(value: unknown): value is FayzApiFilterOperator {
|
|
69
|
-
return typeof value === 'string' && [
|
|
70
|
-
'eq',
|
|
71
|
-
'neq',
|
|
72
|
-
'gt',
|
|
73
|
-
'lt',
|
|
74
|
-
'gte',
|
|
75
|
-
'lte',
|
|
76
|
-
'like',
|
|
77
|
-
'ilike',
|
|
78
|
-
'is_null',
|
|
79
|
-
'is_not_null',
|
|
80
|
-
].includes(value)
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
84
|
-
return typeof value === 'object' && value !== null && !Array.isArray(value)
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
function normalizeFilters(filters?: Record<string, unknown>): FayzApiTableFilter[] {
|
|
88
|
-
if (!filters) return []
|
|
89
|
-
return Object.entries(filters).flatMap(([column, raw]): FayzApiTableFilter[] => {
|
|
90
|
-
if (isRecord(raw) && isFilterOperator(raw.operator)) {
|
|
91
|
-
return [{ column, operator: raw.operator, value: raw.value }]
|
|
92
|
-
}
|
|
93
|
-
// A range is TWO bounds on one column — sending it as a single `eq` filter
|
|
94
|
-
// compared the column against an object and matched nothing.
|
|
95
|
-
const range = parseRangeFilter(raw)
|
|
96
|
-
if (range) {
|
|
97
|
-
return (['gte', 'gt', 'lte', 'lt'] as const)
|
|
98
|
-
.filter((op) => range[op] !== undefined)
|
|
99
|
-
.map((op) => ({ column, operator: op, value: range[op] }))
|
|
100
|
-
}
|
|
101
|
-
return [{ column, operator: 'eq', value: raw }]
|
|
102
|
-
})
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
export function createFayzApiProvider<T extends { id: string }>(
|
|
106
|
-
entityOrTable: string,
|
|
107
|
-
config: FayzApiProviderConfig = {},
|
|
108
|
-
): DataProvider<T> {
|
|
109
|
-
const fetcher = config.fetcher ?? fetch
|
|
110
|
-
const baseUrl = trimTrailingSlash(config.baseUrl ?? '')
|
|
111
|
-
const projectId = config.projectId
|
|
112
|
-
const table = config.table ?? entityOrTable
|
|
113
|
-
const idColumn = config.idColumn ?? 'id'
|
|
114
|
-
|
|
115
|
-
async function headers(runtimeToken?: string): Promise<HeadersInit> {
|
|
116
|
-
return {
|
|
117
|
-
'Content-Type': 'application/json',
|
|
118
|
-
...(config.headers ? await config.headers() : {}),
|
|
119
|
-
...(runtimeToken ? { Authorization: `Bearer ${runtimeToken}` } : {}),
|
|
120
|
-
}
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
function tableRowsUrl(runtime = false): URL {
|
|
124
|
-
const prefix = baseUrl || (typeof window !== 'undefined' ? window.location.origin : 'http://localhost')
|
|
125
|
-
const encodedProjectId = encodeURIComponent(projectId ?? 'current')
|
|
126
|
-
const encodedTable = encodeURIComponent(table)
|
|
127
|
-
const route = runtime
|
|
128
|
-
? `/api/v1/runtime/projects/${encodedProjectId}/database/tables/${encodedTable}/rows`
|
|
129
|
-
: `/api/projects/${encodedProjectId}/database/tables/${encodedTable}/rows`
|
|
130
|
-
const url = new URL(route, prefix)
|
|
131
|
-
if (config.schema) url.searchParams.set('schema', config.schema)
|
|
132
|
-
return url
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
function withTenant(data: Record<string, unknown>, runtime = false): Record<string, unknown> {
|
|
136
|
-
if (runtime) return data
|
|
137
|
-
const tenantId = resolveTenantId(config)
|
|
138
|
-
if (!tenantId || config.tenantIdColumn === false) return data
|
|
139
|
-
return { ...data, [config.tenantIdColumn ?? 'tenant_id']: tenantId }
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
function primaryKeys(id: string): Record<string, unknown> {
|
|
143
|
-
return { [idColumn]: id }
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
async function listRows(query: CrudQuery): Promise<CrudResult<T>> {
|
|
147
|
-
const runtimeToken = await resolveRuntimeToken(config)
|
|
148
|
-
const runtime = Boolean(runtimeToken)
|
|
149
|
-
const url = tableRowsUrl(runtime)
|
|
150
|
-
appendQuery(
|
|
151
|
-
url,
|
|
152
|
-
query,
|
|
153
|
-
runtime || config.tenantIdColumn === false ? undefined : resolveTenantId(config),
|
|
154
|
-
config.tenantIdColumn || undefined,
|
|
155
|
-
config.searchColumns,
|
|
156
|
-
)
|
|
157
|
-
const result = await parseResponse<{ rows: T[]; total: number }>(await fetcher(url, { headers: await headers(runtimeToken) }))
|
|
158
|
-
return { data: result.rows, total: result.total }
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
async function assertTenantScopedRowExists(id: string, runtime = false): Promise<void> {
|
|
162
|
-
if (runtime) return
|
|
163
|
-
const tenantId = config.tenantIdColumn === false ? undefined : resolveTenantId(config)
|
|
164
|
-
if (!tenantId) return
|
|
165
|
-
|
|
166
|
-
const result = await listRows({ filters: { [idColumn]: id }, page: 1, pageSize: 1 })
|
|
167
|
-
if (result.data.length === 0) {
|
|
168
|
-
throw new Error(`[@fayz-ai/core] Row "${id}" was not found for the active tenant.`)
|
|
169
|
-
}
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
return {
|
|
173
|
-
async list(query: CrudQuery): Promise<CrudResult<T>> {
|
|
174
|
-
return listRows(query)
|
|
175
|
-
},
|
|
176
|
-
|
|
177
|
-
async create(data) {
|
|
178
|
-
const runtimeToken = await resolveRuntimeToken(config)
|
|
179
|
-
const runtime = Boolean(runtimeToken)
|
|
180
|
-
return parseResponse<T>(await fetcher(tableRowsUrl(runtime), {
|
|
181
|
-
method: 'POST',
|
|
182
|
-
headers: await headers(runtimeToken),
|
|
183
|
-
body: JSON.stringify(withTenant(data as Record<string, unknown>, runtime)),
|
|
184
|
-
}))
|
|
185
|
-
},
|
|
186
|
-
|
|
187
|
-
async update(id, data) {
|
|
188
|
-
const runtimeToken = await resolveRuntimeToken(config)
|
|
189
|
-
const runtime = Boolean(runtimeToken)
|
|
190
|
-
await assertTenantScopedRowExists(id, runtime)
|
|
191
|
-
return parseResponse<T>(await fetcher(tableRowsUrl(runtime), {
|
|
192
|
-
method: 'PATCH',
|
|
193
|
-
headers: await headers(runtimeToken),
|
|
194
|
-
body: JSON.stringify({ primaryKeys: primaryKeys(id), data }),
|
|
195
|
-
}))
|
|
196
|
-
},
|
|
197
|
-
|
|
198
|
-
async remove(id) {
|
|
199
|
-
const runtimeToken = await resolveRuntimeToken(config)
|
|
200
|
-
const runtime = Boolean(runtimeToken)
|
|
201
|
-
await assertTenantScopedRowExists(id, runtime)
|
|
202
|
-
await parseResponse<{ deletedCount: number }>(await fetcher(tableRowsUrl(runtime), {
|
|
203
|
-
method: 'DELETE',
|
|
204
|
-
headers: await headers(runtimeToken),
|
|
205
|
-
body: JSON.stringify({ rows: [primaryKeys(id)] }),
|
|
206
|
-
}))
|
|
207
|
-
},
|
|
208
|
-
}
|
|
209
|
-
}
|
package/src/data/refresh.ts
DELETED
|
@@ -1,115 +0,0 @@
|
|
|
1
|
-
import * as React from 'react'
|
|
2
|
-
import { eventBus } from '../events'
|
|
3
|
-
import { globalCache } from '../lib/cache'
|
|
4
|
-
import { getActiveTenantId } from '../tenant'
|
|
5
|
-
|
|
6
|
-
// ---------------------------------------------------------------------------
|
|
7
|
-
// Data refresh bus — "something wrote; whoever is showing it should reload".
|
|
8
|
-
//
|
|
9
|
-
// A write that happens OUTSIDE the list showing it (the assistant creating an
|
|
10
|
-
// appointment, a webhook, another tab) used to leave the screen stale until the
|
|
11
|
-
// user reloaded by hand. Providers already invalidate their own read cache on
|
|
12
|
-
// create/update/remove, but a mounted list holds its rows in a store — nothing
|
|
13
|
-
// told it to fetch again.
|
|
14
|
-
//
|
|
15
|
-
// This is the missing half: one broadcast, every surface that renders the
|
|
16
|
-
// affected data listens. Deliberately coarse — a table name, not a row diff.
|
|
17
|
-
// Refetching a page of rows is cheap; a stale screen after "criei o
|
|
18
|
-
// agendamento" is not.
|
|
19
|
-
// ---------------------------------------------------------------------------
|
|
20
|
-
|
|
21
|
-
export const DATA_CHANGED_EVENT = 'data:changed'
|
|
22
|
-
|
|
23
|
-
export interface DataChangedPayload {
|
|
24
|
-
/** Physical table the write touched, when the writer knows it. */
|
|
25
|
-
table?: string
|
|
26
|
-
/** Registered entity key (deriveEntityKey), when the writer knows it. */
|
|
27
|
-
entityKey?: string
|
|
28
|
-
/** Archetype the record belongs to (`person:client`), when known. */
|
|
29
|
-
archetype?: string
|
|
30
|
-
op?: 'create' | 'update' | 'delete' | 'unknown'
|
|
31
|
-
/** Affected record id, when known. */
|
|
32
|
-
id?: string
|
|
33
|
-
/** What caused the write — lets a surface ignore its own UI writes. */
|
|
34
|
-
source?: 'agent' | 'ui' | 'remote'
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
/** What a subscriber renders. Any field left out matches everything. */
|
|
38
|
-
export interface DataChangedMatch {
|
|
39
|
-
table?: string
|
|
40
|
-
entityKey?: string
|
|
41
|
-
archetype?: string
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
/**
|
|
45
|
-
* A write with NO table/entityKey is a write we could not attribute (a plugin
|
|
46
|
-
* tool, an RPC). Subscribers still refresh for it: a redundant refetch costs a
|
|
47
|
-
* request, a missed one costs the user's trust in what the screen shows.
|
|
48
|
-
*/
|
|
49
|
-
export function matchesDataChange(match: DataChangedMatch, payload: DataChangedPayload): boolean {
|
|
50
|
-
const attributed = payload.table ?? payload.entityKey ?? payload.archetype
|
|
51
|
-
if (!attributed) return true
|
|
52
|
-
if (match.table && payload.table && match.table === payload.table) return true
|
|
53
|
-
if (match.entityKey && payload.entityKey && match.entityKey === payload.entityKey) return true
|
|
54
|
-
if (match.archetype && payload.archetype && match.archetype === payload.archetype) return true
|
|
55
|
-
// A subscriber that declared nothing listens to everything.
|
|
56
|
-
return !match.table && !match.entityKey && !match.archetype
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
// One agent turn can write twice (create + link). Coalesce so the lists behind
|
|
60
|
-
// it fetch once, after the turn settles, instead of once per call.
|
|
61
|
-
let pending: DataChangedPayload[] = []
|
|
62
|
-
let flushTimer: ReturnType<typeof setTimeout> | null = null
|
|
63
|
-
|
|
64
|
-
function flush(): void {
|
|
65
|
-
const batch = pending
|
|
66
|
-
pending = []
|
|
67
|
-
flushTimer = null
|
|
68
|
-
for (const payload of batch) eventBus.emit(DATA_CHANGED_EVENT, payload)
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
/**
|
|
72
|
-
* Announce a write. Invalidates the provider read cache for the affected table
|
|
73
|
-
* (an unattributed write clears it wholesale — a stale hit would defeat the
|
|
74
|
-
* refetch it just triggered) and broadcasts to mounted surfaces.
|
|
75
|
-
*/
|
|
76
|
-
export function emitDataChanged(payload: DataChangedPayload = {}): void {
|
|
77
|
-
if (payload.table) {
|
|
78
|
-
const tenant = getActiveTenantId() ?? '_'
|
|
79
|
-
// The base table and its read-model view are the SAME data under two cache
|
|
80
|
-
// namespaces (appointments / v_appointments). Invalidating only the one the
|
|
81
|
-
// writer names leaves the other serving a snapshot that predates the write
|
|
82
|
-
// — which is how the assistant kept reporting an empty agenda after a
|
|
83
|
-
// booking was created on the agenda screen.
|
|
84
|
-
const sibling = payload.table.startsWith('v_')
|
|
85
|
-
? payload.table.slice(2)
|
|
86
|
-
: `v_${payload.table}`
|
|
87
|
-
globalCache.invalidate(`${tenant}:${payload.table}`)
|
|
88
|
-
globalCache.invalidate(`${tenant}:${sibling}`)
|
|
89
|
-
} else {
|
|
90
|
-
globalCache.clear()
|
|
91
|
-
}
|
|
92
|
-
pending.push({ op: 'unknown', source: 'agent', ...payload })
|
|
93
|
-
if (flushTimer) return
|
|
94
|
-
flushTimer = setTimeout(flush, 120)
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
/** Subscribe for the lifetime of the component. The latest handler always runs. */
|
|
98
|
-
export function useDataChanged(
|
|
99
|
-
match: DataChangedMatch,
|
|
100
|
-
handler: (payload: DataChangedPayload) => void,
|
|
101
|
-
deps: React.DependencyList = [],
|
|
102
|
-
): void {
|
|
103
|
-
const handlerRef = React.useRef(handler)
|
|
104
|
-
handlerRef.current = handler
|
|
105
|
-
const { table, entityKey, archetype } = match
|
|
106
|
-
|
|
107
|
-
React.useEffect(
|
|
108
|
-
() =>
|
|
109
|
-
eventBus.on<DataChangedPayload>(DATA_CHANGED_EVENT, (payload) => {
|
|
110
|
-
if (matchesDataChange({ table, entityKey, archetype }, payload)) handlerRef.current(payload)
|
|
111
|
-
}),
|
|
112
|
-
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
113
|
-
[table, entityKey, archetype, ...deps],
|
|
114
|
-
)
|
|
115
|
-
}
|