gymmonk-schema 0.56.0 → 0.58.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 (64) hide show
  1. package/dist/center.d.ts +1 -1
  2. package/dist/center.d.ts.map +1 -1
  3. package/dist/center.js +16 -3
  4. package/dist/center.js.map +1 -1
  5. package/dist/common.d.ts +14 -0
  6. package/dist/common.d.ts.map +1 -1
  7. package/dist/common.js +20 -0
  8. package/dist/common.js.map +1 -1
  9. package/dist/diet-chart.d.ts +59 -4
  10. package/dist/diet-chart.d.ts.map +1 -1
  11. package/dist/diet-chart.js +24 -4
  12. package/dist/diet-chart.js.map +1 -1
  13. package/dist/equipment.d.ts +2 -2
  14. package/dist/equipment.d.ts.map +1 -1
  15. package/dist/equipment.js +16 -5
  16. package/dist/equipment.js.map +1 -1
  17. package/dist/exercise.d.ts +153 -19
  18. package/dist/exercise.d.ts.map +1 -1
  19. package/dist/exercise.js +91 -7
  20. package/dist/exercise.js.map +1 -1
  21. package/dist/food.d.ts +549 -13
  22. package/dist/food.d.ts.map +1 -1
  23. package/dist/food.js +533 -6
  24. package/dist/food.js.map +1 -1
  25. package/dist/gym-registration.d.ts +1 -0
  26. package/dist/gym-registration.d.ts.map +1 -1
  27. package/dist/index.d.ts +1 -0
  28. package/dist/index.d.ts.map +1 -1
  29. package/dist/index.js +2 -0
  30. package/dist/index.js.map +1 -1
  31. package/dist/member-profile.d.ts +4 -8
  32. package/dist/member-profile.d.ts.map +1 -1
  33. package/dist/member-profile.js +16 -2
  34. package/dist/member-profile.js.map +1 -1
  35. package/dist/member.d.ts +7 -7
  36. package/dist/member.d.ts.map +1 -1
  37. package/dist/member.js +15 -3
  38. package/dist/member.js.map +1 -1
  39. package/dist/membership-plan.d.ts +25 -15
  40. package/dist/membership-plan.d.ts.map +1 -1
  41. package/dist/membership-plan.js +16 -3
  42. package/dist/membership-plan.js.map +1 -1
  43. package/dist/membership-request.d.ts.map +1 -1
  44. package/dist/membership-request.js +4 -2
  45. package/dist/membership-request.js.map +1 -1
  46. package/dist/nutrition.d.ts +49 -0
  47. package/dist/nutrition.d.ts.map +1 -1
  48. package/dist/nutrition.js +61 -0
  49. package/dist/nutrition.js.map +1 -1
  50. package/dist/onboarding.d.ts +1 -0
  51. package/dist/onboarding.d.ts.map +1 -1
  52. package/dist/organisation.d.ts +4 -0
  53. package/dist/organisation.d.ts.map +1 -1
  54. package/dist/organisation.js +18 -0
  55. package/dist/organisation.js.map +1 -1
  56. package/dist/upi.d.ts +166 -0
  57. package/dist/upi.d.ts.map +1 -0
  58. package/dist/upi.js +200 -0
  59. package/dist/upi.js.map +1 -0
  60. package/dist/workout-plan.d.ts +3 -3
  61. package/dist/workout-plan.d.ts.map +1 -1
  62. package/dist/workout-plan.js +18 -2
  63. package/dist/workout-plan.js.map +1 -1
  64. package/package.json +1 -1
package/dist/center.d.ts CHANGED
@@ -311,7 +311,7 @@ export declare const updateCenterBodySchema: z.ZodObject<{
311
311
  to: z.ZodString;
312
312
  }, z.core.$strip>;
313
313
  }, z.core.$strip>>;
314
- photos: z.ZodOptional<z.ZodDefault<z.ZodArray<z.ZodString>>>;
314
+ photos: z.ZodOptional<z.ZodArray<z.ZodString>>;
315
315
  }, z.core.$strip>;
316
316
  export type UpdateCenterBody = z.infer<typeof updateCenterBodySchema>;
