visibility.ts 6.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231
  1. import { z } from 'zod';
  2. import type {
  3. VisibilityCondition,
  4. LogicExpression,
  5. DataModel,
  6. AuthState,
  7. DynamicValue,
  8. } from './types';
  9. import { resolveDynamicValue, DynamicValueSchema } from './types';
  10. // Dynamic value schema for comparisons (number-focused)
  11. const DynamicNumberValueSchema = z.union([
  12. z.number(),
  13. z.object({ path: z.string() }),
  14. ]);
  15. /**
  16. * Logic expression schema (recursive)
  17. * Using a more permissive schema that aligns with runtime behavior
  18. */
  19. export const LogicExpressionSchema: z.ZodType<LogicExpression> = z.lazy(() =>
  20. z.union([
  21. z.object({ and: z.array(LogicExpressionSchema) }),
  22. z.object({ or: z.array(LogicExpressionSchema) }),
  23. z.object({ not: LogicExpressionSchema }),
  24. z.object({ path: z.string() }),
  25. z.object({ eq: z.tuple([DynamicValueSchema, DynamicValueSchema]) }),
  26. z.object({ neq: z.tuple([DynamicValueSchema, DynamicValueSchema]) }),
  27. z.object({ gt: z.tuple([DynamicNumberValueSchema, DynamicNumberValueSchema]) }),
  28. z.object({ gte: z.tuple([DynamicNumberValueSchema, DynamicNumberValueSchema]) }),
  29. z.object({ lt: z.tuple([DynamicNumberValueSchema, DynamicNumberValueSchema]) }),
  30. z.object({ lte: z.tuple([DynamicNumberValueSchema, DynamicNumberValueSchema]) }),
  31. ])
  32. ) as z.ZodType<LogicExpression>;
  33. /**
  34. * Visibility condition schema
  35. */
  36. export const VisibilityConditionSchema: z.ZodType<VisibilityCondition> = z.union([
  37. z.boolean(),
  38. z.object({ path: z.string() }),
  39. z.object({ auth: z.enum(['signedIn', 'signedOut']) }),
  40. LogicExpressionSchema,
  41. ]);
  42. /**
  43. * Context for evaluating visibility
  44. */
  45. export interface VisibilityContext {
  46. dataModel: DataModel;
  47. authState?: AuthState;
  48. }
  49. /**
  50. * Evaluate a logic expression against data and auth state
  51. */
  52. export function evaluateLogicExpression(
  53. expr: LogicExpression,
  54. ctx: VisibilityContext
  55. ): boolean {
  56. const { dataModel } = ctx;
  57. // AND expression
  58. if ('and' in expr) {
  59. return expr.and.every((subExpr) => evaluateLogicExpression(subExpr, ctx));
  60. }
  61. // OR expression
  62. if ('or' in expr) {
  63. return expr.or.some((subExpr) => evaluateLogicExpression(subExpr, ctx));
  64. }
  65. // NOT expression
  66. if ('not' in expr) {
  67. return !evaluateLogicExpression(expr.not, ctx);
  68. }
  69. // Path expression (resolve to boolean)
  70. if ('path' in expr) {
  71. const value = resolveDynamicValue({ path: expr.path }, dataModel);
  72. return Boolean(value);
  73. }
  74. // Equality comparison
  75. if ('eq' in expr) {
  76. const [left, right] = expr.eq;
  77. const leftValue = resolveDynamicValue(left, dataModel);
  78. const rightValue = resolveDynamicValue(right, dataModel);
  79. return leftValue === rightValue;
  80. }
  81. // Not equal comparison
  82. if ('neq' in expr) {
  83. const [left, right] = expr.neq;
  84. const leftValue = resolveDynamicValue(left, dataModel);
  85. const rightValue = resolveDynamicValue(right, dataModel);
  86. return leftValue !== rightValue;
  87. }
  88. // Greater than
  89. if ('gt' in expr) {
  90. const [left, right] = expr.gt;
  91. const leftValue = resolveDynamicValue(left as DynamicValue<number>, dataModel);
  92. const rightValue = resolveDynamicValue(right as DynamicValue<number>, dataModel);
  93. if (typeof leftValue === 'number' && typeof rightValue === 'number') {
  94. return leftValue > rightValue;
  95. }
  96. return false;
  97. }
  98. // Greater than or equal
  99. if ('gte' in expr) {
  100. const [left, right] = expr.gte;
  101. const leftValue = resolveDynamicValue(left as DynamicValue<number>, dataModel);
  102. const rightValue = resolveDynamicValue(right as DynamicValue<number>, dataModel);
  103. if (typeof leftValue === 'number' && typeof rightValue === 'number') {
  104. return leftValue >= rightValue;
  105. }
  106. return false;
  107. }
  108. // Less than
  109. if ('lt' in expr) {
  110. const [left, right] = expr.lt;
  111. const leftValue = resolveDynamicValue(left as DynamicValue<number>, dataModel);
  112. const rightValue = resolveDynamicValue(right as DynamicValue<number>, dataModel);
  113. if (typeof leftValue === 'number' && typeof rightValue === 'number') {
  114. return leftValue < rightValue;
  115. }
  116. return false;
  117. }
  118. // Less than or equal
  119. if ('lte' in expr) {
  120. const [left, right] = expr.lte;
  121. const leftValue = resolveDynamicValue(left as DynamicValue<number>, dataModel);
  122. const rightValue = resolveDynamicValue(right as DynamicValue<number>, dataModel);
  123. if (typeof leftValue === 'number' && typeof rightValue === 'number') {
  124. return leftValue <= rightValue;
  125. }
  126. return false;
  127. }
  128. return false;
  129. }
  130. /**
  131. * Evaluate a visibility condition
  132. */
  133. export function evaluateVisibility(
  134. condition: VisibilityCondition | undefined,
  135. ctx: VisibilityContext
  136. ): boolean {
  137. // No condition = visible
  138. if (condition === undefined) {
  139. return true;
  140. }
  141. // Boolean literal
  142. if (typeof condition === 'boolean') {
  143. return condition;
  144. }
  145. // Path reference
  146. if ('path' in condition && !('and' in condition) && !('or' in condition)) {
  147. const value = resolveDynamicValue({ path: condition.path }, ctx.dataModel);
  148. return Boolean(value);
  149. }
  150. // Auth condition
  151. if ('auth' in condition) {
  152. const isSignedIn = ctx.authState?.isSignedIn ?? false;
  153. if (condition.auth === 'signedIn') {
  154. return isSignedIn;
  155. }
  156. if (condition.auth === 'signedOut') {
  157. return !isSignedIn;
  158. }
  159. return false;
  160. }
  161. // Logic expression
  162. return evaluateLogicExpression(condition as LogicExpression, ctx);
  163. }
  164. /**
  165. * Helper to create visibility conditions
  166. */
  167. export const visibility = {
  168. /** Always visible */
  169. always: true as const,
  170. /** Never visible */
  171. never: false as const,
  172. /** Visible when path is truthy */
  173. when: (path: string): VisibilityCondition => ({ path }),
  174. /** Visible when signed in */
  175. signedIn: { auth: 'signedIn' } as const,
  176. /** Visible when signed out */
  177. signedOut: { auth: 'signedOut' } as const,
  178. /** AND multiple conditions */
  179. and: (...conditions: LogicExpression[]): LogicExpression => ({ and: conditions }),
  180. /** OR multiple conditions */
  181. or: (...conditions: LogicExpression[]): LogicExpression => ({ or: conditions }),
  182. /** NOT a condition */
  183. not: (condition: LogicExpression): LogicExpression => ({ not: condition }),
  184. /** Equality check */
  185. eq: (left: DynamicValue, right: DynamicValue): LogicExpression => ({ eq: [left, right] }),
  186. /** Not equal check */
  187. neq: (left: DynamicValue, right: DynamicValue): LogicExpression => ({ neq: [left, right] }),
  188. /** Greater than */
  189. gt: (left: DynamicValue<number>, right: DynamicValue<number>): LogicExpression => ({ gt: [left, right] }),
  190. /** Greater than or equal */
  191. gte: (left: DynamicValue<number>, right: DynamicValue<number>): LogicExpression => ({ gte: [left, right] }),
  192. /** Less than */
  193. lt: (left: DynamicValue<number>, right: DynamicValue<number>): LogicExpression => ({ lt: [left, right] }),
  194. /** Less than or equal */
  195. lte: (left: DynamicValue<number>, right: DynamicValue<number>): LogicExpression => ({ lte: [left, right] }),
  196. };