gymmonk-schema 0.3.0 → 0.4.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/file.d.ts ADDED
@@ -0,0 +1,116 @@
1
+ /**
2
+ * gymmonk-schema — File uploads
3
+ * =============================
4
+ * Contracts for the signed-URL upload flow. GymMonk never streams file bytes
5
+ * through the API: the browser asks the backend for a short-lived v4 signed
6
+ * PUT URL, uploads straight to Google Cloud Storage, and stores the returned
7
+ * PERMANENT public URL.
8
+ *
9
+ * The download URL is deliberately NOT signed. A signed read URL expires,
10
+ * which would rot every image reference already written to Mongo; the bucket
11
+ * instead grants `allUsers` object-viewer and we store the plain public URL.
12
+ *
13
+ * Mirrors reform-backend's `shared/schemas/file.schema.ts`.
14
+ *
15
+ * @module gymmonk-schema/file
16
+ */
17
+ import { z } from 'zod';
18
+ /**
19
+ * Every folder an upload may target. This is an allow-list, not a hint: the
20
+ * backend rejects anything outside it, so a compromised client cannot scatter
21
+ * objects across the bucket. Adding a new upload surface means adding it here
22
+ * FIRST, then publishing the package.
23
+ *
24
+ * `userId` is appended by the backend, so the effective path is
25
+ * `<folder>/<userId>/<timestamp>-<random><ext>`. That per-user prefix is what
26
+ * makes ownership checkable on cleanup.
27
+ */
28
+ export declare const UPLOAD_FOLDERS: readonly ["profile-photos", "center-photos", "logos", "member-documents", "equipment-photos", "announcement-media", "exercise-media", "progress-photos", "feedback-media"];
29
+ export declare const uploadFolderSchema: z.ZodEnum<{
30
+ "profile-photos": "profile-photos";
31
+ "center-photos": "center-photos";
32
+ logos: "logos";
33
+ "member-documents": "member-documents";
34
+ "equipment-photos": "equipment-photos";
35
+ "announcement-media": "announcement-media";
36
+ "exercise-media": "exercise-media";
37
+ "progress-photos": "progress-photos";
38
+ "feedback-media": "feedback-media";
39
+ }>;
40
+ export type UploadFolder = z.infer<typeof uploadFolderSchema>;
41
+ /** MIME types the signed-URL endpoint will sign for. */
42
+ export declare const ALLOWED_UPLOAD_MIME_TYPES: readonly ["image/jpeg", "image/png", "image/webp", "image/gif", "application/pdf"];
43
+ export declare const uploadMimeTypeSchema: z.ZodEnum<{
44
+ "image/jpeg": "image/jpeg";
45
+ "image/png": "image/png";
46
+ "image/webp": "image/webp";
47
+ "image/gif": "image/gif";
48
+ "application/pdf": "application/pdf";
49
+ }>;
50
+ export type UploadMimeType = z.infer<typeof uploadMimeTypeSchema>;
51
+ /**
52
+ * Default per-file ceiling, in MB. Validation runs on the RAW pick, before the
53
+ * client-side optimizer shrinks it — a modern phone camera produces 8-12 MB
54
+ * JPEGs that compress to well under 1 MB, so a tighter cap would reject
55
+ * perfectly ordinary photos.
56
+ */
57
+ export declare const MAX_UPLOAD_SIZE_MB = 10;
58
+ /** Per-folder overrides. Documents may be larger scans than a photo. */
59
+ export declare const FOLDER_MAX_SIZE_MB: Partial<Record<UploadFolder, number>>;
60
+ /**
61
+ * Request body for `POST /api/v1/files/signed-url`.
62
+ *
63
+ * `fileName` is restricted to a safe charset because it becomes part of the GCS
64
+ * object path — the client sanitises before sending, and this rejects anything
65
+ * that slipped through (path separators, control characters, unicode tricks).
66
+ */
67
+ export declare const signedUploadUrlBodySchema: z.ZodObject<{
68
+ fileName: z.ZodString;
69
+ contentType: z.ZodEnum<{
70
+ "image/jpeg": "image/jpeg";
71
+ "image/png": "image/png";
72
+ "image/webp": "image/webp";
73
+ "image/gif": "image/gif";
74
+ "application/pdf": "application/pdf";
75
+ }>;
76
+ folder: z.ZodEnum<{
77
+ "profile-photos": "profile-photos";
78
+ "center-photos": "center-photos";
79
+ logos: "logos";
80
+ "member-documents": "member-documents";
81
+ "equipment-photos": "equipment-photos";
82
+ "announcement-media": "announcement-media";
83
+ "exercise-media": "exercise-media";
84
+ "progress-photos": "progress-photos";
85
+ "feedback-media": "feedback-media";
86
+ }>;
87
+ fileSize: z.ZodNumber;
88
+ }, z.core.$strip>;
89
+ export type SignedUploadUrlBody = z.infer<typeof signedUploadUrlBodySchema>;
90
+ /** Response of `POST /api/v1/files/signed-url`. */
91
+ export declare const signedUploadUrlResponseSchema: z.ZodObject<{
92
+ uploadUrl: z.ZodString;
93
+ downloadUrl: z.ZodString;
94
+ filePath: z.ZodString;
95
+ expiresAt: z.ZodISODateTime;
96
+ }, z.core.$strip>;
97
+ export type SignedUploadUrlResponse = z.infer<typeof signedUploadUrlResponseSchema>;
98
+ /**
99
+ * Body for `POST /api/v1/files/cleanup-uploads`. Called when a user removes a
100
+ * photo they uploaded but never saved, so the orphaned object does not sit in
101
+ * the bucket forever.
102
+ *
103
+ * Ownership is enforced per-path from the `userId` segment the backend embedded
104
+ * at signing time. Paths belonging to someone else are skipped silently rather
105
+ * than 403-ing, so one foreign entry cannot void a legitimate batch.
106
+ */
107
+ export declare const cleanupUploadsBodySchema: z.ZodObject<{
108
+ filePaths: z.ZodArray<z.ZodString>;
109
+ }, z.core.$strip>;
110
+ export type CleanupUploadsBody = z.infer<typeof cleanupUploadsBodySchema>;
111
+ export declare const cleanupUploadsResponseSchema: z.ZodObject<{
112
+ deleted: z.ZodNumber;
113
+ skipped: z.ZodNumber;
114
+ }, z.core.$strip>;
115
+ export type CleanupUploadsResponse = z.infer<typeof cleanupUploadsResponseSchema>;
116
+ //# sourceMappingURL=file.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"file.d.ts","sourceRoot":"","sources":["../src/file.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAIxB;;;;;;;;;GASG;AACH,eAAO,MAAM,cAAc,4KAmBjB,CAAC;AAEX,eAAO,MAAM,kBAAkB;;;;;;;;;;EAAyB,CAAC;AACzD,MAAM,MAAM,YAAY,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,kBAAkB,CAAC,CAAC;AAI9D,wDAAwD;AACxD,eAAO,MAAM,yBAAyB,oFAM5B,CAAC;AAEX,eAAO,MAAM,oBAAoB;;;;;;EAAoC,CAAC;AACtE,MAAM,MAAM,cAAc,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,oBAAoB,CAAC,CAAC;AAIlE;;;;;GAKG;AACH,eAAO,MAAM,kBAAkB,KAAK,CAAC;AAErC,wEAAwE;AACxE,eAAO,MAAM,kBAAkB,EAAE,OAAO,CAAC,MAAM,CAAC,YAAY,EAAE,MAAM,CAAC,CAEpE,CAAC;AAMF;;;;;;GAMG;AACH,eAAO,MAAM,yBAAyB;;;;;;;;;;;;;;;;;;;;;iBA0BlC,CAAC;AAEL,MAAM,MAAM,mBAAmB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,yBAAyB,CAAC,CAAC;AAE5E,mDAAmD;AACnD,eAAO,MAAM,6BAA6B;;;;;iBAUxC,CAAC;AAEH,MAAM,MAAM,uBAAuB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,6BAA6B,CAAC,CAAC;AAIpF;;;;;;;;GAQG;AACH,eAAO,MAAM,wBAAwB;;iBAEnC,CAAC;AAEH,MAAM,MAAM,kBAAkB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,wBAAwB,CAAC,CAAC;AAE1E,eAAO,MAAM,4BAA4B;;;iBAGvC,CAAC;AAEH,MAAM,MAAM,sBAAsB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,4BAA4B,CAAC,CAAC"}
package/dist/file.js ADDED
@@ -0,0 +1,137 @@
1
+ /**
2
+ * gymmonk-schema — File uploads
3
+ * =============================
4
+ * Contracts for the signed-URL upload flow. GymMonk never streams file bytes
5
+ * through the API: the browser asks the backend for a short-lived v4 signed
6
+ * PUT URL, uploads straight to Google Cloud Storage, and stores the returned
7
+ * PERMANENT public URL.
8
+ *
9
+ * The download URL is deliberately NOT signed. A signed read URL expires,
10
+ * which would rot every image reference already written to Mongo; the bucket
11
+ * instead grants `allUsers` object-viewer and we store the plain public URL.
12
+ *
13
+ * Mirrors reform-backend's `shared/schemas/file.schema.ts`.
14
+ *
15
+ * @module gymmonk-schema/file
16
+ */
17
+ import { z } from 'zod';
18
+ // ─── Folders ─────────────────────────────────────────────────────────────────
19
+ /**
20
+ * Every folder an upload may target. This is an allow-list, not a hint: the
21
+ * backend rejects anything outside it, so a compromised client cannot scatter
22
+ * objects across the bucket. Adding a new upload surface means adding it here
23
+ * FIRST, then publishing the package.
24
+ *
25
+ * `userId` is appended by the backend, so the effective path is
26
+ * `<folder>/<userId>/<timestamp>-<random><ext>`. That per-user prefix is what
27
+ * makes ownership checkable on cleanup.
28
+ */
29
+ export const UPLOAD_FOLDERS = [
30
+ /** Member + owner + staff avatars. Square, cropped round in the UI. */
31
+ 'profile-photos',
32
+ /** Gym/center gallery photos. Multi-select, uncropped. */
33
+ 'center-photos',
34
+ /** Organisation and center logos. */
35
+ 'logos',
36
+ /** Member ID proof / address proof captured at the front desk. */
37
+ 'member-documents',
38
+ /** Equipment photos in the gym inventory. */
39
+ 'equipment-photos',
40
+ /** Images attached to an announcement. */
41
+ 'announcement-media',
42
+ /** Custom exercise thumbnails added by a gym. */
43
+ 'exercise-media',
44
+ /** Progress photos a member uploads against a measurement entry. */
45
+ 'progress-photos',
46
+ /** Screenshots and photos attached to feedback or a complaint. */
47
+ 'feedback-media',
48
+ ];
49
+ export const uploadFolderSchema = z.enum(UPLOAD_FOLDERS);
50
+ // ─── Content types ───────────────────────────────────────────────────────────
51
+ /** MIME types the signed-URL endpoint will sign for. */
52
+ export const ALLOWED_UPLOAD_MIME_TYPES = [
53
+ 'image/jpeg',
54
+ 'image/png',
55
+ 'image/webp',
56
+ 'image/gif',
57
+ 'application/pdf',
58
+ ];
59
+ export const uploadMimeTypeSchema = z.enum(ALLOWED_UPLOAD_MIME_TYPES);
60
+ // ─── Size limits ─────────────────────────────────────────────────────────────
61
+ /**
62
+ * Default per-file ceiling, in MB. Validation runs on the RAW pick, before the
63
+ * client-side optimizer shrinks it — a modern phone camera produces 8-12 MB
64
+ * JPEGs that compress to well under 1 MB, so a tighter cap would reject
65
+ * perfectly ordinary photos.
66
+ */
67
+ export const MAX_UPLOAD_SIZE_MB = 10;
68
+ /** Per-folder overrides. Documents may be larger scans than a photo. */
69
+ export const FOLDER_MAX_SIZE_MB = {
70
+ 'member-documents': 20,
71
+ };
72
+ const HARD_CAP_MB = Math.max(MAX_UPLOAD_SIZE_MB, ...Object.values(FOLDER_MAX_SIZE_MB));
73
+ // ─── Signed upload URL ───────────────────────────────────────────────────────
74
+ /**
75
+ * Request body for `POST /api/v1/files/signed-url`.
76
+ *
77
+ * `fileName` is restricted to a safe charset because it becomes part of the GCS
78
+ * object path — the client sanitises before sending, and this rejects anything
79
+ * that slipped through (path separators, control characters, unicode tricks).
80
+ */
81
+ export const signedUploadUrlBodySchema = z
82
+ .object({
83
+ fileName: z
84
+ .string()
85
+ .min(1)
86
+ .max(255)
87
+ .regex(/^[a-zA-Z0-9_.-]+$/, 'Invalid file name'),
88
+ contentType: uploadMimeTypeSchema,
89
+ folder: uploadFolderSchema,
90
+ fileSize: z
91
+ .number()
92
+ .int()
93
+ .min(1)
94
+ .max(HARD_CAP_MB * 1024 * 1024, `File must be smaller than ${HARD_CAP_MB}MB`),
95
+ })
96
+ .superRefine((data, ctx) => {
97
+ // Tighten to the per-folder cap. The outer `.max` is only the widest
98
+ // ceiling any folder allows; this is the one that actually applies.
99
+ const capMb = FOLDER_MAX_SIZE_MB[data.folder] ?? MAX_UPLOAD_SIZE_MB;
100
+ if (data.fileSize > capMb * 1024 * 1024) {
101
+ ctx.addIssue({
102
+ code: 'custom',
103
+ path: ['fileSize'],
104
+ message: `File must be smaller than ${capMb}MB for ${data.folder}`,
105
+ });
106
+ }
107
+ });
108
+ /** Response of `POST /api/v1/files/signed-url`. */
109
+ export const signedUploadUrlResponseSchema = z.object({
110
+ /** Short-lived v4 signed URL — PUT the bytes here. */
111
+ uploadUrl: z.string(),
112
+ /** Permanent public URL. This is what gets stored in Mongo. */
113
+ downloadUrl: z.string(),
114
+ /** Object path inside the bucket, without the bucket prefix. Used to
115
+ * delete an object the user abandoned before saving. */
116
+ filePath: z.string(),
117
+ /** When `uploadUrl` stops working. */
118
+ expiresAt: z.iso.datetime(),
119
+ });
120
+ // ─── Cleanup ─────────────────────────────────────────────────────────────────
121
+ /**
122
+ * Body for `POST /api/v1/files/cleanup-uploads`. Called when a user removes a
123
+ * photo they uploaded but never saved, so the orphaned object does not sit in
124
+ * the bucket forever.
125
+ *
126
+ * Ownership is enforced per-path from the `userId` segment the backend embedded
127
+ * at signing time. Paths belonging to someone else are skipped silently rather
128
+ * than 403-ing, so one foreign entry cannot void a legitimate batch.
129
+ */
130
+ export const cleanupUploadsBodySchema = z.object({
131
+ filePaths: z.array(z.string().min(1)).min(1).max(20),
132
+ });
133
+ export const cleanupUploadsResponseSchema = z.object({
134
+ deleted: z.number().int().min(0),
135
+ skipped: z.number().int().min(0),
136
+ });
137
+ //# sourceMappingURL=file.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"file.js","sourceRoot":"","sources":["../src/file.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * gymmonk-schema — File uploads\n * =============================\n * Contracts for the signed-URL upload flow. GymMonk never streams file bytes\n * through the API: the browser asks the backend for a short-lived v4 signed\n * PUT URL, uploads straight to Google Cloud Storage, and stores the returned\n * PERMANENT public URL.\n *\n * The download URL is deliberately NOT signed. A signed read URL expires,\n * which would rot every image reference already written to Mongo; the bucket\n * instead grants `allUsers` object-viewer and we store the plain public URL.\n *\n * Mirrors reform-backend's `shared/schemas/file.schema.ts`.\n *\n * @module gymmonk-schema/file\n */\n\nimport { z } from 'zod';\n\n// ─── Folders ─────────────────────────────────────────────────────────────────\n\n/**\n * Every folder an upload may target. This is an allow-list, not a hint: the\n * backend rejects anything outside it, so a compromised client cannot scatter\n * objects across the bucket. Adding a new upload surface means adding it here\n * FIRST, then publishing the package.\n *\n * `userId` is appended by the backend, so the effective path is\n * `<folder>/<userId>/<timestamp>-<random><ext>`. That per-user prefix is what\n * makes ownership checkable on cleanup.\n */\nexport const UPLOAD_FOLDERS = [\n /** Member + owner + staff avatars. Square, cropped round in the UI. */\n 'profile-photos',\n /** Gym/center gallery photos. Multi-select, uncropped. */\n 'center-photos',\n /** Organisation and center logos. */\n 'logos',\n /** Member ID proof / address proof captured at the front desk. */\n 'member-documents',\n /** Equipment photos in the gym inventory. */\n 'equipment-photos',\n /** Images attached to an announcement. */\n 'announcement-media',\n /** Custom exercise thumbnails added by a gym. */\n 'exercise-media',\n /** Progress photos a member uploads against a measurement entry. */\n 'progress-photos',\n /** Screenshots and photos attached to feedback or a complaint. */\n 'feedback-media',\n] as const;\n\nexport const uploadFolderSchema = z.enum(UPLOAD_FOLDERS);\nexport type UploadFolder = z.infer<typeof uploadFolderSchema>;\n\n// ─── Content types ───────────────────────────────────────────────────────────\n\n/** MIME types the signed-URL endpoint will sign for. */\nexport const ALLOWED_UPLOAD_MIME_TYPES = [\n 'image/jpeg',\n 'image/png',\n 'image/webp',\n 'image/gif',\n 'application/pdf',\n] as const;\n\nexport const uploadMimeTypeSchema = z.enum(ALLOWED_UPLOAD_MIME_TYPES);\nexport type UploadMimeType = z.infer<typeof uploadMimeTypeSchema>;\n\n// ─── Size limits ─────────────────────────────────────────────────────────────\n\n/**\n * Default per-file ceiling, in MB. Validation runs on the RAW pick, before the\n * client-side optimizer shrinks it — a modern phone camera produces 8-12 MB\n * JPEGs that compress to well under 1 MB, so a tighter cap would reject\n * perfectly ordinary photos.\n */\nexport const MAX_UPLOAD_SIZE_MB = 10;\n\n/** Per-folder overrides. Documents may be larger scans than a photo. */\nexport const FOLDER_MAX_SIZE_MB: Partial<Record<UploadFolder, number>> = {\n 'member-documents': 20,\n};\n\nconst HARD_CAP_MB = Math.max(MAX_UPLOAD_SIZE_MB, ...Object.values(FOLDER_MAX_SIZE_MB));\n\n// ─── Signed upload URL ───────────────────────────────────────────────────────\n\n/**\n * Request body for `POST /api/v1/files/signed-url`.\n *\n * `fileName` is restricted to a safe charset because it becomes part of the GCS\n * object path — the client sanitises before sending, and this rejects anything\n * that slipped through (path separators, control characters, unicode tricks).\n */\nexport const signedUploadUrlBodySchema = z\n .object({\n fileName: z\n .string()\n .min(1)\n .max(255)\n .regex(/^[a-zA-Z0-9_.-]+$/, 'Invalid file name'),\n contentType: uploadMimeTypeSchema,\n folder: uploadFolderSchema,\n fileSize: z\n .number()\n .int()\n .min(1)\n .max(HARD_CAP_MB * 1024 * 1024, `File must be smaller than ${HARD_CAP_MB}MB`),\n })\n .superRefine((data, ctx) => {\n // Tighten to the per-folder cap. The outer `.max` is only the widest\n // ceiling any folder allows; this is the one that actually applies.\n const capMb = FOLDER_MAX_SIZE_MB[data.folder] ?? MAX_UPLOAD_SIZE_MB;\n if (data.fileSize > capMb * 1024 * 1024) {\n ctx.addIssue({\n code: 'custom',\n path: ['fileSize'],\n message: `File must be smaller than ${capMb}MB for ${data.folder}`,\n });\n }\n });\n\nexport type SignedUploadUrlBody = z.infer<typeof signedUploadUrlBodySchema>;\n\n/** Response of `POST /api/v1/files/signed-url`. */\nexport const signedUploadUrlResponseSchema = z.object({\n /** Short-lived v4 signed URL — PUT the bytes here. */\n uploadUrl: z.string(),\n /** Permanent public URL. This is what gets stored in Mongo. */\n downloadUrl: z.string(),\n /** Object path inside the bucket, without the bucket prefix. Used to\n * delete an object the user abandoned before saving. */\n filePath: z.string(),\n /** When `uploadUrl` stops working. */\n expiresAt: z.iso.datetime(),\n});\n\nexport type SignedUploadUrlResponse = z.infer<typeof signedUploadUrlResponseSchema>;\n\n// ─── Cleanup ─────────────────────────────────────────────────────────────────\n\n/**\n * Body for `POST /api/v1/files/cleanup-uploads`. Called when a user removes a\n * photo they uploaded but never saved, so the orphaned object does not sit in\n * the bucket forever.\n *\n * Ownership is enforced per-path from the `userId` segment the backend embedded\n * at signing time. Paths belonging to someone else are skipped silently rather\n * than 403-ing, so one foreign entry cannot void a legitimate batch.\n */\nexport const cleanupUploadsBodySchema = z.object({\n filePaths: z.array(z.string().min(1)).min(1).max(20),\n});\n\nexport type CleanupUploadsBody = z.infer<typeof cleanupUploadsBodySchema>;\n\nexport const cleanupUploadsResponseSchema = z.object({\n deleted: z.number().int().min(0),\n skipped: z.number().int().min(0),\n});\n\nexport type CleanupUploadsResponse = z.infer<typeof cleanupUploadsResponseSchema>;\n"]}
package/dist/index.d.ts CHANGED
@@ -14,6 +14,7 @@ export * from './common.js';
14
14
  export * from './equipment.js';