317
317
  export declare const centerSchema: z.ZodObject<{
@@ -1 +1 @@
1
- {"version":3,"file":"center.d.ts","sourceRoot":"","sources":["../src/center.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAexB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2CG;AACH,eAAO,MAAM,YAAY,oTAgCf,CAAC;AACX,eAAO,MAAM,gBAAgB;;;;;;;;;;;;;;;;;;;;;;;;;EAAuB,CAAC;AACrD,MAAM,MAAM,UAAU,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,gBAAgB,CAAC,CAAC;AAI1D,eAAO,MAAM,kBAAkB;;;EAAiC,CAAC;AACjE,MAAM,MAAM,YAAY,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,kBAAkB,CAAC,CAAC;AAM9D,OAAO,EAAE,KAAK,UAAU,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAC;AAI/D,eAAO,MAAM,sBAAsB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBAqCjC,CAAC;AACH,MAAM,MAAM,gBAAgB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,sBAAsB,CAAC,CAAC;AAEtE,eAAO,MAAM,sBAAsB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBAAmC,CAAC;AACvE,MAAM,MAAM,gBAAgB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,sBAAsB,CAAC,CAAC;AAItE,eAAO,MAAM,YAAY;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBAqDvB,CAAC;AACH,MAAM,MAAM,MAAM,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,YAAY,CAAC,CAAC;AAIlD;;;;;;;GAOG;AACH,eAAO,MAAM,iBAAiB;;;;iBAI5B,CAAC;AACH,MAAM,MAAM,WAAW,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,iBAAiB,CAAC,CAAC;AAI5D,yEAAyE;AACzE,eAAO,MAAM,eAAe;;;;;;;;;iBAkB1B,CAAC;AACH,MAAM,MAAM,SAAS,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,eAAe,CAAC,CAAC;AAExD;;;;;;;;;;;;;;GAcG;AACH,eAAO,MAAM,sBAAsB,KAAK,CAAC;AAEzC;;;;;;;;GAQG;AACH,eAAO,MAAM,2BAA2B,KAAK,CAAC;AAE9C,eAAO,MAAM,oBAAoB;;;;;;;;;iBAwC9B,CAAC;AACJ,MAAM,MAAM,cAAc,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,oBAAoB,CAAC,CAAC;AAElE;;;;;;GAMG;AACH,eAAO,MAAM,qBAAqB;;;;;;;;;;;;;iBAIhC,CAAC;AACH,MAAM,MAAM,eAAe,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,qBAAqB,CAAC,CAAC;AAKpE,eAAO,MAAM,qBAAqB;;;;iBAIhC,CAAC;AACH,MAAM,MAAM,eAAe,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,qBAAqB,CAAC,CAAC"}
1
+ {"version":3,"file":"center.d.ts","sourceRoot":"","sources":["../src/center.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAexB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2CG;AACH,eAAO,MAAM,YAAY,oTAgCf,CAAC;AACX,eAAO,MAAM,gBAAgB;;;;;;;;;;;;;;;;;;;;;;;;;EAAuB,CAAC;AACrD,MAAM,MAAM,UAAU,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,gBAAgB,CAAC,CAAC;AAI1D,eAAO,MAAM,kBAAkB;;;EAAiC,CAAC;AACjE,MAAM,MAAM,YAAY,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,kBAAkB,CAAC,CAAC;AAM9D,OAAO,EAAE,KAAK,UAAU,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAC;AAqD/D,eAAO,MAAM,sBAAsB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBAEjC,CAAC;AACH,MAAM,MAAM,gBAAgB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,sBAAsB,CAAC,CAAC;AAEtE,eAAO,MAAM,sBAAsB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBAAiC,CAAC;AACrE,MAAM,MAAM,gBAAgB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,sBAAsB,CAAC,CAAC;AAItE,eAAO,MAAM,YAAY;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBAqDvB,CAAC;AACH,MAAM,MAAM,MAAM,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,YAAY,CAAC,CAAC;AAIlD;;;;;;;GAOG;AACH,eAAO,MAAM,iBAAiB;;;;iBAI5B,CAAC;AACH,MAAM,MAAM,WAAW,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,iBAAiB,CAAC,CAAC;AAI5D,yEAAyE;AACzE,eAAO,MAAM,eAAe;;;;;;;;;iBAkB1B,CAAC;AACH,MAAM,MAAM,SAAS,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,eAAe,CAAC,CAAC;AAExD;;;;;;;;;;;;;;GAcG;AACH,eAAO,MAAM,sBAAsB,KAAK,CAAC;AAEzC;;;;;;;;GAQG;AACH,eAAO,MAAM,2BAA2B,KAAK,CAAC;AAE9C,eAAO,MAAM,oBAAoB;;;;;;;;;iBAwC9B,CAAC;AACJ,MAAM,MAAM,cAAc,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,oBAAoB,CAAC,CAAC;AAElE;;;;;;GAMG;AACH,eAAO,MAAM,qBAAqB;;;;;;;;;;;;;iBAIhC,CAAC;AACH,MAAM,MAAM,eAAe,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,qBAAqB,CAAC,CAAC;AAKpE,eAAO,MAAM,qBAAqB;;;;iBAIhC,CAAC;AACH,MAAM,MAAM,eAAe,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,qBAAqB,CAAC,CAAC"}
package/dist/center.js CHANGED
@@ -99,7 +99,17 @@ export const centerStatusSchema = z.enum(['active', 'inactive']);
99
99
  // because a centre's code is a fact about a centre.
100
100
  export { centerCodeSchema } from './codes.js';
101
101
  // ─── Create / update bodies ──────────────────────────────────────────────────
102
- export const createCenterBodySchema = z.object({
102
+ /**
103
+ * ⚠️ NO `.default()` IN THIS BASE — the update schema is derived from it.
104
+ *
105
+ * `.partial()` makes a key optional; it does NOT switch off that key's
106
+ * `.default()`. A base carrying a default therefore parses a partial update
107
+ * body into an object containing that key, and the update services `$set` the
108
+ * parsed body straight through — so editing one field silently erases another
109
+ * the caller never mentioned. Defaults live on the CREATE export below.
110
+ * Pinned by `update-body-clobber.test.ts`.
111
+ */
112
+ const centerBodyBaseSchema = z.object({
103
113
  name: z.string().trim().min(1, 'Center name is required').max(160),
104
114
  description: optionalTrimmedString(2000),
105
115
  types: z.array(centerTypeSchema).min(1, 'Pick at least one'),
@@ -121,7 +131,7 @@ export const createCenterBodySchema = z.object({
121
131
  website: optionalTrimmedString(200),
122
132
  hours: weeklyHoursSchema,
123
133
  /** Uploaded gallery image URLs. */
124
- photos: z.array(z.string().trim().max(2000)).max(20).default([]),
134
+ photos: z.array(z.string().trim().max(2000)).max(20),
125
135
  /**
126
136
  * A pasted Google Maps link. The server reads the coordinates out of it; the
127
137
  * client never sends them.
@@ -137,7 +147,10 @@ export const createCenterBodySchema = z.object({
137
147
  */
138
148
  checkInRadiusM: z.number().int().min(50).max(5000).optional(),
139
149
  });
140
- export const updateCenterBodySchema = createCenterBodySchema.partial();
150
+ export const createCenterBodySchema = centerBodyBaseSchema.extend({
151
+ photos: z.array(z.string().trim().max(2000)).max(20).default([]),
152
+ });
153
+ export const updateCenterBodySchema = centerBodyBaseSchema.partial();
141
154
  // ─── Entity (API response) ───────────────────────────────────────────────────
142
155
  export const centerSchema = z.object({
143
156
  id: objectIdSchema,
@@ -1 +1 @@
1
- {"version":3,"file":"center.js","sourceRoot":"","sources":["../src/center.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * gymmonk-schema — Center (branch)\n * ================================\n * A physical gym location belonging to an organisation. Carries the public gym\n * code members use to join, opening hours, equipment scope, and contact info.\n * v1 memberships are single-center, so a Center is the unit a member joins.\n *\n * @module gymmonk-schema/center\n */\n\nimport { z } from 'zod';\nimport { centerCodeSchema } from './codes.js';\nimport {\n isoDateTimeSchema,\n objectIdSchema,\n optionalEmailSchema,\n optionalPhoneSchema,\n optionalTrimmedString,\n phoneSchema,\n} from './common.js';\nimport { mapLinkInputSchema, placePinSchema, timezoneSchema } from './location.js';\nimport { addressSchema, weeklyHoursSchema } from './shared.js';\n\n// ─── Center type ─────────────────────────────────────────────────────────────\n\n/**\n * What a centre OFFERS, multi-select.\n *\n * ── The axis, and why `personal-training` is gone ───────────────────────────\n * Every value here answers one question: what does somebody DO at this place.\n * That makes them disciplines and training modalities, so an owner ticking\n * three of them describes a real branch and a member reading them knows what\n * they are walking into.\n *\n * `personal-training` answered a different question — HOW coaching is\n * delivered, one-to-one instead of in a class — and nearly every gym in the\n * list offers it. A value almost everyone can tick separates nobody from\n * anybody, which is the opposite of what a type is for. Business model\n * (big-box, boutique, 24-hour) is a third axis again, and one this product\n * already answers better elsewhere: floor area and opening hours are recorded\n * fields, not a guess from a chip.\n *\n * ── Where the list comes from ───────────────────────────────────────────────\n * Cross-referenced rather than invented: Google Business Profile's fitness\n * categories (what gyms actually file themselves under, and therefore what\n * members search), ClassPass and Mindbody's activity taxonomies, and the\n * formats the Indian market actually runs — cult.fit's S&C, HRX, boxing, dance\n * fitness and yoga, plus the bodybuilding and martial-arts traditions that a\n * list drawn only from US sources would miss entirely.\n *\n * Deliberately NOT included, so the next person does not re-litigate it:\n * rock climbing, gymnastics and aerial/pole are real facility categories but\n * not gyms in the sense this product manages; women-only and kids' fitness\n * describe an AUDIENCE, not an activity; 24-hour and big-box describe hours\n * and size, both of which are already their own fields.\n *\n * Values are stable slugs — the LABELS are the frontend's business, so\n * rewording a chip never rewrites stored data.\n *\n * ── The field is called `types`; the screen says \"What this center offers\" ──\n * That divergence is deliberate, not an oversight. \"Type\" reads as one thing a\n * place IS, and a real branch runs several — a weights floor, a yoga room and\n * Zumba on weekends — so a singular-sounding label pushed owners to pick one\n * and misdescribe themselves. (The picker was once a single-select with a\n * \"Mixed\" option that saved as plain `gym`, which is how a CrossFit-and-boxing\n * branch came back as neither.) The label was changed to match what is stored;\n * the identifiers were not, because the values are a wire contract and\n * renaming them would rewrite data to fix a word.\n */\nexport const CENTER_TYPES = [\n // General\n 'gym',\n // Strength\n 'strength',\n 'crossfit',\n 'functional',\n 'bodybuilding',\n 'powerlifting',\n 'calisthenics',\n // Cardio & group training\n 'cardio',\n 'hiit',\n 'spinning',\n 'bootcamp',\n 'aerobics',\n 'sports-conditioning',\n // Mind & body\n 'yoga',\n 'pilates',\n 'meditation',\n // Dance\n 'zumba',\n 'dance',\n // Combat\n 'boxing',\n 'kickboxing',\n 'mma',\n 'martial-arts',\n // Facilities\n 'swimming',\n 'physiotherapy',\n] as const;\nexport const centerTypeSchema = z.enum(CENTER_TYPES);\nexport type CenterType = z.infer<typeof centerTypeSchema>;\n\n// ─── Status ──────────────────────────────────────────────────────────────────\n\nexport const centerStatusSchema = z.enum(['active', 'inactive']);\nexport type CenterStatus = z.infer<typeof centerStatusSchema>;\n\n// The public gym code (`IRN-1-7K2M`) is defined in `codes.ts` alongside the\n// member ids built on the same stem, so the four identifier shapes this product\n// issues can be read — and kept from colliding — in one place. Re-exported here\n// because a centre's code is a fact about a centre.\nexport { type CenterCode, centerCodeSchema } from './codes.js';\n\n// ─── Create / update bodies ──────────────────────────────────────────────────\n\nexport const createCenterBodySchema = z.object({\n name: z.string().trim().min(1, 'Center name is required').max(160),\n description: optionalTrimmedString(2000),\n types: z.array(centerTypeSchema).min(1, 'Pick at least one'),\n address: addressSchema,\n /** Usable floor area in square feet. */\n floorAreaSqft: z.number().int().min(0).max(1_000_000).optional(),\n /**\n * How many people the branch holds at once.\n *\n * The denominator behind \"how full are we\" — the centre detail screen shows\n * it beside the active-member count, and crowd-load has no meaning without\n * it. Optional: a gym that has never measured it should not be blocked from\n * saving the rest of its details.\n */\n capacity: z.number().int().min(1).max(100_000).optional(),\n primaryPhone: phoneSchema,\n secondaryPhone: optionalPhoneSchema,\n email: optionalEmailSchema,\n website: optionalTrimmedString(200),\n hours: weeklyHoursSchema,\n /** Uploaded gallery image URLs. */\n photos: z.array(z.string().trim().max(2000)).max(20).default([]),\n /**\n * A pasted Google Maps link. The server reads the coordinates out of it; the\n * client never sends them.\n *\n * This is the pin attendance is measured against, and it is NOT the same as\n * `address.coords`: that is an area centroid resolved from master data, which\n * can be a whole district's midpoint and is useless for proximity.\n */\n ...mapLinkInputSchema.shape,\n /**\n * How close a member must be for a check-in to count, metres. Optional — the\n * server applies its own default when a gym has not chosen one.\n */\n checkInRadiusM: z.number().int().min(50).max(5000).optional(),\n});\nexport type CreateCenterBody = z.infer<typeof createCenterBodySchema>;\n\nexport const updateCenterBodySchema = createCenterBodySchema.partial();\nexport type UpdateCenterBody = z.infer<typeof updateCenterBodySchema>;\n\n// ─── Entity (API response) ───────────────────────────────────────────────────\n\nexport const centerSchema = z.object({\n id: objectIdSchema,\n organisationId: objectIdSchema,\n /** Public join code, `IRN-1-7K2M`. */\n code: centerCodeSchema,\n /**\n * Which centre this is within its organisation — 1 for the first branch the\n * owner opened, 2 for the next.\n *\n * Stored rather than counted on read, because it is baked into this centre's\n * code AND into every member id the centre issues. Deleting branch 2 must not\n * renumber branch 3 into codes that are already printed.\n */\n sequence: z.number().int().positive(),\n name: z.string(),\n description: z.string().nullable(),\n types: z.array(centerTypeSchema),\n address: addressSchema,\n floorAreaSqft: z.number().int().nullable(),\n capacity: z.number().int().nullable(),\n primaryPhone: z.string(),\n secondaryPhone: z.string().nullable(),\n email: z.string().nullable(),\n website: z.string().nullable(),\n hours: weeklyHoursSchema,\n photos: z.array(z.string()),\n status: centerStatusSchema,\n /** Denormalised active-member count for roster headers. */\n memberCount: z.number().int().nonnegative(),\n // The resolved pin. Absent until an owner pastes a Maps link, which is why\n // attendance is unenforceable rather than failing for a gym without one.\n ...placePinSchema.shape,\n checkInRadiusM: z.number().int().nullable().optional(),\n /**\n * The centre's own timezone, as an IANA identifier (\"Asia/Kolkata\").\n *\n * WHY THE CENTRE'S AND NOT THE VIEWER'S. Attendance is filed under a calendar\n * DAY, and a day is not timezone-free — whichever zone derives it defines when\n * it starts. Deriving it from whoever is looking would put one visit on\n * different days for the member and the owner, and would make an owner's daily\n * roll-up change depending on where they opened the app. A visit belongs to the\n * gym's operating day; only the display of times follows the reader.\n *\n * An IANA NAME, never a UTC offset. Offsets are a property of an instant, not\n * of a place: they move with DST, so a stored \"+05:30\" is already wrong for\n * half the year anywhere that observes it.\n *\n * Server-derived from the centre's pin, never accepted from a client — the\n * same rule the coordinates follow, and for the same reason.\n */\n timezone: timezoneSchema,\n createdAt: isoDateTimeSchema,\n updatedAt: isoDateTimeSchema,\n});\nexport type Center = z.infer<typeof centerSchema>;\n\n// ─── Center stats (owner detail header) ──────────────────────────────────────\n\n/**\n * The three-up header on a centre's detail screen.\n *\n * Computed, never stored — `activeMembers` counts live memberships and\n * `monthlyRevenueInr` sums this month's successful payments, so the card can\n * never drift from the ledger the way a denormalised total would. `capacity`\n * is the one stored value, and is null until the owner fills it in.\n */\nexport const centerStatsSchema = z.object({\n capacity: z.number().int().nullable(),\n activeMembers: z.number().int().nonnegative(),\n monthlyRevenueInr: z.number().nonnegative(),\n});\nexport type CenterStats = z.infer<typeof centerStatsSchema>;\n\n// ─── Gym directory (member \"find a gym\") ─────────────────────────────────────\n\n/** A lightweight public directory result for the \"find a gym\" screen. */\nexport const nearbyGymSchema = z.object({\n id: objectIdSchema,\n code: centerCodeSchema,\n name: z.string(),\n /** The narrowest place name we hold (locality → district → state). */\n area: z.string(),\n district: z.string().nullable(),\n state: z.string().nullable(),\n /**\n * Kilometres from the coordinates the caller supplied.\n *\n * NULL, not 0, when it cannot be computed — the caller sent no position, or\n * the gym's address carries no coordinates. Zero is a real distance (\"you are\n * standing in it\") and using it as the unknown value put every un-geocoded\n * gym at the top of a nearest-first list.\n */\n distanceKm: z.number().nonnegative().nullable(),\n memberCount: z.number().int().nonnegative().optional(),\n});\nexport type NearbyGym = z.infer<typeof nearbyGymSchema>;\n\n/**\n * The gym directory has THREE ways in, and they differ in how far they reach.\n *\n * `code` exact public gym id — matches anywhere in the country, because\n * a member holding the id already knows which gym they mean and\n * may well be joining one in another state.\n * `q` name search — deliberately narrowed to ONE STATE. Gym names\n * repeat heavily (\"Fitness First\"), so a nationwide name search\n * returns a list nobody can pick from.\n * `districtId` browse — the gyms around them, nearest first when they share\n * their position.\n *\n * `stateId` / `districtId` are master-area ids (`area.ts`), matched against the\n * address `ancestry` array every center stores.\n */\nexport const GYM_SEARCH_MAX_RESULTS = 20;\n\n/**\n * How far \"near me\" reaches, in kilometres.\n *\n * A bound rather than \"the nearest 20 wherever they are\". Without one, a member\n * in a town with no gyms is shown the nearest one 400km away as though it were\n * a suggestion — a list that is technically sorted and practically useless. The\n * empty state points them at a different location or a gym id instead, which\n * are the two things that can actually help.\n */\nexport const GYM_SEARCH_NEARBY_RADIUS_KM = 50;\n\nexport const gymSearchQuerySchema = z\n .object({\n /** Exact public gym id, e.g. `GYM-2025-001`. Unscoped by geography. */\n code: optionalTrimmedString(40),\n /** Name fragment. Requires `stateId`. */\n q: optionalTrimmedString(120),\n stateId: optionalTrimmedString(40),\n districtId: optionalTrimmedString(40),\n /**\n * Any master-area id — state, district, or one of the eight sub-district\n * types the location drawer can reach.\n *\n * Matched against the `ancestry` every centre stores, exactly as\n * `stateId` / `districtId` are, so one parameter covers every depth the\n * picker offers. Those two stay because callers already send them and they\n * read more clearly at the two levels that have their own control.\n */\n areaId: optionalTrimmedString(40),\n lat: z.coerce.number().min(-90).max(90).optional(),\n lng: z.coerce.number().min(-180).max(180).optional(),\n limit: z.coerce\n .number()\n .int()\n .min(1)\n .max(GYM_SEARCH_MAX_RESULTS)\n .default(GYM_SEARCH_MAX_RESULTS),\n })\n .refine((v) => !v.q || Boolean(v.stateId), {\n message: 'Pick a state to search gyms by name',\n path: ['stateId'],\n })\n .refine(\n (v) =>\n Boolean(\n v.code || v.q || v.stateId || v.districtId || v.areaId || (v.lat != null && v.lng != null),\n ),\n {\n message: 'Enter a gym id, or pick where to look',\n path: ['stateId'],\n },\n );\nexport type GymSearchQuery = z.infer<typeof gymSearchQuerySchema>;\n\n/**\n * The directory response.\n *\n * `truncated` says the district holds more gyms than were returned, which is\n * what the screen turns into \"showing the 20 nearest — enter a gym id if yours\n * is not here\" rather than letting the member assume their gym is absent.\n */\nexport const gymSearchResultSchema = z.object({\n items: z.array(nearbyGymSchema),\n total: z.number().int().nonnegative(),\n truncated: z.boolean(),\n});\nexport type GymSearchResult = z.infer<typeof gymSearchResultSchema>;\n\n// ─── Retained: nearby-gyms query ─────────────────────────────────────────────\n// Kept for the gym-search route the owner console's repo still serves. See the\n// note in `onboarding.ts`.\nexport const nearbyGymsQuerySchema = z.object({\n lat: z.coerce.number().min(-90).max(90).optional(),\n lng: z.coerce.number().min(-180).max(180).optional(),\n q: optionalTrimmedString(120),\n});\nexport type NearbyGymsQuery = z.infer<typeof nearbyGymsQuerySchema>;\n"]}
1
+ {"version":3,"file":"center.js","sourceRoot":"","sources":["../src/center.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * gymmonk-schema — Center (branch)\n * ================================\n * A physical gym location belonging to an organisation. Carries the public gym\n * code members use to join, opening hours, equipment scope, and contact info.\n * v1 memberships are single-center, so a Center is the unit a member joins.\n *\n * @module gymmonk-schema/center\n */\n\nimport { z } from 'zod';\nimport { centerCodeSchema } from './codes.js';\nimport {\n isoDateTimeSchema,\n objectIdSchema,\n optionalEmailSchema,\n optionalPhoneSchema,\n optionalTrimmedString,\n phoneSchema,\n} from './common.js';\nimport { mapLinkInputSchema, placePinSchema, timezoneSchema } from './location.js';\nimport { addressSchema, weeklyHoursSchema } from './shared.js';\n\n// ─── Center type ─────────────────────────────────────────────────────────────\n\n/**\n * What a centre OFFERS, multi-select.\n *\n * ── The axis, and why `personal-training` is gone ───────────────────────────\n * Every value here answers one question: what does somebody DO at this place.\n * That makes them disciplines and training modalities, so an owner ticking\n * three of them describes a real branch and a member reading them knows what\n * they are walking into.\n *\n * `personal-training` answered a different question — HOW coaching is\n * delivered, one-to-one instead of in a class — and nearly every gym in the\n * list offers it. A value almost everyone can tick separates nobody from\n * anybody, which is the opposite of what a type is for. Business model\n * (big-box, boutique, 24-hour) is a third axis again, and one this product\n * already answers better elsewhere: floor area and opening hours are recorded\n * fields, not a guess from a chip.\n *\n * ── Where the list comes from ───────────────────────────────────────────────\n * Cross-referenced rather than invented: Google Business Profile's fitness\n * categories (what gyms actually file themselves under, and therefore what\n * members search), ClassPass and Mindbody's activity taxonomies, and the\n * formats the Indian market actually runs — cult.fit's S&C, HRX, boxing, dance\n * fitness and yoga, plus the bodybuilding and martial-arts traditions that a\n * list drawn only from US sources would miss entirely.\n *\n * Deliberately NOT included, so the next person does not re-litigate it:\n * rock climbing, gymnastics and aerial/pole are real facility categories but\n * not gyms in the sense this product manages; women-only and kids' fitness\n * describe an AUDIENCE, not an activity; 24-hour and big-box describe hours\n * and size, both of which are already their own fields.\n *\n * Values are stable slugs — the LABELS are the frontend's business, so\n * rewording a chip never rewrites stored data.\n *\n * ── The field is called `types`; the screen says \"What this center offers\" ──\n * That divergence is deliberate, not an oversight. \"Type\" reads as one thing a\n * place IS, and a real branch runs several — a weights floor, a yoga room and\n * Zumba on weekends — so a singular-sounding label pushed owners to pick one\n * and misdescribe themselves. (The picker was once a single-select with a\n * \"Mixed\" option that saved as plain `gym`, which is how a CrossFit-and-boxing\n * branch came back as neither.) The label was changed to match what is stored;\n * the identifiers were not, because the values are a wire contract and\n * renaming them would rewrite data to fix a word.\n */\nexport const CENTER_TYPES = [\n // General\n 'gym',\n // Strength\n 'strength',\n 'crossfit',\n 'functional',\n 'bodybuilding',\n 'powerlifting',\n 'calisthenics',\n // Cardio & group training\n 'cardio',\n 'hiit',\n 'spinning',\n 'bootcamp',\n 'aerobics',\n 'sports-conditioning',\n // Mind & body\n 'yoga',\n 'pilates',\n 'meditation',\n // Dance\n 'zumba',\n 'dance',\n // Combat\n 'boxing',\n 'kickboxing',\n 'mma',\n 'martial-arts',\n // Facilities\n 'swimming',\n 'physiotherapy',\n] as const;\nexport const centerTypeSchema = z.enum(CENTER_TYPES);\nexport type CenterType = z.infer<typeof centerTypeSchema>;\n\n// ─── Status ──────────────────────────────────────────────────────────────────\n\nexport const centerStatusSchema = z.enum(['active', 'inactive']);\nexport type CenterStatus = z.infer<typeof centerStatusSchema>;\n\n// The public gym code (`IRN-1-7K2M`) is defined in `codes.ts` alongside the\n// member ids built on the same stem, so the four identifier shapes this product\n// issues can be read — and kept from colliding — in one place. Re-exported here\n// because a centre's code is a fact about a centre.\nexport { type CenterCode, centerCodeSchema } from './codes.js';\n\n// ─── Create / update bodies ──────────────────────────────────────────────────\n\n/**\n * ⚠️ NO `.default()` IN THIS BASE — the update schema is derived from it.\n *\n * `.partial()` makes a key optional; it does NOT switch off that key's\n * `.default()`. A base carrying a default therefore parses a partial update\n * body into an object containing that key, and the update services `$set` the\n * parsed body straight through — so editing one field silently erases another\n * the caller never mentioned. Defaults live on the CREATE export below.\n * Pinned by `update-body-clobber.test.ts`.\n */\nconst centerBodyBaseSchema = z.object({\n name: z.string().trim().min(1, 'Center name is required').max(160),\n description: optionalTrimmedString(2000),\n types: z.array(centerTypeSchema).min(1, 'Pick at least one'),\n address: addressSchema,\n /** Usable floor area in square feet. */\n floorAreaSqft: z.number().int().min(0).max(1_000_000).optional(),\n /**\n * How many people the branch holds at once.\n *\n * The denominator behind \"how full are we\" — the centre detail screen shows\n * it beside the active-member count, and crowd-load has no meaning without\n * it. Optional: a gym that has never measured it should not be blocked from\n * saving the rest of its details.\n */\n capacity: z.number().int().min(1).max(100_000).optional(),\n primaryPhone: phoneSchema,\n secondaryPhone: optionalPhoneSchema,\n email: optionalEmailSchema,\n website: optionalTrimmedString(200),\n hours: weeklyHoursSchema,\n /** Uploaded gallery image URLs. */\n photos: z.array(z.string().trim().max(2000)).max(20),\n /**\n * A pasted Google Maps link. The server reads the coordinates out of it; the\n * client never sends them.\n *\n * This is the pin attendance is measured against, and it is NOT the same as\n * `address.coords`: that is an area centroid resolved from master data, which\n * can be a whole district's midpoint and is useless for proximity.\n */\n ...mapLinkInputSchema.shape,\n /**\n * How close a member must be for a check-in to count, metres. Optional — the\n * server applies its own default when a gym has not chosen one.\n */\n checkInRadiusM: z.number().int().min(50).max(5000).optional(),\n});\n\nexport const createCenterBodySchema = centerBodyBaseSchema.extend({\n photos: z.array(z.string().trim().max(2000)).max(20).default([]),\n});\nexport type CreateCenterBody = z.infer<typeof createCenterBodySchema>;\n\nexport const updateCenterBodySchema = centerBodyBaseSchema.partial();\nexport type UpdateCenterBody = z.infer<typeof updateCenterBodySchema>;\n\n// ─── Entity (API response) ───────────────────────────────────────────────────\n\nexport const centerSchema = z.object({\n id: objectIdSchema,\n organisationId: objectIdSchema,\n /** Public join code, `IRN-1-7K2M`. */\n code: centerCodeSchema,\n /**\n * Which centre this is within its organisation — 1 for the first branch the\n * owner opened, 2 for the next.\n *\n * Stored rather than counted on read, because it is baked into this centre's\n * code AND into every member id the centre issues. Deleting branch 2 must not\n * renumber branch 3 into codes that are already printed.\n */\n sequence: z.number().int().positive(),\n name: z.string(),\n description: z.string().nullable(),\n types: z.array(centerTypeSchema),\n address: addressSchema,\n floorAreaSqft: z.number().int().nullable(),\n capacity: z.number().int().nullable(),\n primaryPhone: z.string(),\n secondaryPhone: z.string().nullable(),\n email: z.string().nullable(),\n website: z.string().nullable(),\n hours: weeklyHoursSchema,\n photos: z.array(z.string()),\n status: centerStatusSchema,\n /** Denormalised active-member count for roster headers. */\n memberCount: z.number().int().nonnegative(),\n // The resolved pin. Absent until an owner pastes a Maps link, which is why\n // attendance is unenforceable rather than failing for a gym without one.\n ...placePinSchema.shape,\n checkInRadiusM: z.number().int().nullable().optional(),\n /**\n * The centre's own timezone, as an IANA identifier (\"Asia/Kolkata\").\n *\n * WHY THE CENTRE'S AND NOT THE VIEWER'S. Attendance is filed under a calendar\n * DAY, and a day is not timezone-free — whichever zone derives it defines when\n * it starts. Deriving it from whoever is looking would put one visit on\n * different days for the member and the owner, and would make an owner's daily\n * roll-up change depending on where they opened the app. A visit belongs to the\n * gym's operating day; only the display of times follows the reader.\n *\n * An IANA NAME, never a UTC offset. Offsets are a property of an instant, not\n * of a place: they move with DST, so a stored \"+05:30\" is already wrong for\n * half the year anywhere that observes it.\n *\n * Server-derived from the centre's pin, never accepted from a client — the\n * same rule the coordinates follow, and for the same reason.\n */\n timezone: timezoneSchema,\n createdAt: isoDateTimeSchema,\n updatedAt: isoDateTimeSchema,\n});\nexport type Center = z.infer<typeof centerSchema>;\n\n// ─── Center stats (owner detail header) ──────────────────────────────────────\n\n/**\n * The three-up header on a centre's detail screen.\n *\n * Computed, never stored — `activeMembers` counts live memberships and\n * `monthlyRevenueInr` sums this month's successful payments, so the card can\n * never drift from the ledger the way a denormalised total would. `capacity`\n * is the one stored value, and is null until the owner fills it in.\n */\nexport const centerStatsSchema = z.object({\n capacity: z.number().int().nullable(),\n activeMembers: z.number().int().nonnegative(),\n monthlyRevenueInr: z.number().nonnegative(),\n});\nexport type CenterStats = z.infer<typeof centerStatsSchema>;\n\n// ─── Gym directory (member \"find a gym\") ─────────────────────────────────────\n\n/** A lightweight public directory result for the \"find a gym\" screen. */\nexport const nearbyGymSchema = z.object({\n id: objectIdSchema,\n code: centerCodeSchema,\n name: z.string(),\n /** The narrowest place name we hold (locality → district → state). */\n area: z.string(),\n district: z.string().nullable(),\n state: z.string().nullable(),\n /**\n * Kilometres from the coordinates the caller supplied.\n *\n * NULL, not 0, when it cannot be computed — the caller sent no position, or\n * the gym's address carries no coordinates. Zero is a real distance (\"you are\n * standing in it\") and using it as the unknown value put every un-geocoded\n * gym at the top of a nearest-first list.\n */\n distanceKm: z.number().nonnegative().nullable(),\n memberCount: z.number().int().nonnegative().optional(),\n});\nexport type NearbyGym = z.infer<typeof nearbyGymSchema>;\n\n/**\n * The gym directory has THREE ways in, and they differ in how far they reach.\n *\n * `code` exact public gym id — matches anywhere in the country, because\n * a member holding the id already knows which gym they mean and\n * may well be joining one in another state.\n * `q` name search — deliberately narrowed to ONE STATE. Gym names\n * repeat heavily (\"Fitness First\"), so a nationwide name search\n * returns a list nobody can pick from.\n * `districtId` browse — the gyms around them, nearest first when they share\n * their position.\n *\n * `stateId` / `districtId` are master-area ids (`area.ts`), matched against the\n * address `ancestry` array every center stores.\n */\nexport const GYM_SEARCH_MAX_RESULTS = 20;\n\n/**\n * How far \"near me\" reaches, in kilometres.\n *\n * A bound rather than \"the nearest 20 wherever they are\". Without one, a member\n * in a town with no gyms is shown the nearest one 400km away as though it were\n * a suggestion — a list that is technically sorted and practically useless. The\n * empty state points them at a different location or a gym id instead, which\n * are the two things that can actually help.\n */\nexport const GYM_SEARCH_NEARBY_RADIUS_KM = 50;\n\nexport const gymSearchQuerySchema = z\n .object({\n /** Exact public gym id, e.g. `GYM-2025-001`. Unscoped by geography. */\n code: optionalTrimmedString(40),\n /** Name fragment. Requires `stateId`. */\n q: optionalTrimmedString(120),\n stateId: optionalTrimmedString(40),\n districtId: optionalTrimmedString(40),\n /**\n * Any master-area id — state, district, or one of the eight sub-district\n * types the location drawer can reach.\n *\n * Matched against the `ancestry` every centre stores, exactly as\n * `stateId` / `districtId` are, so one parameter covers every depth the\n * picker offers. Those two stay because callers already send them and they\n * read more clearly at the two levels that have their own control.\n */\n areaId: optionalTrimmedString(40),\n lat: z.coerce.number().min(-90).max(90).optional(),\n lng: z.coerce.number().min(-180).max(180).optional(),\n limit: z.coerce\n .number()\n .int()\n .min(1)\n .max(GYM_SEARCH_MAX_RESULTS)\n .default(GYM_SEARCH_MAX_RESULTS),\n })\n .refine((v) => !v.q || Boolean(v.stateId), {\n message: 'Pick a state to search gyms by name',\n path: ['stateId'],\n })\n .refine(\n (v) =>\n Boolean(\n v.code || v.q || v.stateId || v.districtId || v.areaId || (v.lat != null && v.lng != null),\n ),\n {\n message: 'Enter a gym id, or pick where to look',\n path: ['stateId'],\n },\n );\nexport type GymSearchQuery = z.infer<typeof gymSearchQuerySchema>;\n\n/**\n * The directory response.\n *\n * `truncated` says the district holds more gyms than were returned, which is\n * what the screen turns into \"showing the 20 nearest — enter a gym id if yours\n * is not here\" rather than letting the member assume their gym is absent.\n */\nexport const gymSearchResultSchema = z.object({\n items: z.array(nearbyGymSchema),\n total: z.number().int().nonnegative(),\n truncated: z.boolean(),\n});\nexport type GymSearchResult = z.infer<typeof gymSearchResultSchema>;\n\n// ─── Retained: nearby-gyms query ─────────────────────────────────────────────\n// Kept for the gym-search route the owner console's repo still serves. See the\n// note in `onboarding.ts`.\nexport const nearbyGymsQuerySchema = z.object({\n lat: z.coerce.number().min(-90).max(90).optional(),\n lng: z.coerce.number().min(-180).max(180).optional(),\n q: optionalTrimmedString(120),\n});\nexport type NearbyGymsQuery = z.infer<typeof nearbyGymsQuerySchema>;\n"]}
package/dist/common.d.ts CHANGED
@@ -15,6 +15,20 @@ import { z } from 'zod';
15
15
  export declare const emptyStringToUndefined: (value: unknown) => unknown;
16
16
  /** Optional trimmed string. Empty strings are coerced to `undefined`. */
17
17
  export declare const optionalTrimmedString: (maxLength?: number) => z.ZodPreprocess<z.ZodOptional<z.ZodString>>;
18
+ /**
19
+ * A multi-select filter carried in a query string.
20
+ *
21
+ * A URL has no array type, so a multi-select arrives either as
22
+ * `?muscle=chest,back` or, if the client used `URLSearchParams.append`, as
23
+ * repeated keys that Express hands over as a real array. Both are accepted and
24
+ * normalised to one array, because which one a caller produces is an accident
25
+ * of how it built the URL, not a different intent.
26
+ *
27
+ * Unknown values are DROPPED rather than rejected: a filter chip that is
28
+ * renamed or removed should stop matching, not 422 the whole screen for
29
+ * somebody who still has the old URL open.
30
+ */
31
+ export declare const csvOf: <T extends z.ZodTypeAny>(member: T) => z.ZodPreprocess<z.ZodOptional<z.ZodArray<T>>>;
18
32
  /** A 24-character hex MongoDB ObjectId string. */
19
33
  export declare const objectIdSchema: z.ZodString;
20
34
  export type ObjectIdString = z.infer<typeof objectIdSchema>;
@@ -1 +1 @@
1
- {"version":3,"file":"common.d.ts","sourceRoot":"","sources":["../src/common.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"common.d.ts","sourceRoot":"","sources":["../src/common.ts"],"names":[],"mappings":"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"}
package/dist/common.js CHANGED
@@ -20,6 +20,26 @@ export const emptyStringToUndefined = (value) => {
20
20
  };
21
21
  /** Optional trimmed string. Empty strings are coerced to `undefined`. */
22
22
  export const optionalTrimmedString = (maxLength = 100) => z.preprocess(emptyStringToUndefined, z.string().trim().max(maxLength).optional());
23
+ /**
24
+ * A multi-select filter carried in a query string.
25
+ *
26
+ * A URL has no array type, so a multi-select arrives either as
27
+ * `?muscle=chest,back` or, if the client used `URLSearchParams.append`, as
28
+ * repeated keys that Express hands over as a real array. Both are accepted and
29
+ * normalised to one array, because which one a caller produces is an accident
30
+ * of how it built the URL, not a different intent.
31
+ *
32
+ * Unknown values are DROPPED rather than rejected: a filter chip that is
33
+ * renamed or removed should stop matching, not 422 the whole screen for
34
+ * somebody who still has the old URL open.
35
+ */
36
+ export const csvOf = (member) => z.preprocess((value) => {
37
+ if (value === undefined || value === null || value === '')
38
+ return undefined;
39
+ const raw = Array.isArray(value) ? value : String(value).split(',');
40
+ const cleaned = raw.map((v) => String(v).trim()).filter(Boolean);
41
+ return cleaned.length > 0 ? cleaned.filter((v) => member.safeParse(v).success) : undefined;
42
+ }, z.array(member).optional());
23
43
  // ─── Identifiers ─────────────────────────────────────────────────────────────
24
44
  /** A 24-character hex MongoDB ObjectId string. */
25
45
  export const objectIdSchema = z
@@ -1 +1 @@
1
- {"version":3,"file":"common.js","sourceRoot":"","sources":["../src/common.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * gymmonk-schema — Shared helpers and primitives\n * ==============================================\n * The stable building blocks reused across every GymMonk domain schema.\n * Mirrors the `dating-schema` / `near-me-schema` idiom: pure Zod, const-object\n * enums fed to `z.enum(...)`, NodeNext `.js` import specifiers.\n *\n * @module gymmonk-schema/common\n */\n\nimport { z } from 'zod';\n\n// ─── String helpers ──────────────────────────────────────────────────────────\n\n/**\n * Converts empty / whitespace-only strings to `undefined`\n * so Zod `.optional()` treats them as \"not provided\".\n */\nexport const emptyStringToUndefined = (value: unknown): unknown => {\n if (typeof value === 'string' && value.trim() === '') return undefined;\n return value;\n};\n\n/** Optional trimmed string. Empty strings are coerced to `undefined`. */\nexport const optionalTrimmedString = (maxLength = 100) =>\n z.preprocess(emptyStringToUndefined, z.string().trim().max(maxLength).optional());\n\n// ─── Identifiers ─────────────────────────────────────────────────────────────\n\n/** A 24-character hex MongoDB ObjectId string. */\nexport const objectIdSchema = z\n .string()\n .trim()\n .regex(/^[0-9a-fA-F]{24}$/, 'Invalid id');\nexport type ObjectIdString = z.infer<typeof objectIdSchema>;\n\n// ─── Pagination ──────────────────────────────────────────────────────────────\n\n/** Pagination query params shared across list endpoints. */\nexport const paginationSchema = z.object({\n page: z.coerce.number().int().min(1).default(1),\n limit: z.coerce.number().int().min(1).max(100).default(20),\n});\nexport type Pagination = z.infer<typeof paginationSchema>;\n\n// ─── Soft-delete visibility ──────────────────────────────────────────────────\n\n/**\n * How a list query treats soft-deleted rows.\n *\n * THREE states, not a boolean. A boolean can only say \"live\" or \"live +\n * deleted\", which means a console offering a \"Deleted\" filter has to fetch both\n * and drop the live ones client-side — and that breaks the moment the list is\n * paginated: page 1 of \"everything\" may contain no deleted rows at all, so the\n * screen shows an empty list under a filter that matches dozens of records\n * further down. Only the server can answer \"just the deleted ones\".\n *\n * `exclude` the default, and what every ordinary caller wants\n * `include` live and deleted together, for a search that should find both\n * `only` the restore queue\n *\n * Lives here rather than beside one directory because both the people and the\n * gym listings take it, and they must behave identically.\n */\nexport const deletedFilterSchema = z.enum(['exclude', 'include', 'only']);\nexport type DeletedFilter = z.infer<typeof deletedFilterSchema>;\n\n// ─── Contact primitives (India) ──────────────────────────────────────────────\n\n/**\n * The raw patterns, exported so the onboarding FORM schemas validate a typed\n * value exactly the way the API schemas below do — one rule, both sides.\n */\nexport const PHONE_REGEX = /^[6-9]\\d{9}$/;\nexport const PINCODE_REGEX = /^\\d{6}$/;\n\n/**\n * A bare 10-digit Indian mobile number. Stored RAW (no `+91`) app-wide — the\n * country code is display-only. Mirrors the jansathi phone-number standard.\n */\nexport const phoneSchema = z\n .string()\n .trim()\n .regex(PHONE_REGEX, 'Enter a valid 10-digit mobile number');\nexport type Phone = z.infer<typeof phoneSchema>;\n\n/** Optional phone — empty/whitespace becomes `undefined`. */\nexport const optionalPhoneSchema = z.preprocess(emptyStringToUndefined, phoneSchema.optional());\n\n/** A 6-digit Indian postal PIN code. */\nexport const pincodeSchema = z\n .string()\n .trim()\n .regex(PINCODE_REGEX, 'Enter a valid 6-digit pincode');\nexport type Pincode = z.infer<typeof pincodeSchema>;\n\n/** Optional email — empty/whitespace becomes `undefined`, else validated. */\nexport const optionalEmailSchema = z.preprocess(\n emptyStringToUndefined,\n z.email('Enter a valid email').optional(),\n);\n\n// ─── Date & time primitives ──────────────────────────────────────────────────\n\n/** A calendar date with no time component, `YYYY-MM-DD`. */\nexport const isoDateSchema = z\n .string()\n .trim()\n .regex(/^\\d{4}-\\d{2}-\\d{2}$/, 'Expected an YYYY-MM-DD date');\nexport type IsoDate = z.infer<typeof isoDateSchema>;\n\n/** A full ISO-8601 timestamp, e.g. `2026-08-06T09:30:00.000Z`. */\nexport const isoDateTimeSchema = z.iso.datetime({ offset: true });\nexport type IsoDateTime = z.infer<typeof isoDateTimeSchema>;\n\n/** Time of day in 24-hour `HH:MM`. */\nexport const timeOfDaySchema = z\n .string()\n .trim()\n .regex(/^([01]\\d|2[0-3]):[0-5]\\d$/, 'Expected an HH:MM time');\nexport type TimeOfDay = z.infer<typeof timeOfDaySchema>;\n\n/**\n * `Date` rolls overflow forward (Feb 31 → Mar 3), so a real date is one whose\n * parts survive the round-trip.\n *\n * Lives here rather than beside one form's rules because every date-of-birth\n * field in the product needs it — the onboarding wizards and the owner's desk\n * entry forms all check the same thing.\n */\nexport function isRealDate(year: number, month: number, day: number): boolean {\n const date = new Date(Date.UTC(year, month - 1, day));\n return (\n date.getUTCFullYear() === year && date.getUTCMonth() === month - 1 && date.getUTCDate() === day\n );\n}\n\n/** Whole years elapsed since the given Y/M/D, in UTC. Negative when future. */\nexport function ageInYears(year: number, month: number, day: number): number {\n const date = Date.UTC(year, month - 1, day);\n return (Date.now() - date) / (365.25 * 24 * 60 * 60 * 1000);\n}\n\n// ─── Money ───────────────────────────────────────────────────────────────────\n\n/**\n * A whole-rupee (INR) amount. GymMonk prices are whole rupees; format with a\n * `₹` and grouping at the edge, never store the formatted string.\n */\nexport const priceInrSchema = z.number().int().min(0);\nexport type PriceInr = z.infer<typeof priceInrSchema>;\n\n// ─── Names ───────────────────────────────────────────────────────────────────\n\n/**\n * Split one typed name into the two fields a `User` actually stores.\n *\n * Every screen that asks for a name asks for it in ONE box — onboarding step 1\n * and the profile's Personal Details editor — because \"first name / last name\"\n * is a storage detail, not a question a person can answer about their own name.\n * The split therefore has to happen somewhere, and it has to happen the SAME\n * way in both places: if onboarding split on the last space and the editor on\n * the first, a member who opened the editor and pressed Save without touching\n * anything would have their name silently re-cut.\n *\n * Splits on the FIRST space, so \"Ram Kumar Singh\" is `Ram` + `Kumar Singh`.\n * Collapses runs of whitespace, so a double space or a trailing one cannot\n * produce an empty surname or a leading blank.\n *\n * The inverse is the backend's `fullName(first, last)` and the web client's\n * `fullName(user)`, which join the halves back with a single space — so a name\n * that goes through this and back out again is unchanged.\n */\nexport function splitFullName(value: string): { firstName: string; lastName: string } {\n const parts = value.trim().split(/\\s+/).filter(Boolean);\n if (parts.length === 0) return { firstName: '', lastName: '' };\n const [firstName, ...rest] = parts;\n return { firstName: firstName as string, lastName: rest.join(' ') };\n}\n\n/**\n * The two stored name halves joined back into one string.\n *\n * The exact inverse of `splitFullName`, and deliberately sitting beside it:\n * a name that goes through one and back through the other must come out\n * unchanged, which is only guaranteed while both rules live in one file.\n *\n * Trims each half and drops an empty one, so a member with no surname reads as\n * `\"Rahul\"` rather than `\"Rahul \"`. Returns `''` when there is nothing to\n * join; callers pick their own fallback (the email, \"Member\", a dash), because\n * the right one differs per screen.\n *\n * Both products call it through a thin local module — gymmonk-backend\n * `shared/utils/user-name` and gymmonk-web-client `shared/lib/user-name` — so\n * the many existing call sites keep their convenient signatures while there is\n * only ONE rule about what a full name is.\n */\nexport function joinFullName(firstName?: string | null, lastName?: string | null): string {\n return [firstName, lastName]\n .map((part) => (part ?? '').trim())\n .filter(Boolean)\n .join(' ');\n}\n\n// ─── Billing cycle ───────────────────────────────────────────────────────────\n\n/**\n * The day of the month a member's fee falls due, e.g. `16` for \"the 16th of\n * every month\".\n *\n * A DAY NUMBER, not a date, and the distinction is the whole point. A member\n * joining through the app on 5 June with a billing day of 26 is billed\n * 26 May → 26 June: the day is an anchor for a repeating cycle, so it may name\n * a date earlier than the one they joined on. Storing a date instead would\n * make \"the 26th of every month\" unanswerable the moment the first term ended.\n *\n * 1–31 rather than 1–28. A gym that bills on the 31st is making a perfectly\n * ordinary statement, and the months that have no 31st are handled where the\n * term is dated — by clamping to the last day the month actually has, which is\n * what `addMonths` in the backend already does for every other term. Refusing\n * 29, 30 and 31 here would reject a real answer to avoid arithmetic that is\n * already written.\n */\nexport const billingDayOfMonthSchema = z\n .number()\n .int('Pick a day of the month')\n .min(1, 'Pick a day between 1 and 31')\n .max(31, 'Pick a day between 1 and 31');\nexport type BillingDayOfMonth = z.infer<typeof billingDayOfMonthSchema>;\n\n/**\n * The most recent occurrence of `day` on or before `onOrBefore`, as `YYYY-MM-DD`.\n *\n * This is the rule that decides which term a joining member's first payment\n * covers: never a period starting after they joined, so the month they are\n * actually training in is always the month they are paying for. Joining on\n * 5 June with day 26 gives 26 May; with day 10 it gives 10 May, because 10 June\n * has not happened yet.\n *\n * Lives in the schema package because BOTH sides need it — the app previews\n * \"your plan will run 26 May to 26 June\" as the member picks the day, and the\n * server dates the term for real. Two copies of this arithmetic is how a member\n * is shown one period and billed for another.\n *\n * Clamped to the last day of the target month, so day 31 in a 30-day month\n * resolves to the 30th rather than rolling into the next month.\n */\nexport function billingAnchorOnOrBefore(day: number, onOrBefore: string): string {\n const [y, m, d] = onOrBefore.split('-').map(Number) as [number, number, number];\n const clampTo = (year: number, month: number): number =>\n Math.min(day, new Date(Date.UTC(year, month, 0)).getUTCDate());\n\n // This month's occurrence, if it has already happened.\n const thisMonth = clampTo(y, m);\n if (thisMonth <= d) {\n return `${String(y).padStart(4, '0')}-${String(m).padStart(2, '0')}-${String(thisMonth).padStart(2, '0')}`;\n }\n\n // Otherwise the previous month's.\n const py = m === 1 ? y - 1 : y;\n const pm = m === 1 ? 12 : m - 1;\n const prev = clampTo(py, pm);\n return `${String(py).padStart(4, '0')}-${String(pm).padStart(2, '0')}-${String(prev).padStart(2, '0')}`;\n}\n"]}
1
+ {"version":3,"file":"common.js","sourceRoot":"","sources":["../src/common.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB,gFAAgF;AAEhF;;;GAGG;AACH,MAAM,CAAC,MAAM,sBAAsB,GAAG,CAAC,KAAc,EAAW,EAAE;IAChE,IAAI,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,CAAC,IAAI,EAAE,KAAK,EAAE;QAAE,OAAO,SAAS,CAAC;IACvE,OAAO,KAAK,CAAC;AACf,CAAC,CAAC;AAEF,yEAAyE;AACzE,MAAM,CAAC,MAAM,qBAAqB,GAAG,CAAC,SAAS,GAAG,GAAG,EAAE,EAAE,CACvD,CAAC,CAAC,UAAU,CAAC,sBAAsB,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,IAAI,EAAE,CAAC,GAAG,CAAC,SAAS,CAAC,CAAC,QAAQ,EAAE,CAAC,CAAC;AAEpF;;;;;;;;;;;;GAYG;AACH,MAAM,CAAC,MAAM,KAAK,GAAG,CAAyB,MAAS,EAAE,EAAE,CACzD,CAAC,CAAC,UAAU,CAAC,CAAC,KAAK,EAAE,EAAE;IACrB,IAAI,KAAK,KAAK,SAAS,IAAI,KAAK,KAAK,IAAI,IAAI,KAAK,KAAK,EAAE;QAAE,OAAO,SAAS,CAAC;IAC5E,MAAM,GAAG,GAAG,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;IACpE,MAAM,OAAO,GAAG,GAAG,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC;IACjE,OAAO,OAAO,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,MAAM,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC;AAC7F,CAAC,EAAE,CAAC,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC,QAAQ,EAAE,CAAC,CAAC;AAEjC,gFAAgF;AAEhF,kDAAkD;AAClD,MAAM,CAAC,MAAM,cAAc,GAAG,CAAC;KAC5B,MAAM,EAAE;KACR,IAAI,EAAE;KACN,KAAK,CAAC,mBAAmB,EAAE,YAAY,CAAC,CAAC;AAG5C,gFAAgF;AAEhF,4DAA4D;AAC5D,MAAM,CAAC,MAAM,gBAAgB,GAAG,CAAC,CAAC,MAAM,CAAC;IACvC,IAAI,EAAE,CAAC,CAAC,MAAM,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC;IAC/C,KAAK,EAAE,CAAC,CAAC,MAAM,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,OAAO,CAAC,EAAE,CAAC;CAC3D,CAAC,CAAC;AAGH,gFAAgF;AAEhF;;;;;;;;;;;;;;;;GAgBG;AACH,MAAM,CAAC,MAAM,mBAAmB,GAAG,CAAC,CAAC,IAAI,CAAC,CAAC,SAAS,EAAE,SAAS,EAAE,MAAM,CAAC,CAAC,CAAC;AAG1E,gFAAgF;AAEhF;;;GAGG;AACH,MAAM,CAAC,MAAM,WAAW,GAAG,cAAc,CAAC;AAC1C,MAAM,CAAC,MAAM,aAAa,GAAG,SAAS,CAAC;AAEvC;;;GAGG;AACH,MAAM,CAAC,MAAM,WAAW,GAAG,CAAC;KACzB,MAAM,EAAE;KACR,IAAI,EAAE;KACN,KAAK,CAAC,WAAW,EAAE,sCAAsC,CAAC,CAAC;AAG9D,6DAA6D;AAC7D,MAAM,CAAC,MAAM,mBAAmB,GAAG,CAAC,CAAC,UAAU,CAAC,sBAAsB,EAAE,WAAW,CAAC,QAAQ,EAAE,CAAC,CAAC;AAEhG,wCAAwC;AACxC,MAAM,CAAC,MAAM,aAAa,GAAG,CAAC;KAC3B,MAAM,EAAE;KACR,IAAI,EAAE;KACN,KAAK,CAAC,aAAa,EAAE,+BAA+B,CAAC,CAAC;AAGzD,6EAA6E;AAC7E,MAAM,CAAC,MAAM,mBAAmB,GAAG,CAAC,CAAC,UAAU,CAC7C,sBAAsB,EACtB,CAAC,CAAC,KAAK,CAAC,qBAAqB,CAAC,CAAC,QAAQ,EAAE,CAC1C,CAAC;AAEF,gFAAgF;AAEhF,4DAA4D;AAC5D,MAAM,CAAC,MAAM,aAAa,GAAG,CAAC;KAC3B,MAAM,EAAE;KACR,IAAI,EAAE;KACN,KAAK,CAAC,qBAAqB,EAAE,6BAA6B,CAAC,CAAC;AAG/D,kEAAkE;AAClE,MAAM,CAAC,MAAM,iBAAiB,GAAG,CAAC,CAAC,GAAG,CAAC,QAAQ,CAAC,EAAE,MAAM,EAAE,IAAI,EAAE,CAAC,CAAC;AAGlE,sCAAsC;AACtC,MAAM,CAAC,MAAM,eAAe,GAAG,CAAC;KAC7B,MAAM,EAAE;KACR,IAAI,EAAE;KACN,KAAK,CAAC,2BAA2B,EAAE,wBAAwB,CAAC,CAAC;AAGhE;;;;;;;GAOG;AACH,MAAM,UAAU,UAAU,CAAC,IAAY,EAAE,KAAa,EAAE,GAAW;IACjE,MAAM,IAAI,GAAG,IAAI,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,IAAI,EAAE,KAAK,GAAG,CAAC,EAAE,GAAG,CAAC,CAAC,CAAC;IACtD,OAAO,CACL,IAAI,CAAC,cAAc,EAAE,KAAK,IAAI,IAAI,IAAI,CAAC,WAAW,EAAE,KAAK,KAAK,GAAG,CAAC,IAAI,IAAI,CAAC,UAAU,EAAE,KAAK,GAAG,CAChG,CAAC;AACJ,CAAC;AAED,+EAA+E;AAC/E,MAAM,UAAU,UAAU,CAAC,IAAY,EAAE,KAAa,EAAE,GAAW;IACjE,MAAM,IAAI,GAAG,IAAI,CAAC,GAAG,CAAC,IAAI,EAAE,KAAK,GAAG,CAAC,EAAE,GAAG,CAAC,CAAC;IAC5C,OAAO,CAAC,IAAI,CAAC,GAAG,EAAE,GAAG,IAAI,CAAC,GAAG,CAAC,MAAM,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,IAAI,CAAC,CAAC;AAC9D,CAAC;AAED,gFAAgF;AAEhF;;;GAGG;AACH,MAAM,CAAC,MAAM,cAAc,GAAG,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC;AAGtD,gFAAgF;AAEhF;;;;;;;;;;;;;;;;;;GAkBG;AACH,MAAM,UAAU,aAAa,CAAC,KAAa;IACzC,MAAM,KAAK,GAAG,KAAK,CAAC,IAAI,EAAE,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC;IACxD,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,EAAE,SAAS,EAAE,EAAE,EAAE,QAAQ,EAAE,EAAE,EAAE,CAAC;IAC/D,MAAM,CAAC,SAAS,EAAE,GAAG,IAAI,CAAC,GAAG,KAAK,CAAC;IACnC,OAAO,EAAE,SAAS,EAAE,SAAmB,EAAE,QAAQ,EAAE,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC;AACtE,CAAC;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,MAAM,UAAU,YAAY,CAAC,SAAyB,EAAE,QAAwB;IAC9E,OAAO,CAAC,SAAS,EAAE,QAAQ,CAAC;SACzB,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC,IAAI,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC;SAClC,MAAM,CAAC,OAAO,CAAC;SACf,IAAI,CAAC,GAAG,CAAC,CAAC;AACf,CAAC;AAED,gFAAgF;AAEhF;;;;;;;;;;;;;;;;GAgBG;AACH,MAAM,CAAC,MAAM,uBAAuB,GAAG,CAAC;KACrC,MAAM,EAAE;KACR,GAAG,CAAC,yBAAyB,CAAC;KAC9B,GAAG,CAAC,CAAC,EAAE,6BAA6B,CAAC;KACrC,GAAG,CAAC,EAAE,EAAE,6BAA6B,CAAC,CAAC;AAG1C;;;;;;;;;;;;;;;;GAgBG;AACH,MAAM,UAAU,uBAAuB,CAAC,GAAW,EAAE,UAAkB;IACrE,MAAM,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,GAAG,UAAU,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,GAAG,CAAC,MAAM,CAA6B,CAAC;IAChF,MAAM,OAAO,GAAG,CAAC,IAAY,EAAE,KAAa,EAAU,EAAE,CACtD,IAAI,CAAC,GAAG,CAAC,GAAG,EAAE,IAAI,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,IAAI,EAAE,KAAK,EAAE,CAAC,CAAC,CAAC,CAAC,UAAU,EAAE,CAAC,CAAC;IAEjE,uDAAuD;IACvD,MAAM,SAAS,GAAG,OAAO,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC;IAChC,IAAI,SAAS,IAAI,CAAC,EAAE,CAAC;QACnB,OAAO,GAAG,MAAM,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,EAAE,GAAG,CAAC,IAAI,MAAM,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,EAAE,GAAG,CAAC,IAAI,MAAM,CAAC,SAAS,CAAC,CAAC,QAAQ,CAAC,CAAC,EAAE,GAAG,CAAC,EAAE,CAAC;IAC7G,CAAC;IAED,kCAAkC;IAClC,MAAM,EAAE,GAAG,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;IAC/B,MAAM,EAAE,GAAG,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC;IAChC,MAAM,IAAI,GAAG,OAAO,CAAC,EAAE,EAAE,EAAE,CAAC,CAAC;IAC7B,OAAO,GAAG,MAAM,CAAC,EAAE,CAAC,CAAC,QAAQ,CAAC,CAAC,EAAE,GAAG,CAAC,IAAI,MAAM,CAAC,EAAE,CAAC,CAAC,QAAQ,CAAC,CAAC,EAAE,GAAG,CAAC,IAAI,MAAM,CAAC,IAAI,CAAC,CAAC,QAAQ,CAAC,CAAC,EAAE,GAAG,CAAC,EAAE,CAAC;AAC1G,CAAC","sourcesContent":["/**\n * gymmonk-schema — Shared helpers and primitives\n * ==============================================\n * The stable building blocks reused across every GymMonk domain schema.\n * Mirrors the `dating-schema` / `near-me-schema` idiom: pure Zod, const-object\n * enums fed to `z.enum(...)`, NodeNext `.js` import specifiers.\n *\n * @module gymmonk-schema/common\n */\n\nimport { z } from 'zod';\n\n// ─── String helpers ──────────────────────────────────────────────────────────\n\n/**\n * Converts empty / whitespace-only strings to `undefined`\n * so Zod `.optional()` treats them as \"not provided\".\n */\nexport const emptyStringToUndefined = (value: unknown): unknown => {\n if (typeof value === 'string' && value.trim() === '') return undefined;\n return value;\n};\n\n/** Optional trimmed string. Empty strings are coerced to `undefined`. */\nexport const optionalTrimmedString = (maxLength = 100) =>\n z.preprocess(emptyStringToUndefined, z.string().trim().max(maxLength).optional());\n\n/**\n * A multi-select filter carried in a query string.\n *\n * A URL has no array type, so a multi-select arrives either as\n * `?muscle=chest,back` or, if the client used `URLSearchParams.append`, as\n * repeated keys that Express hands over as a real array. Both are accepted and\n * normalised to one array, because which one a caller produces is an accident\n * of how it built the URL, not a different intent.\n *\n * Unknown values are DROPPED rather than rejected: a filter chip that is\n * renamed or removed should stop matching, not 422 the whole screen for\n * somebody who still has the old URL open.\n */\nexport const csvOf = <T extends z.ZodTypeAny>(member: T) =>\n z.preprocess((value) => {\n if (value === undefined || value === null || value === '') return undefined;\n const raw = Array.isArray(value) ? value : String(value).split(',');\n const cleaned = raw.map((v) => String(v).trim()).filter(Boolean);\n return cleaned.length > 0 ? cleaned.filter((v) => member.safeParse(v).success) : undefined;\n }, z.array(member).optional());\n\n// ─── Identifiers ─────────────────────────────────────────────────────────────\n\n/** A 24-character hex MongoDB ObjectId string. */\nexport const objectIdSchema = z\n .string()\n .trim()\n .regex(/^[0-9a-fA-F]{24}$/, 'Invalid id');\nexport type ObjectIdString = z.infer<typeof objectIdSchema>;\n\n// ─── Pagination ──────────────────────────────────────────────────────────────\n\n/** Pagination query params shared across list endpoints. */\nexport const paginationSchema = z.object({\n page: z.coerce.number().int().min(1).default(1),\n limit: z.coerce.number().int().min(1).max(100).default(20),\n});\nexport type Pagination = z.infer<typeof paginationSchema>;\n\n// ─── Soft-delete visibility ──────────────────────────────────────────────────\n\n/**\n * How a list query treats soft-deleted rows.\n *\n * THREE states, not a boolean. A boolean can only say \"live\" or \"live +\n * deleted\", which means a console offering a \"Deleted\" filter has to fetch both\n * and drop the live ones client-side — and that breaks the moment the list is\n * paginated: page 1 of \"everything\" may contain no deleted rows at all, so the\n * screen shows an empty list under a filter that matches dozens of records\n * further down. Only the server can answer \"just the deleted ones\".\n *\n * `exclude` the default, and what every ordinary caller wants\n * `include` live and deleted together, for a search that should find both\n * `only` the restore queue\n *\n * Lives here rather than beside one directory because both the people and the\n * gym listings take it, and they must behave identically.\n */\nexport const deletedFilterSchema = z.enum(['exclude', 'include', 'only']);\nexport type DeletedFilter = z.infer<typeof deletedFilterSchema>;\n\n// ─── Contact primitives (India) ──────────────────────────────────────────────\n\n/**\n * The raw patterns, exported so the onboarding FORM schemas validate a typed\n * value exactly the way the API schemas below do — one rule, both sides.\n */\nexport const PHONE_REGEX = /^[6-9]\\d{9}$/;\nexport const PINCODE_REGEX = /^\\d{6}$/;\n\n/**\n * A bare 10-digit Indian mobile number. Stored RAW (no `+91`) app-wide — the\n * country code is display-only. Mirrors the jansathi phone-number standard.\n */\nexport const phoneSchema = z\n .string()\n .trim()\n .regex(PHONE_REGEX, 'Enter a valid 10-digit mobile number');\nexport type Phone = z.infer<typeof phoneSchema>;\n\n/** Optional phone — empty/whitespace becomes `undefined`. */\nexport const optionalPhoneSchema = z.preprocess(emptyStringToUndefined, phoneSchema.optional());\n\n/** A 6-digit Indian postal PIN code. */\nexport const pincodeSchema = z\n .string()\n .trim()\n .regex(PINCODE_REGEX, 'Enter a valid 6-digit pincode');\nexport type Pincode = z.infer<typeof pincodeSchema>;\n\n/** Optional email — empty/whitespace becomes `undefined`, else validated. */\nexport const optionalEmailSchema = z.preprocess(\n emptyStringToUndefined,\n z.email('Enter a valid email').optional(),\n);\n\n// ─── Date & time primitives ──────────────────────────────────────────────────\n\n/** A calendar date with no time component, `YYYY-MM-DD`. */\nexport const isoDateSchema = z\n .string()\n .trim()\n .regex(/^\\d{4}-\\d{2}-\\d{2}$/, 'Expected an YYYY-MM-DD date');\nexport type IsoDate = z.infer<typeof isoDateSchema>;\n\n/** A full ISO-8601 timestamp, e.g. `2026-08-06T09:30:00.000Z`. */\nexport const isoDateTimeSchema = z.iso.datetime({ offset: true });\nexport type IsoDateTime = z.infer<typeof isoDateTimeSchema>;\n\n/** Time of day in 24-hour `HH:MM`. */\nexport const timeOfDaySchema = z\n .string()\n .trim()\n .regex(/^([01]\\d|2[0-3]):[0-5]\\d$/, 'Expected an HH:MM time');\nexport type TimeOfDay = z.infer<typeof timeOfDaySchema>;\n\n/**\n * `Date` rolls overflow forward (Feb 31 → Mar 3), so a real date is one whose\n * parts survive the round-trip.\n *\n * Lives here rather than beside one form's rules because every date-of-birth\n * field in the product needs it — the onboarding wizards and the owner's desk\n * entry forms all check the same thing.\n */\nexport function isRealDate(year: number, month: number, day: number): boolean {\n const date = new Date(Date.UTC(year, month - 1, day));\n return (\n date.getUTCFullYear() === year && date.getUTCMonth() === month - 1 && date.getUTCDate() === day\n );\n}\n\n/** Whole years elapsed since the given Y/M/D, in UTC. Negative when future. */\nexport function ageInYears(year: number, month: number, day: number): number {\n const date = Date.UTC(year, month - 1, day);\n return (Date.now() - date) / (365.25 * 24 * 60 * 60 * 1000);\n}\n\n// ─── Money ───────────────────────────────────────────────────────────────────\n\n/**\n * A whole-rupee (INR) amount. GymMonk prices are whole rupees; format with a\n * `₹` and grouping at the edge, never store the formatted string.\n */\nexport const priceInrSchema = z.number().int().min(0);\nexport type PriceInr = z.infer<typeof priceInrSchema>;\n\n// ─── Names ───────────────────────────────────────────────────────────────────\n\n/**\n * Split one typed name into the two fields a `User` actually stores.\n *\n * Every screen that asks for a name asks for it in ONE box — onboarding step 1\n * and the profile's Personal Details editor — because \"first name / last name\"\n * is a storage detail, not a question a person can answer about their own name.\n * The split therefore has to happen somewhere, and it has to happen the SAME\n * way in both places: if onboarding split on the last space and the editor on\n * the first, a member who opened the editor and pressed Save without touching\n * anything would have their name silently re-cut.\n *\n * Splits on the FIRST space, so \"Ram Kumar Singh\" is `Ram` + `Kumar Singh`.\n * Collapses runs of whitespace, so a double space or a trailing one cannot\n * produce an empty surname or a leading blank.\n *\n * The inverse is the backend's `fullName(first, last)` and the web client's\n * `fullName(user)`, which join the halves back with a single space — so a name\n * that goes through this and back out again is unchanged.\n */\nexport function splitFullName(value: string): { firstName: string; lastName: string } {\n const parts = value.trim().split(/\\s+/).filter(Boolean);\n if (parts.length === 0) return { firstName: '', lastName: '' };\n const [firstName, ...rest] = parts;\n return { firstName: firstName as string, lastName: rest.join(' ') };\n}\n\n/**\n * The two stored name halves joined back into one string.\n *\n * The exact inverse of `splitFullName`, and deliberately sitting beside it:\n * a name that goes through one and back through the other must come out\n * unchanged, which is only guaranteed while both rules live in one file.\n *\n * Trims each half and drops an empty one, so a member with no surname reads as\n * `\"Rahul\"` rather than `\"Rahul \"`. Returns `''` when there is nothing to\n * join; callers pick their own fallback (the email, \"Member\", a dash), because\n * the right one differs per screen.\n *\n * Both products call it through a thin local module — gymmonk-backend\n * `shared/utils/user-name` and gymmonk-web-client `shared/lib/user-name` — so\n * the many existing call sites keep their convenient signatures while there is\n * only ONE rule about what a full name is.\n */\nexport function joinFullName(firstName?: string | null, lastName?: string | null): string {\n return [firstName, lastName]\n .map((part) => (part ?? '').trim())\n .filter(Boolean)\n .join(' ');\n}\n\n// ─── Billing cycle ───────────────────────────────────────────────────────────\n\n/**\n * The day of the month a member's fee falls due, e.g. `16` for \"the 16th of\n * every month\".\n *\n * A DAY NUMBER, not a date, and the distinction is the whole point. A member\n * joining through the app on 5 June with a billing day of 26 is billed\n * 26 May → 26 June: the day is an anchor for a repeating cycle, so it may name\n * a date earlier than the one they joined on. Storing a date instead would\n * make \"the 26th of every month\" unanswerable the moment the first term ended.\n *\n * 1–31 rather than 1–28. A gym that bills on the 31st is making a perfectly\n * ordinary statement, and the months that have no 31st are handled where the\n * term is dated — by clamping to the last day the month actually has, which is\n * what `addMonths` in the backend already does for every other term. Refusing\n * 29, 30 and 31 here would reject a real answer to avoid arithmetic that is\n * already written.\n */\nexport const billingDayOfMonthSchema = z\n .number()\n .int('Pick a day of the month')\n .min(1, 'Pick a day between 1 and 31')\n .max(31, 'Pick a day between 1 and 31');\nexport type BillingDayOfMonth = z.infer<typeof billingDayOfMonthSchema>;\n\n/**\n * The most recent occurrence of `day` on or before `onOrBefore`, as `YYYY-MM-DD`.\n *\n * This is the rule that decides which term a joining member's first payment\n * covers: never a period starting after they joined, so the month they are\n * actually training in is always the month they are paying for. Joining on\n * 5 June with day 26 gives 26 May; with day 10 it gives 10 May, because 10 June\n * has not happened yet.\n *\n * Lives in the schema package because BOTH sides need it — the app previews\n * \"your plan will run 26 May to 26 June\" as the member picks the day, and the\n * server dates the term for real. Two copies of this arithmetic is how a member\n * is shown one period and billed for another.\n *\n * Clamped to the last day of the target month, so day 31 in a 30-day month\n * resolves to the 30th rather than rolling into the next month.\n */\nexport function billingAnchorOnOrBefore(day: number, onOrBefore: string): string {\n const [y, m, d] = onOrBefore.split('-').map(Number) as [number, number, number];\n const clampTo = (year: number, month: number): number =>\n Math.min(day, new Date(Date.UTC(year, month, 0)).getUTCDate());\n\n // This month's occurrence, if it has already happened.\n const thisMonth = clampTo(y, m);\n if (thisMonth <= d) {\n return `${String(y).padStart(4, '0')}-${String(m).padStart(2, '0')}-${String(thisMonth).padStart(2, '0')}`;\n }\n\n // Otherwise the previous month's.\n const py = m === 1 ? y - 1 : y;\n const pm = m === 1 ? 12 : m - 1;\n const prev = clampTo(py, pm);\n return `${String(py).padStart(4, '0')}-${String(pm).padStart(2, '0')}-${String(prev).padStart(2, '0')}`;\n}\n"]}
@@ -133,10 +133,21 @@ export declare const dietItemSchema: z.ZodObject<{
133
133
  thumbnailUrl: z.ZodNullable<z.ZodString>;
134
134
  centerId: z.ZodNullable<z.ZodString>;
135
135
  calories: z.ZodNumber;
136
+ protein: z.ZodNumber;
137
+ carbs: z.ZodNumber;
138
+ fat: z.ZodNumber;
139
+ fibre: z.ZodNumber;
140
+ dietType: z.ZodEnum<{
141
+ veg: "veg";
142
+ egg: "egg";
143
+ "non-veg": "non-veg";
144
+ }>;
145
+ isVegan: z.ZodBoolean;
136
146
  serving: z.ZodNullable<z.ZodObject<{
137
147
  label: z.ZodString;
138
148
  grams: z.ZodNumber;
139
149
  }, z.core.$strip>>;
150
+ isRecipe: z.ZodBoolean;
140
151
  }, z.core.$strip>;
141
152
  quantityGrams: z.ZodNumber;
142
153
  note: z.ZodNullable<z.ZodString>;
@@ -197,10 +208,21 @@ export declare const dietMealSchema: z.ZodObject<{
197
208
  thumbnailUrl: z.ZodNullable<z.ZodString>;
198
209
  centerId: z.ZodNullable<z.ZodString>;
199
210
  calories: z.ZodNumber;
211
+ protein: z.ZodNumber;
212
+ carbs: z.ZodNumber;
213
+ fat: z.ZodNumber;
214
+ fibre: z.ZodNumber;
215
+ dietType: z.ZodEnum<{
216
+ veg: "veg";
217
+ egg: "egg";
218
+ "non-veg": "non-veg";
219
+ }>;
220
+ isVegan: z.ZodBoolean;
200
221
  serving: z.ZodNullable<z.ZodObject<{
201
222
  label: z.ZodString;
202
223
  grams: z.ZodNumber;
203
224
  }, z.core.$strip>>;
225
+ isRecipe: z.ZodBoolean;
204
226
  }, z.core.$strip>;
205
227
  quantityGrams: z.ZodNumber;
206
228
  note: z.ZodNullable<z.ZodString>;
@@ -289,10 +311,21 @@ export declare const dietChartDaySchema: z.ZodObject<{
289
311
  thumbnailUrl: z.ZodNullable<z.ZodString>;
290
312
  centerId: z.ZodNullable<z.ZodString>;
291
313
  calories: z.ZodNumber;
314
+ protein: z.ZodNumber;
315
+ carbs: z.ZodNumber;
316
+ fat: z.ZodNumber;
317
+ fibre: z.ZodNumber;
318
+ dietType: z.ZodEnum<{
319
+ veg: "veg";
320
+ egg: "egg";
321
+ "non-veg": "non-veg";
322
+ }>;
323
+ isVegan: z.ZodBoolean;
292
324
  serving: z.ZodNullable<z.ZodObject<{
293
325
  label: z.ZodString;
294
326
  grams: z.ZodNumber;
295
327
  }, z.core.$strip>>;
328
+ isRecipe: z.ZodBoolean;
296
329
  }, z.core.$strip>;
297
330
  quantityGrams: z.ZodNumber;
298
331
  note: z.ZodNullable<z.ZodString>;
@@ -421,10 +454,21 @@ export declare const dietChartSchema: z.ZodObject<{
421
454
  thumbnailUrl: z.ZodNullable<z.ZodString>;
422
455
  centerId: z.ZodNullable<z.ZodString>;
423
456
  calories: z.ZodNumber;
457
+ protein: z.ZodNumber;
458
+ carbs: z.ZodNumber;
459
+ fat: z.ZodNumber;
460
+ fibre: z.ZodNumber;
461
+ dietType: z.ZodEnum<{
462
+ veg: "veg";
463
+ egg: "egg";
464
+ "non-veg": "non-veg";
465
+ }>;
466
+ isVegan: z.ZodBoolean;
424
467
  serving: z.ZodNullable<z.ZodObject<{
425
468
  label: z.ZodString;
426
469
  grams: z.ZodNumber;
427
470
  }, z.core.$strip>>;
471
+ isRecipe: z.ZodBoolean;
428
472
  }, z.core.$strip>;
429
473
  quantityGrams: z.ZodNumber;
430
474
  note: z.ZodNullable<z.ZodString>;
@@ -530,9 +574,9 @@ export declare const updateDietChartBodySchema: z.ZodObject<{
530
574
  name: z.ZodOptional<z.ZodString>;
531
575
  description: z.ZodOptional<z.ZodPreprocess<z.ZodOptional<z.ZodString>>>;
532
576
  goals: z.ZodOptional<z.ZodPreprocess<z.ZodOptional<z.ZodString>>>;
533
- tags: z.ZodOptional<z.ZodDefault<z.ZodArray<z.ZodString>>>;
534
- targetCalories: z.ZodOptional<z.ZodDefault<z.ZodNullable<z.ZodNumber>>>;
535
- days: z.ZodOptional<z.ZodDefault<z.ZodArray<z.ZodObject<{
577
+ tags: z.ZodOptional<z.ZodArray<z.ZodString>>;
578
+ targetCalories: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
579
+ days: z.ZodOptional<z.ZodArray<z.ZodObject<{
536
580
  weekday: z.ZodEnum<{
537
581
  mon: "mon";
538
582
  tue: "tue";
@@ -560,7 +604,7 @@ export declare const updateDietChartBodySchema: z.ZodObject<{
560
604
  note: z.ZodPreprocess<z.ZodOptional<z.ZodString>>;
561
605
  }, z.core.$strip>>>;
562
606
  }, z.core.$strip>>>;
563
- }, z.core.$strip>>>>;
607
+ }, z.core.$strip>>>;
564
608
  }, z.core.$strip>;
565
609
  export type UpdateDietChartBody = z.infer<typeof updateDietChartBodySchema>;
566
610
  export declare const dietChartQuerySchema: z.ZodObject<{
@@ -642,10 +686,21 @@ export declare const myDietChartSchema: z.ZodObject<{
642
686
  thumbnailUrl: z.ZodNullable<z.ZodString>;
643
687
  centerId: z.ZodNullable<z.ZodString>;
644
688
  calories: z.ZodNumber;
689
+ protein: z.ZodNumber;
690
+ carbs: z.ZodNumber;
691
+ fat: z.ZodNumber;
692
+ fibre: z.ZodNumber;
693
+ dietType: z.ZodEnum<{
694
+ veg: "veg";
695
+ egg: "egg";
696
+ "non-veg": "non-veg";
697
+ }>;
698
+ isVegan: z.ZodBoolean;
645
699
  serving: z.ZodNullable<z.ZodObject<{
646
700
  label: z.ZodString;
647
701
  grams: z.ZodNumber;
648
702
  }, z.core.$strip>>;
703
+ isRecipe: z.ZodBoolean;
649
704
  }, z.core.$strip>;
650
705
  quantityGrams: z.ZodNumber;
651
706
  note: z.ZodNullable<z.ZodString>;
@@ -1 +1 @@
1
- {"version":3,"file":"diet-chart.d.ts","sourceRoot":"","sources":["../src/diet-chart.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"diet-chart.d.ts","sourceRoot":"","sources":["../src/diet-chart.ts"],"names":[],"mappings":"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"}
@@ -155,19 +155,39 @@ export const dietChartSchema = dietChartSummarySchema.extend({
155
155
  days: z.array(dietChartDaySchema),
156
156
  });
157
157
  // ─── Create / update body ────────────────────────────────────────────────────
158
- export const createDietChartBodySchema = z.object({
158
+ /**
159
+ * ⚠️ NO `.default()` IN THIS BASE. It carried three, and one of them lost data.
160
+ *
161
+ * `updateDietChartBodySchema` is this object `.omit().partial()`-ed, and in Zod
162
+ * a `.default()` STILL FIRES under `.partial()` when the key is absent. So
163
+ * `PUT /diet-charts/:id` with `{ name: 'Renamed' }` parsed to
164
+ * `{ name, tags: [], targetCalories: null, days: [] }`, and `updateDietChart`
165
+ * does `$set: body`.
166
+ *
167
+ * **Renaming a chart erased every meal in it**, and every member assigned to it
168
+ * silently got an empty diet. Nothing in the request said so and nothing in the
169
+ * response looked wrong.
170
+ *
171
+ * The defaults live on the CREATE export below, which is the only place a
172
+ * missing key genuinely means "empty". Pinned by a test that parses `{}` and
173
+ * asserts nothing comes back.
174
+ */
175
+ const dietChartBodyBaseSchema = z.object({
159
176
  /** Nullable, never optional — see `createFoodBodySchema.centerId`. */
160
177
  centerId: objectIdSchema.nullable(),
161
178
  name: z.string().trim().min(1, 'Chart name is required').max(120),
162
179
  description: optionalTrimmedString(2000),
163
180
  goals: optionalTrimmedString(400),
181
+ tags: z.array(z.string().trim().max(40)).max(20),
182
+ targetCalories: z.number().min(0).max(20000).nullable(),
183
+ days: z.array(dietChartDayInputSchema).max(7),
184
+ });
185
+ export const createDietChartBodySchema = dietChartBodyBaseSchema.extend({
164
186
  tags: z.array(z.string().trim().max(40)).max(20).default([]),
165
187
  targetCalories: z.number().min(0).max(20000).nullable().default(null),
166
188
  days: z.array(dietChartDayInputSchema).max(7).default([]),
167
189
  });
168
- export const updateDietChartBodySchema = createDietChartBodySchema
169
- .omit({ centerId: true })
170
- .partial();
190
+ export const updateDietChartBodySchema = dietChartBodyBaseSchema.omit({ centerId: true }).partial();
171
191
  // ─── Library query ───────────────────────────────────────────────────────────
172
192
  export const dietChartQuerySchema = z.object({
173
193
  centerId: objectIdSchema.optional(),
@@ -1 +1 @@
1
- {"version":3,"file":"diet-chart.js","sourceRoot":"","sources":["../src/diet-chart.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * gymmonk-schema — Diet charts\n * =============================\n * A weekly eating plan a gym builds from library foods and assigns to a member.\n * The counterpart to `workout-plan`, and deliberately the same shape: a chart\n * scoped by `centerId`, days keyed by weekday, and a single assignment ref on\n * the member. Whoever has understood one understands the other.\n *\n * chart → days (weekday) → meals (type + time) → items (food + grams)\n *\n * That is the structure PT software converges on, and it is the smallest one\n * that expresses what gyms actually write: a plan that differs on training days\n * and rest days, five or six eating occasions rather than three, and a portion\n * per food.\n *\n * ── Nothing nutritional is stored on a chart ────────────────────────────────\n * An item stores a food ref and a quantity in grams. Every calorie and every\n * macro on every response is derived by the API through `scaleNutrition` and\n * `sumNutrition`. Storing totals would freeze them: correcting a food's protein\n * would leave every chart built from it quoting the old figure, which is the\n * exact failure the shared `displayName` column taught this codebase to avoid.\n *\n * @module gymmonk-schema/diet-chart\n */\n\nimport { z } from 'zod';\nimport { isoDateTimeSchema, objectIdSchema, optionalTrimmedString } from './common.js';\nimport { foodServingSchema, foodSummarySchema } from './food.js';\nimport { scaledNutritionSchema } from './nutrition.js';\nimport { weekdaySchema } from './shared.js';\n\n// ─── Meal vocabulary ─────────────────────────────────────────────────────────\n\n/**\n * The eating occasions a plan is written in.\n *\n * A fixed list rather than free text, because the member's day is RENDERED in\n * this order and a typed name cannot be sorted: \"Evening Snack\" and \"evening\n * snack\" would become two meals, and nothing could tell whether a pre-workout\n * shake comes before or after lunch. Seven covers the three-meal plan and the\n * six-meal one without the desk person inventing labels.\n */\nexport const mealTypeSchema = z.enum([\n 'early-morning',\n 'breakfast',\n 'mid-morning',\n 'pre-workout',\n 'lunch',\n 'post-workout',\n 'evening-snack',\n 'dinner',\n]);\nexport type MealType = z.infer<typeof mealTypeSchema>;\n\n/** Render order. The enum's own order is the day's order; this makes that explicit. */\nexport const MEAL_TYPE_ORDER: readonly MealType[] = [\n 'early-morning',\n 'breakfast',\n 'mid-morning',\n 'pre-workout',\n 'lunch',\n 'post-workout',\n 'evening-snack',\n 'dinner',\n] as const;\n\n/** Display labels, defined once so the two clients cannot title-case differently. */\nexport const MEAL_TYPE_LABELS: Record<MealType, string> = {\n 'early-morning': 'Early morning',\n breakfast: 'Breakfast',\n 'mid-morning': 'Mid morning',\n 'pre-workout': 'Pre workout',\n lunch: 'Lunch',\n 'post-workout': 'Post workout',\n 'evening-snack': 'Evening snack',\n dinner: 'Dinner',\n};\n\n// ─── Prescription (stored input) ─────────────────────────────────────────────\n\n/**\n * One food in a meal, as stored.\n *\n * `quantityGrams` is the ONLY quantity. A serving label (\"2 rotis\") is resolved\n * to grams by the client at entry using the food's own `serving`, so the record\n * holds one unambiguous number and a food whose serving weight is later\n * corrected does not silently change what an existing chart prescribes.\n */\nexport const dietItemInputSchema = z.object({\n foodId: objectIdSchema,\n quantityGrams: z.number().min(1).max(5000),\n /** \"cook without oil\", \"with skin\". Not nutritional; guidance for the member. */\n note: optionalTrimmedString(200),\n});\nexport type DietItemInput = z.infer<typeof dietItemInputSchema>;\n\nexport const dietMealInputSchema = z.object({\n mealType: mealTypeSchema,\n /**\n * \"07:30\", or absent. A gym that runs to the clock writes times; one that\n * does not writes \"breakfast\" and leaves when to the member. Free text rather\n * than a time type because \"within 30 min of training\" is a real instruction\n * and no time field can hold it.\n */\n timeLabel: optionalTrimmedString(40),\n items: z.array(dietItemInputSchema).max(20).default([]),\n});\nexport type DietMealInput = z.infer<typeof dietMealInputSchema>;\n\nexport const dietChartDayInputSchema = z.object({\n weekday: weekdaySchema,\n meals: z.array(dietMealInputSchema).max(8).default([]),\n});\nexport type DietChartDayInput = z.infer<typeof dietChartDayInputSchema>;\n\n// ─── Populated response shapes ───────────────────────────────────────────────\n\n/** A chart item with its food summary and the nutrition FOR THIS QUANTITY. */\nexport const dietItemSchema = z.object({\n foodId: objectIdSchema,\n food: foodSummarySchema,\n quantityGrams: z.number(),\n note: z.string().nullable(),\n /**\n * How the quantity reads to a person: \"2 rotis (80 g)\" rather than \"80 g\".\n * Null when the food has no serving, in which case grams IS the human form.\n */\n serving: foodServingSchema.nullable(),\n /** `scaleNutrition(food.nutrition, quantityGrams)`. Derived, never stored. */\n nutrition: scaledNutritionSchema,\n});\nexport type DietItem = z.infer<typeof dietItemSchema>;\n\nexport const dietMealSchema = z.object({\n mealType: mealTypeSchema,\n timeLabel: z.string().nullable(),\n items: z.array(dietItemSchema),\n /** Sum of the items. Derived. */\n totals: scaledNutritionSchema,\n});\nexport type DietMeal = z.infer<typeof dietMealSchema>;\n\nexport const dietChartDaySchema = z.object({\n weekday: weekdaySchema,\n meals: z.array(dietMealSchema),\n /** Sum of the day's meals. Derived. */\n totals: scaledNutritionSchema,\n});\nexport type DietChartDay = z.infer<typeof dietChartDaySchema>;\n\n// ─── Entity ──────────────────────────────────────────────────────────────────\n\nexport const dietChartSummarySchema = z.object({\n id: objectIdSchema,\n /**\n * `null` = a PLATFORM template offered to every gym; a center id = that gym's\n * own chart, visible only to its people. Same rule as `WorkoutPlan`.\n */\n centerId: objectIdSchema.nullable(),\n name: z.string(),\n description: z.string().nullable(),\n /** What the chart is FOR — \"Fat loss, 1800 kcal\" — as distinct from the blurb. */\n goals: z.string().nullable(),\n tags: z.array(z.string()),\n /**\n * The daily calorie target this chart was written to, if the author stated\n * one. It is the AUTHOR'S INTENT and is not computed from the meals: the\n * difference between what a chart aims at and what it actually adds up to is\n * the single most useful thing on the builder screen, and deriving this would\n * destroy it by making the two equal by construction.\n */\n targetCalories: z.number().nullable(),\n createdAt: isoDateTimeSchema,\n updatedAt: isoDateTimeSchema,\n});\nexport type DietChartSummary = z.infer<typeof dietChartSummarySchema>;\n\nexport const dietChartSchema = dietChartSummarySchema.extend({\n days: z.array(dietChartDaySchema),\n});\nexport type DietChart = z.infer<typeof dietChartSchema>;\n\n// ─── Create / update body ────────────────────────────────────────────────────\n\nexport const createDietChartBodySchema = z.object({\n /** Nullable, never optional — see `createFoodBodySchema.centerId`. */\n centerId: objectIdSchema.nullable(),\n name: z.string().trim().min(1, 'Chart name is required').max(120),\n description: optionalTrimmedString(2000),\n goals: optionalTrimmedString(400),\n tags: z.array(z.string().trim().max(40)).max(20).default([]),\n targetCalories: z.number().min(0).max(20000).nullable().default(null),\n days: z.array(dietChartDayInputSchema).max(7).default([]),\n});\nexport type CreateDietChartBody = z.infer<typeof createDietChartBodySchema>;\n\nexport const updateDietChartBodySchema = createDietChartBodySchema\n .omit({ centerId: true })\n .partial();\nexport type UpdateDietChartBody = z.infer<typeof updateDietChartBodySchema>;\n\n// ─── Library query ───────────────────────────────────────────────────────────\n\nexport const dietChartQuerySchema = z.object({\n centerId: objectIdSchema.optional(),\n q: optionalTrimmedString(120),\n});\nexport type DietChartQuery = z.infer<typeof dietChartQuerySchema>;\n\n// ─── Assignment ──────────────────────────────────────────────────────────────\n\n/**\n * Assign a chart to a member, or clear it with `null`.\n *\n * Nullable rather than a separate DELETE endpoint so \"change this member's\n * chart\" and \"take it away\" are one operation with one permission check, which\n * is how `assignedWorkoutPlanId` already behaves.\n */\nexport const assignDietChartBodySchema = z.object({\n dietChartId: objectIdSchema.nullable(),\n});\nexport type AssignDietChartBody = z.infer<typeof assignDietChartBodySchema>;\n\n/**\n * What the member's own Diet tab reads.\n *\n * `chart` is null for a member whose gym has not written them one, which is the\n * ordinary state for most members most of the time and therefore not an error.\n * `assignedAt` is what lets the screen say \"since 3 March\" rather than implying\n * the plan is new.\n */\nexport const myDietChartSchema = z.object({\n chart: dietChartSchema.nullable(),\n assignedAt: isoDateTimeSchema.nullable(),\n});\nexport type MyDietChart = z.infer<typeof myDietChartSchema>;\n"]}
1
+ {"version":3,"file":"diet-chart.js","sourceRoot":"","sources":["../src/diet-chart.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * gymmonk-schema — Diet charts\n * =============================\n * A weekly eating plan a gym builds from library foods and assigns to a member.\n * The counterpart to `workout-plan`, and deliberately the same shape: a chart\n * scoped by `centerId`, days keyed by weekday, and a single assignment ref on\n * the member. Whoever has understood one understands the other.\n *\n * chart → days (weekday) → meals (type + time) → items (food + grams)\n *\n * That is the structure PT software converges on, and it is the smallest one\n * that expresses what gyms actually write: a plan that differs on training days\n * and rest days, five or six eating occasions rather than three, and a portion\n * per food.\n *\n * ── Nothing nutritional is stored on a chart ────────────────────────────────\n * An item stores a food ref and a quantity in grams. Every calorie and every\n * macro on every response is derived by the API through `scaleNutrition` and\n * `sumNutrition`. Storing totals would freeze them: correcting a food's protein\n * would leave every chart built from it quoting the old figure, which is the\n * exact failure the shared `displayName` column taught this codebase to avoid.\n *\n * @module gymmonk-schema/diet-chart\n */\n\nimport { z } from 'zod';\nimport { isoDateTimeSchema, objectIdSchema, optionalTrimmedString } from './common.js';\nimport { foodServingSchema, foodSummarySchema } from './food.js';\nimport { scaledNutritionSchema } from './nutrition.js';\nimport { weekdaySchema } from './shared.js';\n\n// ─── Meal vocabulary ─────────────────────────────────────────────────────────\n\n/**\n * The eating occasions a plan is written in.\n *\n * A fixed list rather than free text, because the member's day is RENDERED in\n * this order and a typed name cannot be sorted: \"Evening Snack\" and \"evening\n * snack\" would become two meals, and nothing could tell whether a pre-workout\n * shake comes before or after lunch. Seven covers the three-meal plan and the\n * six-meal one without the desk person inventing labels.\n */\nexport const mealTypeSchema = z.enum([\n 'early-morning',\n 'breakfast',\n 'mid-morning',\n 'pre-workout',\n 'lunch',\n 'post-workout',\n 'evening-snack',\n 'dinner',\n]);\nexport type MealType = z.infer<typeof mealTypeSchema>;\n\n/** Render order. The enum's own order is the day's order; this makes that explicit. */\nexport const MEAL_TYPE_ORDER: readonly MealType[] = [\n 'early-morning',\n 'breakfast',\n 'mid-morning',\n 'pre-workout',\n 'lunch',\n 'post-workout',\n 'evening-snack',\n 'dinner',\n] as const;\n\n/** Display labels, defined once so the two clients cannot title-case differently. */\nexport const MEAL_TYPE_LABELS: Record<MealType, string> = {\n 'early-morning': 'Early morning',\n breakfast: 'Breakfast',\n 'mid-morning': 'Mid morning',\n 'pre-workout': 'Pre workout',\n lunch: 'Lunch',\n 'post-workout': 'Post workout',\n 'evening-snack': 'Evening snack',\n dinner: 'Dinner',\n};\n\n// ─── Prescription (stored input) ─────────────────────────────────────────────\n\n/**\n * One food in a meal, as stored.\n *\n * `quantityGrams` is the ONLY quantity. A serving label (\"2 rotis\") is resolved\n * to grams by the client at entry using the food's own `serving`, so the record\n * holds one unambiguous number and a food whose serving weight is later\n * corrected does not silently change what an existing chart prescribes.\n */\nexport const dietItemInputSchema = z.object({\n foodId: objectIdSchema,\n quantityGrams: z.number().min(1).max(5000),\n /** \"cook without oil\", \"with skin\". Not nutritional; guidance for the member. */\n note: optionalTrimmedString(200),\n});\nexport type DietItemInput = z.infer<typeof dietItemInputSchema>;\n\nexport const dietMealInputSchema = z.object({\n mealType: mealTypeSchema,\n /**\n * \"07:30\", or absent. A gym that runs to the clock writes times; one that\n * does not writes \"breakfast\" and leaves when to the member. Free text rather\n * than a time type because \"within 30 min of training\" is a real instruction\n * and no time field can hold it.\n */\n timeLabel: optionalTrimmedString(40),\n items: z.array(dietItemInputSchema).max(20).default([]),\n});\nexport type DietMealInput = z.infer<typeof dietMealInputSchema>;\n\nexport const dietChartDayInputSchema = z.object({\n weekday: weekdaySchema,\n meals: z.array(dietMealInputSchema).max(8).default([]),\n});\nexport type DietChartDayInput = z.infer<typeof dietChartDayInputSchema>;\n\n// ─── Populated response shapes ───────────────────────────────────────────────\n\n/** A chart item with its food summary and the nutrition FOR THIS QUANTITY. */\nexport const dietItemSchema = z.object({\n foodId: objectIdSchema,\n food: foodSummarySchema,\n quantityGrams: z.number(),\n note: z.string().nullable(),\n /**\n * How the quantity reads to a person: \"2 rotis (80 g)\" rather than \"80 g\".\n * Null when the food has no serving, in which case grams IS the human form.\n */\n serving: foodServingSchema.nullable(),\n /** `scaleNutrition(food.nutrition, quantityGrams)`. Derived, never stored. */\n nutrition: scaledNutritionSchema,\n});\nexport type DietItem = z.infer<typeof dietItemSchema>;\n\nexport const dietMealSchema = z.object({\n mealType: mealTypeSchema,\n timeLabel: z.string().nullable(),\n items: z.array(dietItemSchema),\n /** Sum of the items. Derived. */\n totals: scaledNutritionSchema,\n});\nexport type DietMeal = z.infer<typeof dietMealSchema>;\n\nexport const dietChartDaySchema = z.object({\n weekday: weekdaySchema,\n meals: z.array(dietMealSchema),\n /** Sum of the day's meals. Derived. */\n totals: scaledNutritionSchema,\n});\nexport type DietChartDay = z.infer<typeof dietChartDaySchema>;\n\n// ─── Entity ──────────────────────────────────────────────────────────────────\n\nexport const dietChartSummarySchema = z.object({\n id: objectIdSchema,\n /**\n * `null` = a PLATFORM template offered to every gym; a center id = that gym's\n * own chart, visible only to its people. Same rule as `WorkoutPlan`.\n */\n centerId: objectIdSchema.nullable(),\n name: z.string(),\n description: z.string().nullable(),\n /** What the chart is FOR — \"Fat loss, 1800 kcal\" — as distinct from the blurb. */\n goals: z.string().nullable(),\n tags: z.array(z.string()),\n /**\n * The daily calorie target this chart was written to, if the author stated\n * one. It is the AUTHOR'S INTENT and is not computed from the meals: the\n * difference between what a chart aims at and what it actually adds up to is\n * the single most useful thing on the builder screen, and deriving this would\n * destroy it by making the two equal by construction.\n */\n targetCalories: z.number().nullable(),\n createdAt: isoDateTimeSchema,\n updatedAt: isoDateTimeSchema,\n});\nexport type DietChartSummary = z.infer<typeof dietChartSummarySchema>;\n\nexport const dietChartSchema = dietChartSummarySchema.extend({\n days: z.array(dietChartDaySchema),\n});\nexport type DietChart = z.infer<typeof dietChartSchema>;\n\n// ─── Create / update body ────────────────────────────────────────────────────\n\n/**\n * ⚠️ NO `.default()` IN THIS BASE. It carried three, and one of them lost data.\n *\n * `updateDietChartBodySchema` is this object `.omit().partial()`-ed, and in Zod\n * a `.default()` STILL FIRES under `.partial()` when the key is absent. So\n * `PUT /diet-charts/:id` with `{ name: 'Renamed' }` parsed to\n * `{ name, tags: [], targetCalories: null, days: [] }`, and `updateDietChart`\n * does `$set: body`.\n *\n * **Renaming a chart erased every meal in it**, and every member assigned to it\n * silently got an empty diet. Nothing in the request said so and nothing in the\n * response looked wrong.\n *\n * The defaults live on the CREATE export below, which is the only place a\n * missing key genuinely means \"empty\". Pinned by a test that parses `{}` and\n * asserts nothing comes back.\n */\nconst dietChartBodyBaseSchema = z.object({\n /** Nullable, never optional — see `createFoodBodySchema.centerId`. */\n centerId: objectIdSchema.nullable(),\n name: z.string().trim().min(1, 'Chart name is required').max(120),\n description: optionalTrimmedString(2000),\n goals: optionalTrimmedString(400),\n tags: z.array(z.string().trim().max(40)).max(20),\n targetCalories: z.number().min(0).max(20000).nullable(),\n days: z.array(dietChartDayInputSchema).max(7),\n});\n\nexport const createDietChartBodySchema = dietChartBodyBaseSchema.extend({\n tags: z.array(z.string().trim().max(40)).max(20).default([]),\n targetCalories: z.number().min(0).max(20000).nullable().default(null),\n days: z.array(dietChartDayInputSchema).max(7).default([]),\n});\nexport type CreateDietChartBody = z.infer<typeof createDietChartBodySchema>;\n\nexport const updateDietChartBodySchema = dietChartBodyBaseSchema.omit({ centerId: true }).partial();\nexport type UpdateDietChartBody = z.infer<typeof updateDietChartBodySchema>;\n\n// ─── Library query ───────────────────────────────────────────────────────────\n\nexport const dietChartQuerySchema = z.object({\n centerId: objectIdSchema.optional(),\n q: optionalTrimmedString(120),\n});\nexport type DietChartQuery = z.infer<typeof dietChartQuerySchema>;\n\n// ─── Assignment ──────────────────────────────────────────────────────────────\n\n/**\n * Assign a chart to a member, or clear it with `null`.\n *\n * Nullable rather than a separate DELETE endpoint so \"change this member's\n * chart\" and \"take it away\" are one operation with one permission check, which\n * is how `assignedWorkoutPlanId` already behaves.\n */\nexport const assignDietChartBodySchema = z.object({\n dietChartId: objectIdSchema.nullable(),\n});\nexport type AssignDietChartBody = z.infer<typeof assignDietChartBodySchema>;\n\n/**\n * What the member's own Diet tab reads.\n *\n * `chart` is null for a member whose gym has not written them one, which is the\n * ordinary state for most members most of the time and therefore not an error.\n * `assignedAt` is what lets the screen say \"since 3 March\" rather than implying\n * the plan is new.\n */\nexport const myDietChartSchema = z.object({\n chart: dietChartSchema.nullable(),\n assignedAt: isoDateTimeSchema.nullable(),\n});\nexport type MyDietChart = z.infer<typeof myDietChartSchema>;\n"]}
@@ -60,8 +60,8 @@ export declare const createEquipmentBodySchema: z.ZodObject<{
60
60
  recovery: "recovery";
61
61
  }>;
62
62
  name: z.ZodString;
63
- quantity: z.ZodDefault<z.ZodNumber>;
64
63
  notes: z.ZodPreprocess<z.ZodOptional<z.ZodString>>;
64
+ quantity: z.ZodDefault<z.ZodNumber>;
65
65
  }, z.core.$strip>;
66
66
  export type CreateEquipmentBody = z.infer<typeof createEquipmentBodySchema>;
67
67
  export declare const updateEquipmentBodySchema: z.ZodObject<{
@@ -74,7 +74,7 @@ export declare const updateEquipmentBodySchema: z.ZodObject<{
74
74
  training: "training";
75
75
  recovery: "recovery";
76
76
  }>>;
77
- quantity: z.ZodOptional<z.ZodDefault<z.ZodNumber>>;
77
+ quantity: z.ZodOptional<z.ZodNumber>;
78
78
  notes: z.ZodOptional<z.ZodPreprocess<z.ZodOptional<z.ZodString>>>;
79
79
  }, z.core.$strip>;
80
80
  export type UpdateEquipmentBody = z.infer<typeof updateEquipmentBodySchema>;