@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.
Files changed (170) hide show
  1. package/README.md +2 -2
  2. package/dist/access/index.js.map +1 -1
  3. package/dist/{chunk-L4TLMYGC.js → chunk-6RBW2Y5K.js} +3 -3
  4. package/dist/chunk-6RBW2Y5K.js.map +1 -0
  5. package/dist/chunk-732GZEUI.js +1592 -0
  6. package/dist/chunk-732GZEUI.js.map +1 -0
  7. package/dist/{chunk-X5THHG6T.js → chunk-CPMHCTX3.js} +3 -3
  8. package/dist/chunk-CPMHCTX3.js.map +1 -0
  9. package/dist/{chunk-V2MSAUHO.js → chunk-JO7Z2AOE.js} +2 -2
  10. package/dist/chunk-JO7Z2AOE.js.map +1 -0
  11. package/dist/{chunk-5RJVUC6J.js → chunk-M27JPX3K.js} +12 -3
  12. package/dist/chunk-M27JPX3K.js.map +1 -0
  13. package/dist/chunk-OXQGRJZT.js +125 -0
  14. package/dist/chunk-OXQGRJZT.js.map +1 -0
  15. package/dist/data/backend-env.d.ts +38 -0
  16. package/dist/data/backend-env.d.ts.map +1 -0
  17. package/dist/data/index.js +1 -1
  18. package/dist/entity/events.d.ts +14 -0
  19. package/dist/entity/events.d.ts.map +1 -0
  20. package/dist/events/envelope.d.ts +31 -0
  21. package/dist/events/envelope.d.ts.map +1 -0
  22. package/dist/events/index.d.ts +4 -0
  23. package/dist/events/index.d.ts.map +1 -1
  24. package/dist/events/log.d.ts +44 -0
  25. package/dist/events/log.d.ts.map +1 -0
  26. package/dist/events/types.d.ts +40 -0
  27. package/dist/events/types.d.ts.map +1 -0
  28. package/dist/i18n/index.d.ts +10 -0
  29. package/dist/i18n/index.d.ts.map +1 -1
  30. package/dist/i18n/index.js +1 -1
  31. package/dist/index.d.ts +27 -17
  32. package/dist/index.d.ts.map +1 -1
  33. package/dist/index.js +388 -52
  34. package/dist/index.js.map +1 -1
  35. package/dist/integrations/index.js +1 -1
  36. package/dist/{integrations/index.d.ts → integrations.d.ts} +2 -2
  37. package/dist/integrations.d.ts.map +1 -0
  38. package/dist/{address/index.d.ts → lib/address.d.ts} +1 -1
  39. package/dist/{address/index.d.ts.map → lib/address.d.ts.map} +1 -1
  40. package/dist/{phone/index.d.ts → lib/phone.d.ts} +1 -1
  41. package/dist/lib/phone.d.ts.map +1 -0
  42. package/dist/{seo/index.d.ts → lib/seo.d.ts} +18 -1
  43. package/dist/lib/seo.d.ts.map +1 -0
  44. package/dist/{tenant/index.d.ts → lib/tenant.d.ts} +1 -1
  45. package/dist/lib/tenant.d.ts.map +1 -0
  46. package/dist/manifest/index.d.ts +1 -1
  47. package/dist/manifest/index.d.ts.map +1 -1
  48. package/dist/messaging/index.d.ts +3 -0
  49. package/dist/messaging/index.d.ts.map +1 -0
  50. package/dist/messaging/registry.d.ts +35 -0
  51. package/dist/messaging/registry.d.ts.map +1 -0
  52. package/dist/messaging/types.d.ts +67 -0
  53. package/dist/messaging/types.d.ts.map +1 -0
  54. package/dist/plugin/index.js +2 -1
  55. package/dist/plugin/runtime.d.ts +17 -0
  56. package/dist/plugin/runtime.d.ts.map +1 -1
  57. package/dist/registry/block-catalog.d.ts +78 -0
  58. package/dist/registry/block-catalog.d.ts.map +1 -0
  59. package/dist/registry/block-constraints.d.ts +64 -0
  60. package/dist/registry/block-constraints.d.ts.map +1 -0
  61. package/dist/registry/block-schema.d.ts +69 -0
  62. package/dist/registry/block-schema.d.ts.map +1 -0
  63. package/dist/registry/blocks.d.ts +84 -0
  64. package/dist/registry/blocks.d.ts.map +1 -0
  65. package/dist/registry/entities.d.ts +153 -0
  66. package/dist/registry/entities.d.ts.map +1 -0
  67. package/dist/registry/extension-points.d.ts +117 -0
  68. package/dist/registry/extension-points.d.ts.map +1 -0
  69. package/dist/registry/handles.d.ts +41 -0
  70. package/dist/registry/handles.d.ts.map +1 -0
  71. package/dist/registry/index.d.ts +41 -41
  72. package/dist/registry/index.d.ts.map +1 -1
  73. package/dist/registry/loaders.d.ts +55 -0
  74. package/dist/registry/loaders.d.ts.map +1 -0
  75. package/dist/registry/registry-class.d.ts +157 -0
  76. package/dist/registry/registry-class.d.ts.map +1 -0
  77. package/dist/registry/schema-check.d.ts +14 -0
  78. package/dist/registry/schema-check.d.ts.map +1 -0
  79. package/dist/router/index.d.ts +78 -1
  80. package/dist/router/index.d.ts.map +1 -1
  81. package/dist/router/index.js +1 -1
  82. package/dist/testing/index.js +1 -1
  83. package/dist/{testing/index.d.ts → testing.d.ts} +3 -3
  84. package/dist/testing.d.ts.map +1 -0
  85. package/dist/types/analytics.d.ts +196 -0
  86. package/dist/types/analytics.d.ts.map +1 -0
  87. package/dist/types/crud.d.ts +22 -2
  88. package/dist/types/crud.d.ts.map +1 -1
  89. package/dist/types/plugins.d.ts +55 -1
  90. package/dist/types/plugins.d.ts.map +1 -1
  91. package/package.json +6 -18
  92. package/dist/blocks/index.d.ts +0 -25
  93. package/dist/blocks/index.d.ts.map +0 -1
  94. package/dist/chunk-5RJVUC6J.js.map +0 -1
  95. package/dist/chunk-L4TLMYGC.js.map +0 -1
  96. package/dist/chunk-V2MSAUHO.js.map +0 -1
  97. package/dist/chunk-X5THHG6T.js.map +0 -1
  98. package/dist/chunk-Y2C2FBQP.js +0 -35
  99. package/dist/chunk-Y2C2FBQP.js.map +0 -1
  100. package/dist/chunk-YH25Y4FW.js +0 -311
  101. package/dist/chunk-YH25Y4FW.js.map +0 -1
  102. package/dist/integrations/index.d.ts.map +0 -1
  103. package/dist/phone/index.d.ts.map +0 -1
  104. package/dist/seo/index.d.ts.map +0 -1
  105. package/dist/tenant/index.d.ts.map +0 -1
  106. package/dist/testing/index.d.ts.map +0 -1
  107. package/src/access/denial.ts +0 -33
  108. package/src/access/engine.test.ts +0 -48
  109. package/src/access/engine.ts +0 -136
  110. package/src/access/index.ts +0 -14
  111. package/src/access/limits.ts +0 -18
  112. package/src/address/index.ts +0 -188
  113. package/src/app/render.tsx +0 -69
  114. package/src/blocks/index.tsx +0 -68
  115. package/src/data/archetype.ts +0 -255
  116. package/src/data/bulk.ts +0 -35
  117. package/src/data/cached.ts +0 -55
  118. package/src/data/count.test.ts +0 -152
  119. package/src/data/count.ts +0 -131
  120. package/src/data/filters.test.ts +0 -70
  121. package/src/data/filters.ts +0 -90
  122. package/src/data/index.ts +0 -11
  123. package/src/data/mock.ts +0 -90
  124. package/src/data/platform-api.ts +0 -209
  125. package/src/data/refresh.ts +0 -115
  126. package/src/data/resolve.ts +0 -126
  127. package/src/data/supabase.ts +0 -230
  128. package/src/data/types.ts +0 -50
  129. package/src/entity/index.ts +0 -2
  130. package/src/entity/registry.ts +0 -49
  131. package/src/events/index.tsx +0 -82
  132. package/src/i18n/index.ts +0 -476
  133. package/src/i18n/shell-translations.en.ts +0 -875
  134. package/src/i18n/shell-translations.pt-br.ts +0 -873
  135. package/src/index.ts +0 -217
  136. package/src/integrations/index.ts +0 -237
  137. package/src/lib/cache.ts +0 -118
  138. package/src/lib/csv.ts +0 -54
  139. package/src/lib/format.ts +0 -58
  140. package/src/manifest/app-manifest.schema.json +0 -676
  141. package/src/manifest/index.ts +0 -518
  142. package/src/manifest/serialize.ts +0 -52
  143. package/src/phone/index.ts +0 -85
  144. package/src/plugin/createSafeDataProvider.ts +0 -30
  145. package/src/plugin/index.ts +0 -1
  146. package/src/plugin/runtime.ts +0 -331
  147. package/src/registry/index.ts +0 -163
  148. package/src/router/history.ts +0 -86
  149. package/src/router/index.ts +0 -29
  150. package/src/runtime/index.ts +0 -15
  151. package/src/runtime/oauth.ts +0 -219
  152. package/src/search/engine.ts +0 -559
  153. package/src/search/index.ts +0 -25
  154. package/src/search/text.ts +0 -206
  155. package/src/search/types.ts +0 -75
  156. package/src/seo/index.ts +0 -128
  157. package/src/tenant/index.ts +0 -38
  158. package/src/testing/index.ts +0 -116
  159. package/src/types/auth.ts +0 -41
  160. package/src/types/billing.ts +0 -39
  161. package/src/types/crud.ts +0 -184
  162. package/src/types/entities.ts +0 -142
  163. package/src/types/entitlements.ts +0 -45
  164. package/src/types/i18n.ts +0 -7
  165. package/src/types/index.ts +0 -11
  166. package/src/types/org.ts +0 -137
  167. package/src/types/payment.ts +0 -44
  168. package/src/types/permissions.ts +0 -32
  169. package/src/types/plugins.ts +0 -480
  170. 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
- }
@@ -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
- })
@@ -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
- }
@@ -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
- }