15
15
  export * from './exercise.js';
16
16
  export * from './feedback.js';
17
+ export * from './file.js';
17
18
  export * from './home.js';
18
19
  export * from './insights.js';
19
20
  export * from './member.js';
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,cAAc,mBAAmB,CAAC;AAClC,cAAc,aAAa,CAAC;AAE5B,cAAc,aAAa,CAAC;AAC5B,cAAc,gBAAgB,CAAC;AAE/B,cAAc,eAAe,CAAC;AAC9B,cAAc,eAAe,CAAC;AAC9B,cAAc,WAAW,CAAC;AAC1B,cAAc,eAAe,CAAC;AAE9B,cAAc,aAAa,CAAC;AAC5B,cAAc,qBAAqB,CAAC;AACpC,cAAc,iBAAiB,CAAC;AAEhC,cAAc,sBAAsB,CAAC;AAErC,cAAc,mBAAmB,CAAC;AAClC,cAAc,iBAAiB,CAAC;AAEhC,cAAc,mBAAmB,CAAC;AAClC,cAAc,oBAAoB,CAAC;AACnC,cAAc,cAAc,CAAC;AAC7B,cAAc,cAAc,CAAC;AAC7B,cAAc,aAAa,CAAC;AAC5B,cAAc,YAAY,CAAC;AAC3B,cAAc,mBAAmB,CAAC;AAClC,cAAc,WAAW,CAAC;AAC1B,cAAc,mBAAmB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,cAAc,mBAAmB,CAAC;AAClC,cAAc,aAAa,CAAC;AAE5B,cAAc,aAAa,CAAC;AAC5B,cAAc,gBAAgB,CAAC;AAE/B,cAAc,eAAe,CAAC;AAC9B,cAAc,eAAe,CAAC;AAE9B,cAAc,WAAW,CAAC;AAC1B,cAAc,WAAW,CAAC;AAC1B,cAAc,eAAe,CAAC;AAE9B,cAAc,aAAa,CAAC;AAC5B,cAAc,qBAAqB,CAAC;AACpC,cAAc,iBAAiB,CAAC;AAEhC,cAAc,sBAAsB,CAAC;AAErC,cAAc,mBAAmB,CAAC;AAClC,cAAc,iBAAiB,CAAC;AAEhC,cAAc,mBAAmB,CAAC;AAClC,cAAc,oBAAoB,CAAC;AACnC,cAAc,cAAc,CAAC;AAC7B,cAAc,cAAc,CAAC;AAC7B,cAAc,aAAa,CAAC;AAC5B,cAAc,YAAY,CAAC;AAC3B,cAAc,mBAAmB,CAAC;AAClC,cAAc,WAAW,CAAC;AAC1B,cAAc,mBAAmB,CAAC"}
package/dist/index.js CHANGED
@@ -16,6 +16,8 @@ export * from './equipment.js';
16
16
  // ─── Training ────────────────────────────────────────────────────────────────
