@fayz-ai/core 0.9.0 → 0.10.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-L4TLMYGC.js → chunk-6RBW2Y5K.js} +3 -3
- package/dist/chunk-6RBW2Y5K.js.map +1 -0
- package/dist/chunk-732GZEUI.js +1592 -0
- package/dist/chunk-732GZEUI.js.map +1 -0
- package/dist/{chunk-X5THHG6T.js → chunk-CPMHCTX3.js} +3 -3
- package/dist/chunk-CPMHCTX3.js.map +1 -0
- package/dist/{chunk-V2MSAUHO.js → chunk-JO7Z2AOE.js} +2 -2
- package/dist/chunk-JO7Z2AOE.js.map +1 -0
- package/dist/{chunk-5RJVUC6J.js → chunk-M27JPX3K.js} +12 -3
- package/dist/chunk-M27JPX3K.js.map +1 -0
- package/dist/chunk-OXQGRJZT.js +125 -0
- package/dist/chunk-OXQGRJZT.js.map +1 -0
- package/dist/data/backend-env.d.ts +38 -0
- package/dist/data/backend-env.d.ts.map +1 -0
- package/dist/data/index.js +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/i18n/index.d.ts +10 -0
- package/dist/i18n/index.d.ts.map +1 -1
- package/dist/i18n/index.js +1 -1
- package/dist/index.d.ts +27 -17
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +388 -52
- package/dist/index.js.map +1 -1
- package/dist/integrations/index.js +1 -1
- package/dist/{integrations/index.d.ts → integrations.d.ts} +2 -2
- package/dist/integrations.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 +2 -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 +153 -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/testing/index.js +1 -1
- package/dist/{testing/index.d.ts → testing.d.ts} +3 -3
- package/dist/testing.d.ts.map +1 -0
- package/dist/types/analytics.d.ts +196 -0
- package/dist/types/analytics.d.ts.map +1 -0
- package/dist/types/crud.d.ts +22 -2
- package/dist/types/crud.d.ts.map +1 -1
- package/dist/types/plugins.d.ts +55 -1
- package/dist/types/plugins.d.ts.map +1 -1
- package/package.json +6 -18
- 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.map +0 -1
- package/dist/chunk-V2MSAUHO.js.map +0 -1
- 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/integrations/index.d.ts.map +0 -1
- package/dist/phone/index.d.ts.map +0 -1
- package/dist/seo/index.d.ts.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/count.ts
DELETED
|
@@ -1,131 +0,0 @@
|
|
|
1
|
-
import { getSupabaseClientOptional } from './supabase'
|
|
2
|
-
import { getActiveTenantId } from '../tenant'
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* Tenant-scoped row counter — the single cheap `count(*)` helper the access
|
|
6
|
-
* engine uses to resolve quantity limits. Uses Supabase's `head:true` count so
|
|
7
|
-
* no rows are transferred. Results are cached briefly per tenant+table+filter to
|
|
8
|
-
* survive the burst of reads a page does (nav badge + gate + list), and can be
|
|
9
|
-
* force-invalidated by creates via {@link invalidateCount}.
|
|
10
|
-
*
|
|
11
|
-
* MOCK MODE NEVER BLOCKS: with no global Supabase client the count is `0`, so
|
|
12
|
-
* limits always read as "well under cap" — offline/demo apps stay fully usable.
|
|
13
|
-
*/
|
|
14
|
-
|
|
15
|
-
export interface CountByTenantOptions {
|
|
16
|
-
/** Optional `kind` column filter for multi-kind tables. */
|
|
17
|
-
kind?: string
|
|
18
|
-
/** `'month'` counts rows created since the 1st of the current month; `'total'` (default) counts all. */
|
|
19
|
-
period?: 'month' | 'total'
|
|
20
|
-
/** Tenant override; defaults to the active tenant (getActiveTenantId). */
|
|
21
|
-
tenantId?: string
|
|
22
|
-
/** Skip the short-lived cache and hit the DB (used by fresh guard checks). */
|
|
23
|
-
fresh?: boolean
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
const TTL_MS = 15_000
|
|
27
|
-
|
|
28
|
-
interface CacheEntry {
|
|
29
|
-
value: number
|
|
30
|
-
expires: number
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
const cache = new Map<string, CacheEntry>()
|
|
34
|
-
|
|
35
|
-
function cacheKey(table: string, tenantId: string, kind: string | undefined, period: string): string {
|
|
36
|
-
return `${tenantId}::${table}::${kind ?? ''}::${period}`
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
function startOfMonthISO(): string {
|
|
40
|
-
const now = new Date()
|
|
41
|
-
return new Date(now.getFullYear(), now.getMonth(), 1).toISOString()
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
/**
|
|
45
|
-
* How long a usage count may take before we give up on it. Failing OPEN on an
|
|
46
|
-
* error was always the policy (see below); a request that never settles is the
|
|
47
|
-
* same failure with worse symptoms — a caller awaiting this (the create guards)
|
|
48
|
-
* hangs forever, which reads to the user as a dead button. Observed on a cold
|
|
49
|
-
* boot, where the query can be issued before the auth session is ready.
|
|
50
|
-
*/
|
|
51
|
-
const COUNT_TIMEOUT_MS = 5_000
|
|
52
|
-
|
|
53
|
-
/**
|
|
54
|
-
* Count rows in `table` for the given (or active) tenant.
|
|
55
|
-
* Returns 0 when no Supabase client is registered (mock mode).
|
|
56
|
-
*/
|
|
57
|
-
export async function countByTenant(table: string, options: CountByTenantOptions = {}): Promise<number> {
|
|
58
|
-
const tenantId = options.tenantId ?? getActiveTenantId()
|
|
59
|
-
if (!tenantId) return 0
|
|
60
|
-
|
|
61
|
-
const period = options.period ?? 'total'
|
|
62
|
-
const key = cacheKey(table, tenantId, options.kind, period)
|
|
63
|
-
|
|
64
|
-
if (!options.fresh) {
|
|
65
|
-
const hit = cache.get(key)
|
|
66
|
-
if (hit && hit.expires > Date.now()) return hit.value
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
const client = getSupabaseClientOptional() as {
|
|
70
|
-
from: (t: string) => {
|
|
71
|
-
select: (columns: string, opts: { count: 'exact'; head: true }) => {
|
|
72
|
-
eq: (col: string, val: unknown) => unknown
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
} | null
|
|
76
|
-
|
|
77
|
-
// No client → mock/offline mode: never block.
|
|
78
|
-
if (!client) return 0
|
|
79
|
-
|
|
80
|
-
let query = client.from(table).select('*', { count: 'exact', head: true }).eq('tenant_id', tenantId) as {
|
|
81
|
-
eq: (col: string, val: unknown) => unknown
|
|
82
|
-
gte: (col: string, val: unknown) => unknown
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
if (options.kind) {
|
|
86
|
-
query = query.eq('kind', options.kind) as typeof query
|
|
87
|
-
}
|
|
88
|
-
if (period === 'month') {
|
|
89
|
-
query = query.gte('created_at', startOfMonthISO()) as typeof query
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
// On error OR timeout, fail OPEN (return 0) — a counting failure must never
|
|
93
|
-
// wall a tenant out of their own product, and must never hang the UI awaiting
|
|
94
|
-
// it. Enforcement's source of truth is the DB anyway.
|
|
95
|
-
let timer: ReturnType<typeof setTimeout> | undefined
|
|
96
|
-
const timeout = new Promise<{ count: null; error: unknown; timedOut: true }>((resolve) => {
|
|
97
|
-
timer = setTimeout(() => resolve({ count: null, error: 'timeout', timedOut: true }), COUNT_TIMEOUT_MS)
|
|
98
|
-
})
|
|
99
|
-
const settled = await Promise.race([
|
|
100
|
-
query as unknown as Promise<{ count: number | null; error: unknown }>,
|
|
101
|
-
timeout,
|
|
102
|
-
])
|
|
103
|
-
if (timer) clearTimeout(timer)
|
|
104
|
-
|
|
105
|
-
const { count, error } = settled
|
|
106
|
-
const value = error ? 0 : count ?? 0
|
|
107
|
-
|
|
108
|
-
// A timed-out count is not a fact about the tenant — caching it would make one
|
|
109
|
-
// slow request suppress the guard for the whole TTL.
|
|
110
|
-
if (!(settled as { timedOut?: true }).timedOut) {
|
|
111
|
-
cache.set(key, { value, expires: Date.now() + TTL_MS })
|
|
112
|
-
}
|
|
113
|
-
return value
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
/**
|
|
117
|
-
* Drop cached counts. Called by create handlers (via invalidateLimit) so the
|
|
118
|
-
* next read reflects the row they just added.
|
|
119
|
-
*
|
|
120
|
-
* @param tenantOrKey Substring matched against cache keys (a tenant id, a table
|
|
121
|
-
* name, or any fragment). Omit to clear the entire cache.
|
|
122
|
-
*/
|
|
123
|
-
export function invalidateCount(tenantOrKey?: string): void {
|
|
124
|
-
if (!tenantOrKey) {
|
|
125
|
-
cache.clear()
|
|
126
|
-
return
|
|
127
|
-
}
|
|
128
|
-
for (const key of cache.keys()) {
|
|
129
|
-
if (key.includes(tenantOrKey)) cache.delete(key)
|
|
130
|
-
}
|
|
131
|
-
}
|
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
|
-
}
|