gymmonk-schema 0.25.0 → 0.26.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.
@@ -18,9 +18,23 @@ export declare const announcementIconSchema: z.ZodEnum<{
18
18
  warning: "warning";
19
19
  }>;
20
20
  export type AnnouncementIcon = z.infer<typeof announcementIconSchema>;
21
+ /**
22
+ * An announcement is a PRESENT EVENT.
23
+ *
24
+ * `scheduled` is gone. A broadcast now goes out the moment it is created, which
25
+ * removes the whole apparatus that existed to make "later" work: the due sweep,
26
+ * the claim protocol, the past-date guard, and the Cloud Scheduler job that had
27
+ * to wake a scale-to-zero service every minute for it to mean anything. That
28
+ * job never existed in production, so every scheduled announcement was in fact
29
+ * delivered "the next time somebody opened the app" — the feature promised a
30
+ * time and could not keep it.
31
+ *
32
+ * `draft` stays, and is not a contradiction: a draft is not a queued
33
+ * announcement, it is an unsent note. Sending it is still a present event, it
34
+ * just happens when the owner presses the button rather than when they save.
35
+ */
21
36
  export declare const announcementStatusSchema: z.ZodEnum<{
22
37
  draft: "draft";
23
- scheduled: "scheduled";
24
38
  sent: "sent";
25
39
  }>;
26
40
  export type AnnouncementStatus = z.infer<typeof announcementStatusSchema>;
@@ -35,30 +49,21 @@ export declare const announcementSchema: z.ZodObject<{
35
49
  body: z.ZodString;
36
50
  status: z.ZodEnum<{
37
51
  draft: "draft";
38
- scheduled: "scheduled";
39
52
  sent: "sent";
40
53
  }>;
41
- scheduledAt: z.ZodNullable<z.ZodISODateTime>;
42
54
  sentAt: z.ZodNullable<z.ZodISODateTime>;
43
55
  deliveredCount: z.ZodNullable<z.ZodNumber>;
44
- deliveryFailedAt: z.ZodNullable<z.ZodISODateTime>;
45
56
  createdAt: z.ZodISODateTime;
46
57
  updatedAt: z.ZodISODateTime;
47
58
  }, z.core.$strip>;
48
59
  export type Announcement = z.infer<typeof announcementSchema>;
49
60
  /**
50
- * How far in the past a `scheduledAt` may sit and still be treated as "now".
61
+ * Write one and it goes out, unless it is explicitly saved as a draft.
51
62
  *
52
- * Not zero. The browser builds the instant from two wall-clock inputs and the
53
- * request then crosses a network, so an owner who picks the next minute and
54
- * taps promptly can arrive a few seconds "late" through no fault of their own.
55
- * Refusing that would be a validation error for doing exactly the right thing.
56
- * Anything older is a genuine mistake: it would be delivered by the very next
57
- * sweep while the screen called it Scheduled.
63
+ * No `scheduledAt`, and no refinements: with delivery immediate there is
64
+ * nothing left to cross-validate. The two that used to be here both existed to
65
+ * police a future time that no longer exists.
58
66
  */