17
17
  export * from './exercise.js';
18
18
  export * from './feedback.js';
19
+ // ─── Uploads ─────────────────────────────────────────────────────────────────
20
+ export * from './file.js';
19
21
  export * from './home.js';
20
22
  export * from './insights.js';
21
23
  // ─── People ──────────────────────────────────────────────────────────────────
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,cAAc,mBAAmB,CAAC;AAClC,cAAc,aAAa,CAAC;AAC5B,gFAAgF;AAChF,cAAc,aAAa,CAAC;AAC5B,cAAc,gBAAgB,CAAC;AAC/B,gFAAgF;AAChF,cAAc,eAAe,CAAC;AAC9B,cAAc,eAAe,CAAC;AAC9B,cAAc,WAAW,CAAC;AAC1B,cAAc,eAAe,CAAC;AAC9B,gFAAgF;AAChF,cAAc,aAAa,CAAC;AAC5B,cAAc,qBAAqB,CAAC;AACpC,cAAc,iBAAiB,CAAC;AAChC,gFAAgF;AAChF,cAAc,sBAAsB,CAAC;AACrC,gFAAgF;AAChF,cAAc,mBAAmB,CAAC;AAClC,cAAc,iBAAiB,CAAC;AAChC,gFAAgF;AAChF,cAAc,mBAAmB,CAAC;AAClC,cAAc,oBAAoB,CAAC;AACnC,cAAc,cAAc,CAAC;AAC7B,cAAc,cAAc,CAAC;AAC7B,cAAc,aAAa,CAAC;AAC5B,cAAc,YAAY,CAAC;AAC3B,cAAc,mBAAmB,CAAC;AAClC,cAAc,WAAW,CAAC;AAC1B,cAAc,mBAAmB,CAAC","sourcesContent":["/**\n * gymmonk-schema — Public API\n * ===========================\n * Shared Zod schemas, enums and domain types for GymMonk (fitness SaaS).\n * Single source of truth (SSOT) for data shapes, TypeScript types and\n * validation, consumed by both gymmonk-backend and gymmonk-web-client.\n *\n * Every domain module is re-exported through this barrel. Import names are\n * globally unique across modules, so `export *` composes without collisions.\n */\n\nexport * from './announcement.js';\nexport * from './center.js';\n// ─── Foundation ──────────────────────────────────────────────────────────────\nexport * from './common.js';\nexport * from './equipment.js';\n// ─── Training ────────────────────────────────────────────────────────────────\nexport * from './exercise.js';\nexport * from './feedback.js';\nexport * from './home.js';\nexport * from './insights.js';\n// ─── People ──────────────────────────────────────────────────────────────────\nexport * from './member.js';\nexport * from './member-profile.js';\nexport * from './membership.js';\n// ─── Membership ──────────────────────────────────────────────────────────────\nexport * from './membership-plan.js';\n// ─── Engagement & analytics ──────────────────────────────────────────────────\nexport * from './notification.js';\nexport * from './onboarding.js';\n// ─── Gym structure & onboarding ──────────────────────────────────────────────\nexport * from './organisation.js';\nexport * from './owner-profile.js';\nexport * from './pricing.js';\nexport * from './session.js';\nexport * from './shared.js';\nexport * from './staff.js';\nexport * from './subscription.js';\nexport * from './user.js';\nexport * from './workout-plan.js';\n"]}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,cAAc,mBAAmB,CAAC;AAClC,cAAc,aAAa,CAAC;AAC5B,gFAAgF;AAChF,cAAc,aAAa,CAAC;AAC5B,cAAc,gBAAgB,CAAC;AAC/B,gFAAgF;AAChF,cAAc,eAAe,CAAC;AAC9B,cAAc,eAAe,CAAC;AAC9B,gFAAgF;AAChF,cAAc,WAAW,CAAC;AAC1B,cAAc,WAAW,CAAC;AAC1B,cAAc,eAAe,CAAC;AAC9B,gFAAgF;AAChF,cAAc,aAAa,CAAC;AAC5B,cAAc,qBAAqB,CAAC;AACpC,cAAc,iBAAiB,CAAC;AAChC,gFAAgF;AAChF,cAAc,sBAAsB,CAAC;AACrC,gFAAgF;AAChF,cAAc,mBAAmB,CAAC;AAClC,cAAc,iBAAiB,CAAC;AAChC,gFAAgF;AAChF,cAAc,mBAAmB,CAAC;AAClC,cAAc,oBAAoB,CAAC;AACnC,cAAc,cAAc,CAAC;AAC7B,cAAc,cAAc,CAAC;AAC7B,cAAc,aAAa,CAAC;AAC5B,cAAc,YAAY,CAAC;AAC3B,cAAc,mBAAmB,CAAC;AAClC,cAAc,WAAW,CAAC;AAC1B,cAAc,mBAAmB,CAAC","sourcesContent":["/**\n * gymmonk-schema — Public API\n * ===========================\n * Shared Zod schemas, enums and domain types for GymMonk (fitness SaaS).\n * Single source of truth (SSOT) for data shapes, TypeScript types and\n * validation, consumed by both gymmonk-backend and gymmonk-web-client.\n *\n * Every domain module is re-exported through this barrel. Import names are\n * globally unique across modules, so `export *` composes without collisions.\n */\n\nexport * from './announcement.js';\nexport * from './center.js';\n// ─── Foundation ──────────────────────────────────────────────────────────────\nexport * from './common.js';\nexport * from './equipment.js';\n// ─── Training ────────────────────────────────────────────────────────────────\nexport * from './exercise.js';\nexport * from './feedback.js';\n// ─── Uploads ─────────────────────────────────────────────────────────────────\nexport * from './file.js';\nexport * from './home.js';\nexport * from './insights.js';\n// ─── People ──────────────────────────────────────────────────────────────────\nexport * from './member.js';\nexport * from './member-profile.js';\nexport * from './membership.js';\n// ─── Membership ──────────────────────────────────────────────────────────────\nexport * from './membership-plan.js';\n// ─── Engagement & analytics ──────────────────────────────────────────────────\nexport * from './notification.js';\nexport * from './onboarding.js';\n// ─── Gym structure & onboarding ──────────────────────────────────────────────\nexport * from './organisation.js';\nexport * from './owner-profile.js';\nexport * from './pricing.js';\nexport * from './session.js';\nexport * from './shared.js';\nexport * from './staff.js';\nexport * from './subscription.js';\nexport * from './user.js';\nexport * from './workout-plan.js';\n"]}
@@ -54,6 +54,7 @@ export declare const memberOnboardingBodySchema: z.ZodObject<{
54
54
  "AB+": "AB+";
55
55
  "AB-": "AB-";
56
56
  }>;
