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.
- package/dist/announcement.d.ts +29 -41
- package/dist/announcement.d.ts.map +1 -1
- package/dist/announcement.js +34 -64
- package/dist/announcement.js.map +1 -1
- package/package.json +1 -1
package/dist/announcement.d.ts
CHANGED
|
@@ -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
|
-
*
|
|
61
|
+
* Write one and it goes out, unless it is explicitly saved as a draft.
|
|
51
62
|
*
|
|
52
|
-
*
|
|
53
|
-
*
|
|
54
|
-
*
|
|
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.
|
|
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,
|
|
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
|
|
100
|
-
* reason this exists — before it there was no update endpoint at all,
|
|
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
|
-
*
|
|
126
|
-
*
|
|
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
|
|
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"}
|
package/dist/announcement.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
// ───
|
|
55
|
+
// ─── Create body ─────────────────────────────────────────────────────────────
|
|
50
56
|
/**
|
|
51
|
-
*
|
|
57
|
+
* Write one and it goes out, unless it is explicitly saved as a draft.
|
|
52
58
|
*
|
|
53
|
-
*
|
|
54
|
-
*
|
|
55
|
-
*
|
|
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
|
|
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.
|
|
70
|
+
export const createAnnouncementWithCenterSchema = createAnnouncementBodySchema.extend({
|
|
71
|
+
centerId: objectIdSchema,
|
|
72
|
+
});
|
|
96
73
|
// ─── Update body ─────────────────────────────────────────────────────────────
|
|
97
74
|
/**
|
|
98
|
-
* Edit a draft,
|
|
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
|
|
102
|
-
* reason this exists — before it there was no update endpoint at all,
|
|
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
|
-
*
|
|
121
|
-
*
|
|
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'
|
|
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
|
/**
|
package/dist/announcement.js.map
CHANGED
|
@@ -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.
|
|
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",
|