59
- export declare const SCHEDULE_GRACE_MS: number;
60
- export declare const PAST_SCHEDULE_MESSAGE = "Pick a time in the future";
61
- export declare function isScheduleInPast(iso: string, now?: Date): boolean;
62
67
  export declare const createAnnouncementBodySchema: z.ZodObject<{
63
68
  icon: z.ZodDefault<z.ZodEnum<{
64
69
  megaphone: "megaphone";
@@ -66,43 +71,34 @@ export declare const createAnnouncementBodySchema: z.ZodObject<{
66
71
  }>>;
67
72
  title: z.ZodString;
68
73
  body: z.ZodString;
69
- scheduledAt: z.ZodOptional<z.ZodISODateTime>;
70
74
  status: z.ZodDefault<z.ZodEnum<{
71
75
  draft: "draft";
72
- scheduled: "scheduled";
73
76
  sent: "sent";
74
77
  }>>;
75
78
  }, z.core.$strip>;
76
79
  export type CreateAnnouncementBody = z.infer<typeof createAnnouncementBodySchema>;
77
80
  /** The create body plus the center it belongs to, as the route receives it. */
78
- export declare const createAnnouncementWithCenterSchema: z.ZodIntersection<z.ZodObject<{
81
+ export declare const createAnnouncementWithCenterSchema: z.ZodObject<{
79
82
  icon: z.ZodDefault<z.ZodEnum<{
80
83
  megaphone: "megaphone";
81
84
  warning: "warning";
82
85
  }>>;
83
86
  title: z.ZodString;
84
87
  body: z.ZodString;
85
- scheduledAt: z.ZodOptional<z.ZodISODateTime>;
86
88
  status: z.ZodDefault<z.ZodEnum<{
87
89
  draft: "draft";
88
- scheduled: "scheduled";
89
90
  sent: "sent";
90
91
  }>>;
91
- }, z.core.$strip>, z.ZodObject<{
92
92
  centerId: z.ZodString;
93
- }, z.core.$strip>>;
93
+ }, z.core.$strip>;
94
94
  export type CreateAnnouncementWithCenterBody = z.infer<typeof createAnnouncementWithCenterSchema>;
95
95
  /**
96
- * Edit a draft, reschedule a queued one, or send either.
96
+ * Edit a draft, and send it.
97
97
  *
98
98
  * Every field is optional because this is a PATCH: an owner fixing a typo sends
99
- * the title alone. `status` is how a draft finally goes out, which is the whole
100
- * reason this exists — before it there was no update endpoint at all, so "Save
101
- * as Draft" wrote a row that could only ever be deleted.
102
- *
103
- * `scheduledAt` accepts null explicitly, meaning "drop the schedule", which is
104
- * different from omitting it ("leave the schedule alone"). Conflating the two
105
- * would make it impossible to turn a scheduled announcement back into a draft.
99
+ * the title alone. `status: 'sent'` is how a draft finally goes out, which is
100
+ * the whole reason this exists — before it there was no update endpoint at all,
101
+ * so "Save as Draft" wrote a row that could only ever be deleted.
106
102
  */
107
103
  export declare const updateAnnouncementBodySchema: z.ZodObject<{
108
104
  icon: z.ZodOptional<z.ZodEnum<{
@@ -113,31 +109,25 @@ export declare const updateAnnouncementBodySchema: z.ZodObject<{
113
109
  body: z.ZodOptional<z.ZodString>;
114
110
  status: z.ZodOptional<z.ZodEnum<{
115
111
  draft: "draft";
116
- scheduled: "scheduled";
117
112
  sent: "sent";
118
113
  }>>;
119
- scheduledAt: z.ZodOptional<z.ZodNullable<z.ZodISODateTime>>;
120
114
  }, z.core.$strip>;
121
115
  export type UpdateAnnouncementBody = z.infer<typeof updateAnnouncementBodySchema>;
122
116
  /**
123
- * How the list is ordered.
117
+ * How the list is ordered. `newest` by default, because the screen's job is
118
+ * "what did we just send".
124
119
  *
125
- * `newest` is the default because the screen's job is "what did we just send".
126
- * `upcoming` orders by `scheduledAt` ascending and is meaningful only for rows
127
- * that HAVE one: in a mixed list every draft and every already-sent row sorts as
128
- * null and groups at the front, so the UI offers it under the Scheduled filter
129
- * alone.
120
+ * There was a third, `upcoming`, ordering by the scheduled time. It went with
121
+ * scheduling.
130
122
  */
131
123
  export declare const announcementSortSchema: z.ZodEnum<{
132
124
  newest: "newest";
133
125
  oldest: "oldest";
134
- upcoming: "upcoming";
135
126
  }>;
136
127
  export type AnnouncementSort = z.infer<typeof announcementSortSchema>;
137
128
  /** `all` is a real choice, not an absent filter, so the UI can round-trip it. */
138
129
  export declare const announcementStatusFilterSchema: z.ZodUnion<readonly [z.ZodLiteral<"all">, z.ZodEnum<{
139
130
  draft: "draft";
140
- scheduled: "scheduled";
141
131
  sent: "sent";
142
132
  }>]>;
143
133
  export type AnnouncementStatusFilter = z.infer<typeof announcementStatusFilterSchema>;
@@ -155,13 +145,11 @@ export declare const listAnnouncementsQuerySchema: z.ZodObject<{
155
145
  q: z.ZodOptional<z.ZodString>;
156
146
  status: z.ZodDefault<z.ZodUnion<readonly [z.ZodLiteral<"all">, z.ZodEnum<{
157
147
  draft: "draft";
158
- scheduled: "scheduled";
159
148
  sent: "sent";
160
149
  }>]>>;
161
150
  sort: z.ZodDefault<z.ZodEnum<{
162
151
  newest: "newest";
163
152
  oldest: "oldest";
164
- upcoming: "upcoming";
165
153
  }>>;
166
154
  }, z.core.$strip>;
167
155
  export type ListAnnouncementsQuery = z.infer<typeof listAnnouncementsQuerySchema>;
@@ -1 +1 @@
1
- {"version":3,"file":"announcement.d.ts","sourceRoot":"","sources":["../src/announcement.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAGxB,eAAO,MAAM,sBAAsB;;;EAAmC,CAAC;AACvE,MAAM,MAAM,gBAAgB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,sBAAsB,CAAC,CAAC;AAEtE,eAAO,MAAM,wBAAwB;;;;EAAyC,CAAC;AAC/E,MAAM,MAAM,kBAAkB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,wBAAwB,CAAC,CAAC;AAI1E,eAAO,MAAM,kBAAkB;;;;;;;;;;;;;;;;;;;;iBA4B7B,CAAC;AACH,MAAM,MAAM,YAAY,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,kBAAkB,CAAC,CAAC;AAI9D;;;;;;;;;GASG;AACH,eAAO,MAAM,iBAAiB,QAAgB,CAAC;AAE/C,eAAO,MAAM,qBAAqB,8BAA8B,CAAC;AAEjE,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,MAAM,EAAE,GAAG,GAAE,IAAiB,GAAG,OAAO,CAI7E;AAID,eAAO,MAAM,4BAA4B;;;;;;;;;;;;;iBAwBrC,CAAC;AACL,MAAM,MAAM,sBAAsB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,4BAA4B,CAAC,CAAC;AAElF,+EAA+E;AAC/E,eAAO,MAAM,kCAAkC;;;;;;;;;;;;;;;kBAE9C,CAAC;AACF,MAAM,MAAM,gCAAgC,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,kCAAkC,CAAC,CAAC;AAIlG;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,4BAA4B;;;;;;;;;;;;;iBAMvC,CAAC;AACH,MAAM,MAAM,sBAAsB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,4BAA4B,CAAC,CAAC;AAIlF;;;;;;;;GAQG;AACH,eAAO,MAAM,sBAAsB;;;;EAA2C,CAAC;AAC/E,MAAM,MAAM,gBAAgB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,sBAAsB,CAAC,CAAC;AAEtE,iFAAiF;AACjF,eAAO,MAAM,8BAA8B;;;;IAAwD,CAAC;AACpG,MAAM,MAAM,wBAAwB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,8BAA8B,CAAC,CAAC;AAEtF;;;;;;GAMG;AACH,eAAO,MAAM,4BAA4B;;;;;;;;;;;;;;;iBAMvC,CAAC;AACH,MAAM,MAAM,sBAAsB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,4BAA4B,CAAC,CAAC;AAElF,kFAAkF;AAClF,eAAO,MAAM,sBAAsB;;iBAAyC,CAAC;AAC7E,MAAM,MAAM,gBAAgB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,sBAAsB,CAAC,CAAC"}
1
+ {"version":3,"file":"announcement.d.ts","sourceRoot":"","sources":["../src/announcement.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAGxB,eAAO,MAAM,sBAAsB;;;EAAmC,CAAC;AACvE,MAAM,MAAM,gBAAgB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,sBAAsB,CAAC,CAAC;AAEtE;;;;;;;;;;;;;;GAcG;AACH,eAAO,MAAM,wBAAwB;;;EAA4B,CAAC;AAClE,MAAM,MAAM,kBAAkB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,wBAAwB,CAAC,CAAC;AAI1E,eAAO,MAAM,kBAAkB;;;;;;;;;;;;;;;;;iBAmB7B,CAAC;AACH,MAAM,MAAM,YAAY,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,kBAAkB,CAAC,CAAC;AAI9D;;;;;;GAMG;AACH,eAAO,MAAM,4BAA4B;;;;;;;;;;;iBAKvC,CAAC;AACH,MAAM,MAAM,sBAAsB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,4BAA4B,CAAC,CAAC;AAElF,+EAA+E;AAC/E,eAAO,MAAM,kCAAkC;;;;;;;;;;;;iBAE7C,CAAC;AACH,MAAM,MAAM,gCAAgC,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,kCAAkC,CAAC,CAAC;AAIlG;;;;;;;GAOG;AACH,eAAO,MAAM,4BAA4B;;;;;;;;;;;iBAKvC,CAAC;AACH,MAAM,MAAM,sBAAsB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,4BAA4B,CAAC,CAAC;AAIlF;;;;;;GAMG;AACH,eAAO,MAAM,sBAAsB;;;EAA+B,CAAC;AACnE,MAAM,MAAM,gBAAgB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,sBAAsB,CAAC,CAAC;AAEtE,iFAAiF;AACjF,eAAO,MAAM,8BAA8B;;;IAAwD,CAAC;AACpG,MAAM,MAAM,wBAAwB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,8BAA8B,CAAC,CAAC;AAEtF;;;;;;GAMG;AACH,eAAO,MAAM,4BAA4B;;;;;;;;;;;;;iBAMvC,CAAC;AACH,MAAM,MAAM,sBAAsB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,4BAA4B,CAAC,CAAC;AAElF,kFAAkF;AAClF,eAAO,MAAM,sBAAsB;;iBAAyC,CAAC;AAC7E,MAAM,MAAM,gBAAgB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,sBAAsB,CAAC,CAAC"}
@@ -15,7 +15,22 @@
15
15
  import { z } from 'zod';
16
16
  import { isoDateTimeSchema, objectIdSchema, paginationSchema } from './common.js';
17
17
  export const announcementIconSchema = z.enum(['megaphone', 'warning']);
18
- export const announcementStatusSchema = z.enum(['draft', 'scheduled', 'sent']);
18
+ /**
19
+ * An announcement is a PRESENT EVENT.
20
+ *
21
+ * `scheduled` is gone. A broadcast now goes out the moment it is created, which
22
+ * removes the whole apparatus that existed to make "later" work: the due sweep,
23
+ * the claim protocol, the past-date guard, and the Cloud Scheduler job that had
24
+ * to wake a scale-to-zero service every minute for it to mean anything. That
25
+ * job never existed in production, so every scheduled announcement was in fact
26
+ * delivered "the next time somebody opened the app" — the feature promised a
27
+ * time and could not keep it.
28
+ *
29
+ * `draft` stays, and is not a contradiction: a draft is not a queued
30
+ * announcement, it is an unsent note. Sending it is still a present event, it
31
+ * just happens when the owner presses the button rather than when they save.
32
+ */
33
+ export const announcementStatusSchema = z.enum(['draft', 'sent']);
19
34
  // ─── Entity ──────────────────────────────────────────────────────────────────
20
35
  export const announcementSchema = z.object({
21
36
  id: objectIdSchema,
@@ -24,7 +39,6 @@ export const announcementSchema = z.object({
24
39
  title: z.string(),
25
40
  body: z.string(),
26
41
  status: announcementStatusSchema,
27
- scheduledAt: isoDateTimeSchema.nullable(),
28
42
  sentAt: isoDateTimeSchema.nullable(),
29
43
  /**
30
44
  * How many people it actually reached, once delivery has been attempted.
@@ -35,95 +49,51 @@ export const announcementSchema = z.object({
35
49
  * both to zero would leave the screen unable to say which.
36
50
  */
37
51
  deliveredCount: z.number().int().nullable(),
38
- /**
39
- * Set when fan-out threw, cleared when a later attempt succeeds.
40
- *
41
- * A row is marked `sent` BEFORE fan-out (it prevents a duplicate broadcast to
42
- * an entire gym), so without this a failure left no trace but a log line while
43
- * the owner's screen said Sent.
44
- */
45
- deliveryFailedAt: isoDateTimeSchema.nullable(),
46
52
  createdAt: isoDateTimeSchema,
47
53
  updatedAt: isoDateTimeSchema,
48
54
  });
49
- // ─── Scheduling guard ────────────────────────────────────────────────────────
55
+ // ─── Create body ─────────────────────────────────────────────────────────────
50
56
  /**
51
- * How far in the past a `scheduledAt` may sit and still be treated as "now".
57
+ * Write one and it goes out, unless it is explicitly saved as a draft.
52
58
  *
53
- * Not zero. The browser builds the instant from two wall-clock inputs and the
54
- * request then crosses a network, so an owner who picks the next minute and
55
- * taps promptly can arrive a few seconds "late" through no fault of their own.
56
- * Refusing that would be a validation error for doing exactly the right thing.
57
- * Anything older is a genuine mistake: it would be delivered by the very next
58
- * sweep while the screen called it Scheduled.
59
+ * No `scheduledAt`, and no refinements: with delivery immediate there is
60
+ * nothing left to cross-validate. The two that used to be here both existed to
61
+ * police a future time that no longer exists.
59
62
  */
60
- export const SCHEDULE_GRACE_MS = 2 * 60 * 1000;
61
- export const PAST_SCHEDULE_MESSAGE = 'Pick a time in the future';
62
- export function isScheduleInPast(iso, now = new Date()) {
63
- const at = Date.parse(iso);
64
- if (Number.isNaN(at))
65
- return false; // shape is the format check's problem
66
- return at < now.getTime() - SCHEDULE_GRACE_MS;
67
- }
68
- // ─── Create body ─────────────────────────────────────────────────────────────
69
- export const createAnnouncementBodySchema = z
70
- .object({
63
+ export const createAnnouncementBodySchema = z.object({
71
64
  icon: announcementIconSchema.default('megaphone'),
72
65
  title: z.string().trim().min(1, 'Title is required').max(120),
73
66
  body: z.string().trim().min(1, 'Message is required').max(250),
74
- /** Required when `status` is `scheduled`; otherwise sent immediately. */
75
- scheduledAt: isoDateTimeSchema.optional(),
76
67
  status: announcementStatusSchema.default('sent'),
77
- })
78
- .refine((v) => v.status !== 'scheduled' || v.scheduledAt != null, {
79
- message: 'Pick a schedule time',
80
- path: ['scheduledAt'],
81
- })
82
- /**
83
- * A scheduled time must be in the FUTURE. Without this, yesterday 9am
84
- * validated cleanly, was stored as `scheduled`, and went out on the next sweep
85
- * seconds later: the owner saw "Scheduled" and the members got it at once.
86
- *
87
- * Drafts are exempt on purpose. Nothing is going out, so a stale time on one
88
- * is not yet a mistake.
89
- */
90
- .refine((v) => v.status !== 'scheduled' || !v.scheduledAt || !isScheduleInPast(v.scheduledAt), {
91
- message: PAST_SCHEDULE_MESSAGE,
92
- path: ['scheduledAt'],
93
68
  });
94
69
  /** The create body plus the center it belongs to, as the route receives it. */
95
- export const createAnnouncementWithCenterSchema = createAnnouncementBodySchema.and(z.object({ centerId: objectIdSchema }));
70
+ export const createAnnouncementWithCenterSchema = createAnnouncementBodySchema.extend({
71
+ centerId: objectIdSchema,
72
+ });
96
73
  // ─── Update body ─────────────────────────────────────────────────────────────
97
74
  /**
98
- * Edit a draft, reschedule a queued one, or send either.
75
+ * Edit a draft, and send it.
99
76
  *
100
77
  * Every field is optional because this is a PATCH: an owner fixing a typo sends
101
- * the title alone. `status` is how a draft finally goes out, which is the whole
102
- * reason this exists — before it there was no update endpoint at all, so "Save
103
- * as Draft" wrote a row that could only ever be deleted.
104
- *
105
- * `scheduledAt` accepts null explicitly, meaning "drop the schedule", which is
106
- * different from omitting it ("leave the schedule alone"). Conflating the two
107
- * would make it impossible to turn a scheduled announcement back into a draft.
78
+ * the title alone. `status: 'sent'` is how a draft finally goes out, which is
79
+ * the whole reason this exists — before it there was no update endpoint at all,
80
+ * so "Save as Draft" wrote a row that could only ever be deleted.
108
81
  */
109
82
  export const updateAnnouncementBodySchema = z.object({
110
83
  icon: announcementIconSchema.optional(),
111
84
  title: z.string().trim().min(1, 'Title is required').max(120).optional(),
112
85
  body: z.string().trim().min(1, 'Message is required').max(250).optional(),
113
86
  status: announcementStatusSchema.optional(),
114
- scheduledAt: isoDateTimeSchema.nullable().optional(),
115
87
  });
116
88
  // ─── Listing query ───────────────────────────────────────────────────────────
117
89
  /**
118
- * How the list is ordered.
90
+ * How the list is ordered. `newest` by default, because the screen's job is
91
+ * "what did we just send".
119
92
  *
120
- * `newest` is the default because the screen's job is "what did we just send".
121
- * `upcoming` orders by `scheduledAt` ascending and is meaningful only for rows
122
- * that HAVE one: in a mixed list every draft and every already-sent row sorts as
123
- * null and groups at the front, so the UI offers it under the Scheduled filter
124
- * alone.
93
+ * There was a third, `upcoming`, ordering by the scheduled time. It went with
94
+ * scheduling.
125
95
  */
126
- export const announcementSortSchema = z.enum(['newest', 'oldest', 'upcoming']);
96
+ export const announcementSortSchema = z.enum(['newest', 'oldest']);
127
97
  /** `all` is a real choice, not an absent filter, so the UI can round-trip it. */
128
98
  export const announcementStatusFilterSchema = z.union([z.literal('all'), announcementStatusSchema]);
129
99
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"announcement.js","sourceRoot":"","sources":["../src/announcement.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * gymmonk-schema — Owner announcements\n * ====================================\n * Broadcasts an owner sends to a center's members and staff (now or scheduled).\n * Delivered as notifications + push.\n *\n * SINGLE SOURCE OF TRUTH. The listing query, the update body and the\n * past-schedule guard lived in gymmonk-backend and gymmonk-web-client as two\n * hand-kept mirrors before this. They are here so a field can only be added\n * once: zod STRIPS unknown keys, so a mirror that drifts does not fail loudly,\n * it silently drops the field somewhere between the browser and the database.\n *\n * @module gymmonk-schema/announcement\n */\n\nimport { z } from 'zod';\nimport { isoDateTimeSchema, objectIdSchema, paginationSchema } from './common.js';\n\nexport const announcementIconSchema = z.enum(['megaphone', 'warning']);\nexport type AnnouncementIcon = z.infer<typeof announcementIconSchema>;\n\nexport const announcementStatusSchema = z.enum(['draft', 'scheduled', 'sent']);\nexport type AnnouncementStatus = z.infer<typeof announcementStatusSchema>;\n\n// ─── Entity ──────────────────────────────────────────────────────────────────\n\nexport const announcementSchema = z.object({\n id: objectIdSchema,\n centerId: objectIdSchema,\n icon: announcementIconSchema,\n title: z.string(),\n body: z.string(),\n status: announcementStatusSchema,\n scheduledAt: isoDateTimeSchema.nullable(),\n sentAt: isoDateTimeSchema.nullable(),\n /**\n * How many people it actually reached, once delivery has been attempted.\n *\n * `null` means \"not attempted yet\", which is NOT the same as `0` (\"attempted,\n * nobody to tell\"). An owner broadcasting to an empty roster and an owner\n * whose broadcast has not run yet are different situations, and collapsing\n * both to zero would leave the screen unable to say which.\n */\n deliveredCount: z.number().int().nullable(),\n /**\n * Set when fan-out threw, cleared when a later attempt succeeds.\n *\n * A row is marked `sent` BEFORE fan-out (it prevents a duplicate broadcast to\n * an entire gym), so without this a failure left no trace but a log line while\n * the owner's screen said Sent.\n */\n deliveryFailedAt: isoDateTimeSchema.nullable(),\n createdAt: isoDateTimeSchema,\n updatedAt: isoDateTimeSchema,\n});\nexport type Announcement = z.infer<typeof announcementSchema>;\n\n// ─── Scheduling guard ────────────────────────────────────────────────────────\n\n/**\n * How far in the past a `scheduledAt` may sit and still be treated as \"now\".\n *\n * Not zero. The browser builds the instant from two wall-clock inputs and the\n * request then crosses a network, so an owner who picks the next minute and\n * taps promptly can arrive a few seconds \"late\" through no fault of their own.\n * Refusing that would be a validation error for doing exactly the right thing.\n * Anything older is a genuine mistake: it would be delivered by the very next\n * sweep while the screen called it Scheduled.\n */\nexport const SCHEDULE_GRACE_MS = 2 * 60 * 1000;\n\nexport const PAST_SCHEDULE_MESSAGE = 'Pick a time in the future';\n\nexport function isScheduleInPast(iso: string, now: Date = new Date()): boolean {\n const at = Date.parse(iso);\n if (Number.isNaN(at)) return false; // shape is the format check's problem\n return at < now.getTime() - SCHEDULE_GRACE_MS;\n}\n\n// ─── Create body ─────────────────────────────────────────────────────────────\n\nexport const createAnnouncementBodySchema = z\n .object({\n icon: announcementIconSchema.default('megaphone'),\n title: z.string().trim().min(1, 'Title is required').max(120),\n body: z.string().trim().min(1, 'Message is required').max(250),\n /** Required when `status` is `scheduled`; otherwise sent immediately. */\n scheduledAt: isoDateTimeSchema.optional(),\n status: announcementStatusSchema.default('sent'),\n })\n .refine((v) => v.status !== 'scheduled' || v.scheduledAt != null, {\n message: 'Pick a schedule time',\n path: ['scheduledAt'],\n })\n /**\n * A scheduled time must be in the FUTURE. Without this, yesterday 9am\n * validated cleanly, was stored as `scheduled`, and went out on the next sweep\n * seconds later: the owner saw \"Scheduled\" and the members got it at once.\n *\n * Drafts are exempt on purpose. Nothing is going out, so a stale time on one\n * is not yet a mistake.\n */\n .refine((v) => v.status !== 'scheduled' || !v.scheduledAt || !isScheduleInPast(v.scheduledAt), {\n message: PAST_SCHEDULE_MESSAGE,\n path: ['scheduledAt'],\n });\nexport type CreateAnnouncementBody = z.infer<typeof createAnnouncementBodySchema>;\n\n/** The create body plus the center it belongs to, as the route receives it. */\nexport const createAnnouncementWithCenterSchema = createAnnouncementBodySchema.and(\n z.object({ centerId: objectIdSchema }),\n);\nexport type CreateAnnouncementWithCenterBody = z.infer<typeof createAnnouncementWithCenterSchema>;\n\n// ─── Update body ─────────────────────────────────────────────────────────────\n\n/**\n * Edit a draft, reschedule a queued one, or send either.\n *\n * Every field is optional because this is a PATCH: an owner fixing a typo sends\n * the title alone. `status` is how a draft finally goes out, which is the whole\n * reason this exists — before it there was no update endpoint at all, so \"Save\n * as Draft\" wrote a row that could only ever be deleted.\n *\n * `scheduledAt` accepts null explicitly, meaning \"drop the schedule\", which is\n * different from omitting it (\"leave the schedule alone\"). Conflating the two\n * would make it impossible to turn a scheduled announcement back into a draft.\n */\nexport const updateAnnouncementBodySchema = z.object({\n icon: announcementIconSchema.optional(),\n title: z.string().trim().min(1, 'Title is required').max(120).optional(),\n body: z.string().trim().min(1, 'Message is required').max(250).optional(),\n status: announcementStatusSchema.optional(),\n scheduledAt: isoDateTimeSchema.nullable().optional(),\n});\nexport type UpdateAnnouncementBody = z.infer<typeof updateAnnouncementBodySchema>;\n\n// ─── Listing query ───────────────────────────────────────────────────────────\n\n/**\n * How the list is ordered.\n *\n * `newest` is the default because the screen's job is \"what did we just send\".\n * `upcoming` orders by `scheduledAt` ascending and is meaningful only for rows\n * that HAVE one: in a mixed list every draft and every already-sent row sorts as\n * null and groups at the front, so the UI offers it under the Scheduled filter\n * alone.\n */\nexport const announcementSortSchema = z.enum(['newest', 'oldest', 'upcoming']);\nexport type AnnouncementSort = z.infer<typeof announcementSortSchema>;\n\n/** `all` is a real choice, not an absent filter, so the UI can round-trip it. */\nexport const announcementStatusFilterSchema = z.union([z.literal('all'), announcementStatusSchema]);\nexport type AnnouncementStatusFilter = z.infer<typeof announcementStatusFilterSchema>;\n\n/**\n * The listing query.\n *\n * Everything arrives as a STRING off the query string, so `paginationSchema`\n * coerces page and limit. Without the coercion `page=2` fails validation and\n * the client gets a 400 for a link it built correctly.\n */\nexport const listAnnouncementsQuerySchema = paginationSchema.extend({\n centerId: objectIdSchema,\n /** Free text over title and body. Trimmed; empty means \"no search\". */\n q: z.string().trim().max(120).optional(),\n status: announcementStatusFilterSchema.default('all'),\n sort: announcementSortSchema.default('newest'),\n});\nexport type ListAnnouncementsQuery = z.infer<typeof listAnnouncementsQuerySchema>;\n\n/** Just the center, for routes that identify the row by id but still scope it. */\nexport const centerScopeQuerySchema = z.object({ centerId: objectIdSchema });\nexport type CenterScopeQuery = z.infer<typeof centerScopeQuerySchema>;\n"]}
1
+ {"version":3,"file":"announcement.js","sourceRoot":"","sources":["../src/announcement.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * gymmonk-schema — Owner announcements\n * ====================================\n * Broadcasts an owner sends to a center's members and staff (now or scheduled).\n * Delivered as notifications + push.\n *\n * SINGLE SOURCE OF TRUTH. The listing query, the update body and the\n * past-schedule guard lived in gymmonk-backend and gymmonk-web-client as two\n * hand-kept mirrors before this. They are here so a field can only be added\n * once: zod STRIPS unknown keys, so a mirror that drifts does not fail loudly,\n * it silently drops the field somewhere between the browser and the database.\n *\n * @module gymmonk-schema/announcement\n */\n\nimport { z } from 'zod';\nimport { isoDateTimeSchema, objectIdSchema, paginationSchema } from './common.js';\n\nexport const announcementIconSchema = z.enum(['megaphone', 'warning']);\nexport type AnnouncementIcon = z.infer<typeof announcementIconSchema>;\n\n/**\n * An announcement is a PRESENT EVENT.\n *\n * `scheduled` is gone. A broadcast now goes out the moment it is created, which\n * removes the whole apparatus that existed to make \"later\" work: the due sweep,\n * the claim protocol, the past-date guard, and the Cloud Scheduler job that had\n * to wake a scale-to-zero service every minute for it to mean anything. That\n * job never existed in production, so every scheduled announcement was in fact\n * delivered \"the next time somebody opened the app\" — the feature promised a\n * time and could not keep it.\n *\n * `draft` stays, and is not a contradiction: a draft is not a queued\n * announcement, it is an unsent note. Sending it is still a present event, it\n * just happens when the owner presses the button rather than when they save.\n */\nexport const announcementStatusSchema = z.enum(['draft', 'sent']);\nexport type AnnouncementStatus = z.infer<typeof announcementStatusSchema>;\n\n// ─── Entity ──────────────────────────────────────────────────────────────────\n\nexport const announcementSchema = z.object({\n id: objectIdSchema,\n centerId: objectIdSchema,\n icon: announcementIconSchema,\n title: z.string(),\n body: z.string(),\n status: announcementStatusSchema,\n sentAt: isoDateTimeSchema.nullable(),\n /**\n * How many people it actually reached, once delivery has been attempted.\n *\n * `null` means \"not attempted yet\", which is NOT the same as `0` (\"attempted,\n * nobody to tell\"). An owner broadcasting to an empty roster and an owner\n * whose broadcast has not run yet are different situations, and collapsing\n * both to zero would leave the screen unable to say which.\n */\n deliveredCount: z.number().int().nullable(),\n createdAt: isoDateTimeSchema,\n updatedAt: isoDateTimeSchema,\n});\nexport type Announcement = z.infer<typeof announcementSchema>;\n\n// ─── Create body ─────────────────────────────────────────────────────────────\n\n/**\n * Write one and it goes out, unless it is explicitly saved as a draft.\n *\n * No `scheduledAt`, and no refinements: with delivery immediate there is\n * nothing left to cross-validate. The two that used to be here both existed to\n * police a future time that no longer exists.\n */\nexport const createAnnouncementBodySchema = z.object({\n icon: announcementIconSchema.default('megaphone'),\n title: z.string().trim().min(1, 'Title is required').max(120),\n body: z.string().trim().min(1, 'Message is required').max(250),\n status: announcementStatusSchema.default('sent'),\n});\nexport type CreateAnnouncementBody = z.infer<typeof createAnnouncementBodySchema>;\n\n/** The create body plus the center it belongs to, as the route receives it. */\nexport const createAnnouncementWithCenterSchema = createAnnouncementBodySchema.extend({\n centerId: objectIdSchema,\n});\nexport type CreateAnnouncementWithCenterBody = z.infer<typeof createAnnouncementWithCenterSchema>;\n\n// ─── Update body ─────────────────────────────────────────────────────────────\n\n/**\n * Edit a draft, and send it.\n *\n * Every field is optional because this is a PATCH: an owner fixing a typo sends\n * the title alone. `status: 'sent'` is how a draft finally goes out, which is\n * the whole reason this exists — before it there was no update endpoint at all,\n * so \"Save as Draft\" wrote a row that could only ever be deleted.\n */\nexport const updateAnnouncementBodySchema = z.object({\n icon: announcementIconSchema.optional(),\n title: z.string().trim().min(1, 'Title is required').max(120).optional(),\n body: z.string().trim().min(1, 'Message is required').max(250).optional(),\n status: announcementStatusSchema.optional(),\n});\nexport type UpdateAnnouncementBody = z.infer<typeof updateAnnouncementBodySchema>;\n\n// ─── Listing query ───────────────────────────────────────────────────────────\n\n/**\n * How the list is ordered. `newest` by default, because the screen's job is\n * \"what did we just send\".\n *\n * There was a third, `upcoming`, ordering by the scheduled time. It went with\n * scheduling.\n */\nexport const announcementSortSchema = z.enum(['newest', 'oldest']);\nexport type AnnouncementSort = z.infer<typeof announcementSortSchema>;\n\n/** `all` is a real choice, not an absent filter, so the UI can round-trip it. */\nexport const announcementStatusFilterSchema = z.union([z.literal('all'), announcementStatusSchema]);\nexport type AnnouncementStatusFilter = z.infer<typeof announcementStatusFilterSchema>;\n\n/**\n * The listing query.\n *\n * Everything arrives as a STRING off the query string, so `paginationSchema`\n * coerces page and limit. Without the coercion `page=2` fails validation and\n * the client gets a 400 for a link it built correctly.\n */\nexport const listAnnouncementsQuerySchema = paginationSchema.extend({\n centerId: objectIdSchema,\n /** Free text over title and body. Trimmed; empty means \"no search\". */\n q: z.string().trim().max(120).optional(),\n status: announcementStatusFilterSchema.default('all'),\n sort: announcementSortSchema.default('newest'),\n});\nexport type ListAnnouncementsQuery = z.infer<typeof listAnnouncementsQuerySchema>;\n\n/** Just the center, for routes that identify the row by id but still scope it. */\nexport const centerScopeQuerySchema = z.object({ centerId: objectIdSchema });\nexport type CenterScopeQuery = z.infer<typeof centerScopeQuerySchema>;\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gymmonk-schema",
3
- "version": "0.25.0",
3
+ "version": "0.26.0",
4
4
  "description": "Shared Zod schemas, enums and domain types for GymMonk (fitness SaaS) — single source of truth (SSOT) consumed by gymmonk-backend and gymmonk-web-client.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",