57
+ avatarUrl: z.ZodPreprocess<z.ZodOptional<z.ZodString>>;
57
58
  }, z.core.$strip>;
58
59
  export type MemberOnboardingBody = z.infer<typeof memberOnboardingBodySchema>;
59
60
  /** The owner's personal basics captured before the organisation/center. */
@@ -358,4 +359,64 @@ export declare const ownerOnboardingBodySchema: z.ZodObject<{
358
359
  }, z.core.$strip>;
359
360
  }, z.core.$strip>;
360
361
  export type OwnerOnboardingBody = z.infer<typeof ownerOnboardingBodySchema>;
362
+ /**
363
+ * In-progress onboarding draft: the UI-shaped, partially-filled form the client
364
+ * persists so a returning user resumes with their typed data intact. Opaque to
365
+ * the backend by design — the client owns the form shape, and the data is
366
+ * re-validated strictly by the member/owner body schemas at finalize. Keys are
367
+ * form field names; the backend caps the stored size.
368
+ */
369
+ export declare const onboardingDraftSchema: z.ZodRecord<z.ZodString, z.ZodUnknown>;
370
+ export type OnboardingDraft = z.infer<typeof onboardingDraftSchema>;
371
+ /**
372
+ * The server-persisted onboarding cursor: which role the user picked, how far
373
+ * they reached (`step`, the 1-indexed sub-step of the current form), the gym a
374
+ * member chose, and the drafts. Read on app entry to resume to the exact step.
375
+ * `welcomed` records that the one-time welcome carousel has been seen.
376
+ */
377
+ export declare const onboardingProgressSchema: z.ZodObject<{
378
+ welcomed: z.ZodBoolean;
379
+ role: z.ZodNullable<z.ZodEnum<{
380
+ owner: "owner";
381
+ member: "member";
382
+ }>>;
383
+ step: z.ZodNumber;
384
+ gymCode: z.ZodOptional<z.ZodString>;
385
+ memberDraft: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
386
+ ownerDraft: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
387
+ }, z.core.$strip>;
388
+ export type OnboardingProgress = z.infer<typeof onboardingProgressSchema>;
389
+ /** `PUT /onboarding/progress` body — any subset of the cursor, merged server-side. */
390
+ export declare const onboardingProgressUpdateSchema: z.ZodObject<{
391
+ welcomed: z.ZodOptional<z.ZodBoolean>;
392
+ role: z.ZodOptional<z.ZodNullable<z.ZodEnum<{
393
+ owner: "owner";
394
+ member: "member";
395
+ }>>>;
396
+ step: z.ZodOptional<z.ZodNumber>;
397
+ gymCode: z.ZodOptional<z.ZodString>;
398
+ memberDraft: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
399
+ ownerDraft: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
400
+ }, z.core.$strip>;
401
+ export type OnboardingProgressUpdate = z.infer<typeof onboardingProgressUpdateSchema>;
402
+ /**
403
+ * `GET /onboarding/status` response: completion truth (derived from the user's
404
+ * domain data) plus the resume cursor. `onboarded` is authoritative — an owner
405
+ * with an organisation, or a member with a profile, is onboarded regardless of
406
+ * the cursor.
407
+ */
408
+ export declare const onboardingStatusSchema: z.ZodObject<{
409
+ onboarded: z.ZodBoolean;
410
+ role: z.ZodNullable<z.ZodEnum<{
411
+ owner: "owner";
412
+ member: "member";
413
+ }>>;
414
+ organisationId: z.ZodOptional<z.ZodString>;
415
+ welcomed: z.ZodBoolean;
416
+ step: z.ZodNumber;
417
+ gymCode: z.ZodOptional<z.ZodString>;
418
+ memberDraft: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
419
+ ownerDraft: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
420
+ }, z.core.$strip>;
421
+ export type OnboardingStatus = z.infer<typeof onboardingStatusSchema>;
361
422
  //# sourceMappingURL=onboarding.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"onboarding.d.ts","sourceRoot":"","sources":["../src/onboarding.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAcxB,eAAO,MAAM,oBAAoB;;;EAA8B,CAAC;AAChE,MAAM,MAAM,cAAc,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,oBAAoB,CAAC,CAAC;AAIlE,eAAO,MAAM,0BAA0B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBAwBnC,CAAC;AACL,MAAM,MAAM,oBAAoB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,0BAA0B,CAAC,CAAC;AAI9E,2EAA2E;AAC3E,eAAO,MAAM,iBAAiB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBAa5B,CAAC;AACH,MAAM,MAAM,WAAW,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,iBAAiB,CAAC,CAAC;AAE5D,mEAAmE;AACnE,eAAO,MAAM,6BAA6B;;;iBAGxC,CAAC;AACH,MAAM,MAAM,uBAAuB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,6BAA6B,CAAC,CAAC;AAEpF,eAAO,MAAM,yBAAyB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBAKpC,CAAC;AACH,MAAM,MAAM,mBAAmB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,yBAAyB,CAAC,CAAC"}
1
+ {"version":3,"file":"onboarding.d.ts","sourceRoot":"","sources":["../src/onboarding.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAcxB,eAAO,MAAM,oBAAoB;;;EAA8B,CAAC;AAChE,MAAM,MAAM,cAAc,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,oBAAoB,CAAC,CAAC;AAIlE,eAAO,MAAM,0BAA0B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBA2BnC,CAAC;AACL,MAAM,MAAM,oBAAoB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,0BAA0B,CAAC,CAAC;AAI9E,2EAA2E;AAC3E,eAAO,MAAM,iBAAiB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBAa5B,CAAC;AACH,MAAM,MAAM,WAAW,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,iBAAiB,CAAC,CAAC;AAE5D,mEAAmE;AACnE,eAAO,MAAM,6BAA6B;;;iBAGxC,CAAC;AACH,MAAM,MAAM,uBAAuB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,6BAA6B,CAAC,CAAC;AAEpF,eAAO,MAAM,yBAAyB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBAKpC,CAAC;AACH,MAAM,MAAM,mBAAmB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,yBAAyB,CAAC,CAAC;AAI5E;;;;;;GAMG;AACH,eAAO,MAAM,qBAAqB,wCAAoC,CAAC;AACvE,MAAM,MAAM,eAAe,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,qBAAqB,CAAC,CAAC;AAEpE;;;;;GAKG;AACH,eAAO,MAAM,wBAAwB;;;;;;;;;;iBAOnC,CAAC;AACH,MAAM,MAAM,kBAAkB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,wBAAwB,CAAC,CAAC;AAE1E,sFAAsF;AACtF,eAAO,MAAM,8BAA8B;;;;;;;;;;iBAS/B,CAAC;AACb,MAAM,MAAM,wBAAwB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,8BAA8B,CAAC,CAAC;AAEtF;;;;;GAKG;AACH,eAAO,MAAM,sBAAsB;;;;;;;;;;;;iBASjC,CAAC;AACH,MAAM,MAAM,gBAAgB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,sBAAsB,CAAC,CAAC"}
@@ -31,6 +31,9 @@ export const memberOnboardingBodySchema = z
31
31
  body: bodyMetricsSchema,
32
32
  emergencyContact: emergencyContactSchema,
33
33
  bloodGroup: bloodGroupSchema,
34
+ /** Uploaded profile photo → stored on the shared `User.avatar`.
35
+ * Mirrors `ownerBasicsSchema.avatarUrl`. */
36
+ avatarUrl: optionalTrimmedString(2000),
34
37
  })
35
38
  .refine((v) => v.body.heightCm != null && v.body.weightKg != null, {
36
39
  message: 'Height and weight are required',
@@ -71,4 +74,54 @@ export const ownerOnboardingBodySchema = z.object({
71
74
  center: createCenterBodySchema,
72
75
  subscription: onboardingPlanSelectionSchema,
73
76
  });
77
+ // ─── Onboarding progress & resume ─────────────────────────────────────────────
78
+ /**
79
+ * In-progress onboarding draft: the UI-shaped, partially-filled form the client
80
+ * persists so a returning user resumes with their typed data intact. Opaque to
81
+ * the backend by design — the client owns the form shape, and the data is
82
+ * re-validated strictly by the member/owner body schemas at finalize. Keys are
83
+ * form field names; the backend caps the stored size.
84
+ */
85
+ export const onboardingDraftSchema = z.record(z.string(), z.unknown());
86
+ /**
87
+ * The server-persisted onboarding cursor: which role the user picked, how far
88
+ * they reached (`step`, the 1-indexed sub-step of the current form), the gym a
89
+ * member chose, and the drafts. Read on app entry to resume to the exact step.
90
+ * `welcomed` records that the one-time welcome carousel has been seen.
91
+ */
92
+ export const onboardingProgressSchema = z.object({
93
+ welcomed: z.boolean(),
94
+ role: onboardingRoleSchema.nullable(),
95
+ step: z.number().int().min(0).max(20),
96
+ gymCode: gymCodeSchema.optional(),
97
+ memberDraft: onboardingDraftSchema.optional(),
98
+ ownerDraft: onboardingDraftSchema.optional(),
99
+ });
100
+ /** `PUT /onboarding/progress` body — any subset of the cursor, merged server-side. */
101
+ export const onboardingProgressUpdateSchema = z
102
+ .object({
103
+ welcomed: z.boolean(),
104
+ role: onboardingRoleSchema.nullable(),
105
+ step: z.number().int().min(0).max(20),
106
+ gymCode: gymCodeSchema,
107
+ memberDraft: onboardingDraftSchema,
108
+ ownerDraft: onboardingDraftSchema,
109
+ })
110
+ .partial();
111
+ /**
112
+ * `GET /onboarding/status` response: completion truth (derived from the user's
113
+ * domain data) plus the resume cursor. `onboarded` is authoritative — an owner
114
+ * with an organisation, or a member with a profile, is onboarded regardless of
115
+ * the cursor.
116
+ */
117
+ export const onboardingStatusSchema = z.object({
118
+ onboarded: z.boolean(),
119
+ role: onboardingRoleSchema.nullable(),
120
+ organisationId: z.string().optional(),
121
+ welcomed: z.boolean(),
122
+ step: z.number().int().min(0),
123
+ gymCode: z.string().optional(),
124
+ memberDraft: onboardingDraftSchema.optional(),
125
+ ownerDraft: onboardingDraftSchema.optional(),
126
+ });
74
127
  //# sourceMappingURL=onboarding.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"onboarding.js","sourceRoot":"","sources":["../src/onboarding.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * gymmonk-schema — Onboarding submit payloads\n * ============================================\n * The end-of-flow POST bodies. The frontend forms stay flat (three-part DOB,\n * etc.); at submit they map to these structured payloads, which compose the\n * canonical create schemas so the backend can fan out to the right collections.\n *\n * member → member-profile + a pending membership (join a gym by code)\n * owner → organisation + first center + SaaS subscription selection\n *\n * @module gymmonk-schema/onboarding\n */\n\nimport { z } from 'zod';\nimport { createCenterBodySchema, gymCodeSchema } from './center.js';\nimport {\n isoDateSchema,\n optionalEmailSchema,\n optionalTrimmedString,\n phoneSchema,\n} from './common.js';\nimport { bodyMetricsSchema, emergencyContactSchema } from './member-profile.js';\nimport { createOrganisationBodySchema } from './organisation.js';\nimport { addressSchema, bloodGroupSchema, genderSchema, socialLinksSchema } from './shared.js';\n\n// ─── Role ────────────────────────────────────────────────────────────────────\n\nexport const onboardingRoleSchema = z.enum(['owner', 'member']);\nexport type OnboardingRole = z.infer<typeof onboardingRoleSchema>;\n\n// ─── Member onboarding ───────────────────────────────────────────────────────\n\nexport const memberOnboardingBodySchema = z\n .object({\n /** The gym code the member picked at the \"find your gym\" step. */\n gymCode: gymCodeSchema,\n firstName: z.string().trim().min(1, 'First name is required').max(80),\n lastName: optionalTrimmedString(80),\n gender: genderSchema,\n dob: isoDateSchema,\n phone: phoneSchema,\n body: bodyMetricsSchema,\n emergencyContact: emergencyContactSchema,\n bloodGroup: bloodGroupSchema,\n })\n .refine((v) => v.body.heightCm != null && v.body.weightKg != null, {\n message: 'Height and weight are required',\n path: ['body'],\n })\n .refine((v) => v.body.neckCm != null && v.body.waistCm != null, {\n message: 'Neck and waist are required',\n path: ['body'],\n })\n .refine((v) => v.gender !== 'female' || v.body.hipsCm != null, {\n message: 'Hips measurement is required',\n path: ['body', 'hipsCm'],\n });\nexport type MemberOnboardingBody = z.infer<typeof memberOnboardingBodySchema>;\n\n// ─── Owner onboarding ────────────────────────────────────────────────────────\n\n/** The owner's personal basics captured before the organisation/center. */\nexport const ownerBasicsSchema = z.object({\n firstName: z.string().trim().min(1, 'First name is required').max(80),\n lastName: optionalTrimmedString(80),\n gender: genderSchema,\n dob: isoDateSchema.optional(),\n phone: phoneSchema,\n altEmail: optionalEmailSchema,\n website: optionalTrimmedString(200),\n socials: socialLinksSchema,\n /** The owner's personal contact address (step-2 of owner onboarding). */\n address: addressSchema.optional(),\n /** Uploaded profile photo → stored on the shared `User.avatar`. */\n avatarUrl: optionalTrimmedString(2000),\n});\nexport type OwnerBasics = z.infer<typeof ownerBasicsSchema>;\n\n/** The SaaS plan an owner selects at the final onboarding step. */\nexport const onboardingPlanSelectionSchema = z.object({\n planId: z.string().trim().min(1),\n tenureId: z.string().trim().min(1).nullable(),\n});\nexport type OnboardingPlanSelection = z.infer<typeof onboardingPlanSelectionSchema>;\n\nexport const ownerOnboardingBodySchema = z.object({\n owner: ownerBasicsSchema,\n organisation: createOrganisationBodySchema,\n center: createCenterBodySchema,\n subscription: onboardingPlanSelectionSchema,\n});\nexport type OwnerOnboardingBody = z.infer<typeof ownerOnboardingBodySchema>;\n"]}
1
+ {"version":3,"file":"onboarding.js","sourceRoot":"","sources":["../src/onboarding.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * gymmonk-schema — Onboarding submit payloads\n * ============================================\n * The end-of-flow POST bodies. The frontend forms stay flat (three-part DOB,\n * etc.); at submit they map to these structured payloads, which compose the\n * canonical create schemas so the backend can fan out to the right collections.\n *\n * member → member-profile + a pending membership (join a gym by code)\n * owner → organisation + first center + SaaS subscription selection\n *\n * @module gymmonk-schema/onboarding\n */\n\nimport { z } from 'zod';\nimport { createCenterBodySchema, gymCodeSchema } from './center.js';\nimport {\n isoDateSchema,\n optionalEmailSchema,\n optionalTrimmedString,\n phoneSchema,\n} from './common.js';\nimport { bodyMetricsSchema, emergencyContactSchema } from './member-profile.js';\nimport { createOrganisationBodySchema } from './organisation.js';\nimport { addressSchema, bloodGroupSchema, genderSchema, socialLinksSchema } from './shared.js';\n\n// ─── Role ────────────────────────────────────────────────────────────────────\n\nexport const onboardingRoleSchema = z.enum(['owner', 'member']);\nexport type OnboardingRole = z.infer<typeof onboardingRoleSchema>;\n\n// ─── Member onboarding ───────────────────────────────────────────────────────\n\nexport const memberOnboardingBodySchema = z\n .object({\n /** The gym code the member picked at the \"find your gym\" step. */\n gymCode: gymCodeSchema,\n firstName: z.string().trim().min(1, 'First name is required').max(80),\n lastName: optionalTrimmedString(80),\n gender: genderSchema,\n dob: isoDateSchema,\n phone: phoneSchema,\n body: bodyMetricsSchema,\n emergencyContact: emergencyContactSchema,\n bloodGroup: bloodGroupSchema,\n /** Uploaded profile photo → stored on the shared `User.avatar`.\n * Mirrors `ownerBasicsSchema.avatarUrl`. */\n avatarUrl: optionalTrimmedString(2000),\n })\n .refine((v) => v.body.heightCm != null && v.body.weightKg != null, {\n message: 'Height and weight are required',\n path: ['body'],\n })\n .refine((v) => v.body.neckCm != null && v.body.waistCm != null, {\n message: 'Neck and waist are required',\n path: ['body'],\n })\n .refine((v) => v.gender !== 'female' || v.body.hipsCm != null, {\n message: 'Hips measurement is required',\n path: ['body', 'hipsCm'],\n });\nexport type MemberOnboardingBody = z.infer<typeof memberOnboardingBodySchema>;\n\n// ─── Owner onboarding ────────────────────────────────────────────────────────\n\n/** The owner's personal basics captured before the organisation/center. */\nexport const ownerBasicsSchema = z.object({\n firstName: z.string().trim().min(1, 'First name is required').max(80),\n lastName: optionalTrimmedString(80),\n gender: genderSchema,\n dob: isoDateSchema.optional(),\n phone: phoneSchema,\n altEmail: optionalEmailSchema,\n website: optionalTrimmedString(200),\n socials: socialLinksSchema,\n /** The owner's personal contact address (step-2 of owner onboarding). */\n address: addressSchema.optional(),\n /** Uploaded profile photo → stored on the shared `User.avatar`. */\n avatarUrl: optionalTrimmedString(2000),\n});\nexport type OwnerBasics = z.infer<typeof ownerBasicsSchema>;\n\n/** The SaaS plan an owner selects at the final onboarding step. */\nexport const onboardingPlanSelectionSchema = z.object({\n planId: z.string().trim().min(1),\n tenureId: z.string().trim().min(1).nullable(),\n});\nexport type OnboardingPlanSelection = z.infer<typeof onboardingPlanSelectionSchema>;\n\nexport const ownerOnboardingBodySchema = z.object({\n owner: ownerBasicsSchema,\n organisation: createOrganisationBodySchema,\n center: createCenterBodySchema,\n subscription: onboardingPlanSelectionSchema,\n});\nexport type OwnerOnboardingBody = z.infer<typeof ownerOnboardingBodySchema>;\n\n// ─── Onboarding progress & resume ─────────────────────────────────────────────\n\n/**\n * In-progress onboarding draft: the UI-shaped, partially-filled form the client\n * persists so a returning user resumes with their typed data intact. Opaque to\n * the backend by design — the client owns the form shape, and the data is\n * re-validated strictly by the member/owner body schemas at finalize. Keys are\n * form field names; the backend caps the stored size.\n */\nexport const onboardingDraftSchema = z.record(z.string(), z.unknown());\nexport type OnboardingDraft = z.infer<typeof onboardingDraftSchema>;\n\n/**\n * The server-persisted onboarding cursor: which role the user picked, how far\n * they reached (`step`, the 1-indexed sub-step of the current form), the gym a\n * member chose, and the drafts. Read on app entry to resume to the exact step.\n * `welcomed` records that the one-time welcome carousel has been seen.\n */\nexport const onboardingProgressSchema = z.object({\n welcomed: z.boolean(),\n role: onboardingRoleSchema.nullable(),\n step: z.number().int().min(0).max(20),\n gymCode: gymCodeSchema.optional(),\n memberDraft: onboardingDraftSchema.optional(),\n ownerDraft: onboardingDraftSchema.optional(),\n});\nexport type OnboardingProgress = z.infer<typeof onboardingProgressSchema>;\n\n/** `PUT /onboarding/progress` body — any subset of the cursor, merged server-side. */\nexport const onboardingProgressUpdateSchema = z\n .object({\n welcomed: z.boolean(),\n role: onboardingRoleSchema.nullable(),\n step: z.number().int().min(0).max(20),\n gymCode: gymCodeSchema,\n memberDraft: onboardingDraftSchema,\n ownerDraft: onboardingDraftSchema,\n })\n .partial();\nexport type OnboardingProgressUpdate = z.infer<typeof onboardingProgressUpdateSchema>;\n\n/**\n * `GET /onboarding/status` response: completion truth (derived from the user's\n * domain data) plus the resume cursor. `onboarded` is authoritative — an owner\n * with an organisation, or a member with a profile, is onboarded regardless of\n * the cursor.\n */\nexport const onboardingStatusSchema = z.object({\n onboarded: z.boolean(),\n role: onboardingRoleSchema.nullable(),\n organisationId: z.string().optional(),\n welcomed: z.boolean(),\n step: z.number().int().min(0),\n gymCode: z.string().optional(),\n memberDraft: onboardingDraftSchema.optional(),\n ownerDraft: onboardingDraftSchema.optional(),\n});\nexport type OnboardingStatus = z.infer<typeof onboardingStatusSchema>;\n"]}
package/dist/user.d.ts CHANGED
@@ -20,8 +20,8 @@ export type AppRole = z.infer<typeof appRoleSchema>;
20
20
  export declare const userStatusSchema: z.ZodEnum<{
21
21
  active: "active";
22
22
  inactive: "inactive";
23
- suspended: "suspended";
24
23
  deleted: "deleted";
24
+ suspended: "suspended";
25
25
  }>;
26
26
  export type UserStatus = z.infer<typeof userStatusSchema>;
27
27
  export declare const meSchema: z.ZodObject<{
@@ -41,8 +41,8 @@ export declare const meSchema: z.ZodObject<{
41
41
  status: z.ZodEnum<{
42
42
  active: "active";
43
43
  inactive: "inactive";
44
- suspended: "suspended";
45
44
  deleted: "deleted";
45
+ suspended: "suspended";
46
46
  }>;
47
47
  createdAt: z.ZodISODateTime;
48
48
  updatedAt: z.ZodISODateTime;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "gymmonk-schema",
3
- "version": "0.3.0",
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.",
3
+ "version": "0.4.0",
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",
7
7
  "module": "./dist/index.js",