@tbs-fitness/contract 0.8.0 → 0.9.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/README.md CHANGED
@@ -43,6 +43,13 @@ const result = createFlagSchema.safeParse(input); // The same rules as the API.
43
43
 
44
44
  Requests have two types. Clients use `*Input`, where fields with a default are optional. The backend uses `*Body`, which is the result after validation. List queries come from the URL as strings, so they have no `*Input`: use `Partial<PaginationQuery>` or `Partial<AuditLogQuery>`.
45
45
 
46
+ ## Version 0.9.0
47
+
48
+ The goal library. Additive; consumers install `^0.9.0` explicitly.
49
+
50
+ - `/goals`: `createGoalSchema`, `updateGoalSchema` (with `version`), `goalListQuerySchema`, `goalResponseSchema`, `goalStatsResponseSchema`, and the client's `clientGoalResponseSchema` and `clientGoalListResponseSchema`. `normalizeGoalName` is what duplicate name checks compare.
51
+ - `/enums`: `GOAL_ICONS` (icon names each app maps to its own icon), `GOAL_STATUSES` (`active`, `inactive`), `GOAL_SORT_FIELDS`, and the permissions `goals:read`, `goals:create`, `goals:update`, `goals:delete` and `goals:read:self`.
52
+
46
53
  ## Version 0.8.0 (breaking)
47
54
 
48
55
  - `newPasswordSchema` now matches the new-password screens: at least 8 characters (was 12) and at least one number. `MIN_PASSWORD_LENGTH` is `8`, and the new `PASSWORD_DIGIT_PATTERN` lets apps show the number rule as it is typed. Sign-up, password change and reset all use it; login does not, so existing passwords keep working.
@@ -32,6 +32,11 @@ export declare const PERMISSIONS: {
32
32
  readonly foodsSearchSelf: "foods:search:self";
33
33
  readonly foodSuggestionsSubmitSelf: "food_suggestions:submit:self";
34
34
  readonly foodSuggestionsReadSelf: "food_suggestions:read:self";
35
+ readonly goalsRead: "goals:read";
36
+ readonly goalsCreate: "goals:create";
37
+ readonly goalsUpdate: "goals:update";
38
+ readonly goalsDelete: "goals:delete";
39
+ readonly goalsReadSelf: "goals:read:self";
35
40
  };
36
41
  export type Permission = (typeof PERMISSIONS)[keyof typeof PERMISSIONS];
37
42
  /**
@@ -124,3 +129,11 @@ export declare const FOOD_SORT_FIELDS: readonly ["name", "updatedAt", "calories"
124
129
  export type FoodSortField = (typeof FOOD_SORT_FIELDS)[number];
125
130
  export declare const SORT_ORDERS: readonly ["asc", "desc"];
126
131
  export type SortOrder = (typeof SORT_ORDERS)[number];
132
+ /** The icons a goal can show, by name. Each app maps a name to its own icon. */
133
+ export declare const GOAL_ICONS: readonly ["target", "trending-down", "trending-up", "dumbbell", "refresh", "activity", "trophy", "heart", "flame", "scale"];
134
+ export type GoalIcon = (typeof GOAL_ICONS)[number];
135
+ /** Whether clients can pick a goal: live goals are either shown or hidden. */
136
+ export declare const GOAL_STATUSES: readonly ["active", "inactive"];
137
+ export type GoalStatus = (typeof GOAL_STATUSES)[number];
138
+ export declare const GOAL_SORT_FIELDS: readonly ["sortOrder", "name", "updatedAt"];
139
+ export type GoalSortField = (typeof GOAL_SORT_FIELDS)[number];
@@ -2,7 +2,7 @@
2
2
  // These values are stored in the database and sent over the API. Renaming or
3
3
  // removing one is a breaking change, so it needs a major version.
4
4
  Object.defineProperty(exports, "__esModule", { value: true });
5
- exports.SORT_ORDERS = exports.FOOD_SORT_FIELDS = exports.FOOD_METADATA_SELECTIONS = exports.FOOD_IMPORT_ISSUE_SEVERITIES = exports.FOOD_IMPORT_FILE_TYPES = exports.FOOD_IMPORT_ROW_STATUSES = exports.FOOD_IMPORT_STATUSES = exports.FOOD_IMPORT_MODES = exports.FOOD_SUGGESTION_RESOLUTIONS = exports.FOOD_SUGGESTION_STATUSES = exports.NUTRITION_BASES = exports.PORTION_UNITS = exports.SERVING_UNITS = exports.FOOD_SOURCES = exports.TEMPLATE_STATUSES = exports.DELIVERY_STATUSES = exports.MESSAGE_TYPES = exports.COMMUNICATION_CHANNELS = exports.FEATURE_FLAG_RULE_TYPES = exports.AUDIT_OUTCOMES = exports.CSRF_HEADER = exports.CSRF_COOKIE_NAMES = exports.TOKEN_TRANSPORTS = exports.TIMEZONE_SOURCES = exports.ADMIN_SETTABLE_ACCOUNT_STATUSES = exports.ACCOUNT_STATUSES = exports.PERMISSIONS = exports.ROLES = exports.APP_ENVIRONMENTS = void 0;
5
+ exports.GOAL_SORT_FIELDS = exports.GOAL_STATUSES = exports.GOAL_ICONS = exports.SORT_ORDERS = exports.FOOD_SORT_FIELDS = exports.FOOD_METADATA_SELECTIONS = exports.FOOD_IMPORT_ISSUE_SEVERITIES = exports.FOOD_IMPORT_FILE_TYPES = exports.FOOD_IMPORT_ROW_STATUSES = exports.FOOD_IMPORT_STATUSES = exports.FOOD_IMPORT_MODES = exports.FOOD_SUGGESTION_RESOLUTIONS = exports.FOOD_SUGGESTION_STATUSES = exports.NUTRITION_BASES = exports.PORTION_UNITS = exports.SERVING_UNITS = exports.FOOD_SOURCES = exports.TEMPLATE_STATUSES = exports.DELIVERY_STATUSES = exports.MESSAGE_TYPES = exports.COMMUNICATION_CHANNELS = exports.FEATURE_FLAG_RULE_TYPES = exports.AUDIT_OUTCOMES = exports.CSRF_HEADER = exports.CSRF_COOKIE_NAMES = exports.TOKEN_TRANSPORTS = exports.TIMEZONE_SOURCES = exports.ADMIN_SETTABLE_ACCOUNT_STATUSES = exports.ACCOUNT_STATUSES = exports.PERMISSIONS = exports.ROLES = exports.APP_ENVIRONMENTS = void 0;
6
6
  exports.APP_ENVIRONMENTS = [
7
7
  'development',
8
8
  'staging',
@@ -39,6 +39,11 @@ exports.PERMISSIONS = {
39
39
  foodsSearchSelf: 'foods:search:self',
40
40
  foodSuggestionsSubmitSelf: 'food_suggestions:submit:self',
41
41
  foodSuggestionsReadSelf: 'food_suggestions:read:self',
42
+ goalsRead: 'goals:read',
43
+ goalsCreate: 'goals:create',
44
+ goalsUpdate: 'goals:update',
45
+ goalsDelete: 'goals:delete',
46
+ goalsReadSelf: 'goals:read:self',
42
47
  };
43
48
  /**
44
49
  * `pending_verification` is a self-registered user who has not yet proved they
@@ -156,3 +161,19 @@ exports.FOOD_SORT_FIELDS = [
156
161
  'protein',
157
162
  ];
158
163
  exports.SORT_ORDERS = ['asc', 'desc'];
164
+ /** The icons a goal can show, by name. Each app maps a name to its own icon. */
165
+ exports.GOAL_ICONS = [
166
+ 'target',
167
+ 'trending-down',
168
+ 'trending-up',
169
+ 'dumbbell',
170
+ 'refresh',
171
+ 'activity',
172
+ 'trophy',
173
+ 'heart',
174
+ 'flame',
175
+ 'scale',
176
+ ];
177
+ /** Whether clients can pick a goal: live goals are either shown or hidden. */
178
+ exports.GOAL_STATUSES = ['active', 'inactive'];
179
+ exports.GOAL_SORT_FIELDS = ['sortOrder', 'name', 'updatedAt'];
@@ -0,0 +1,137 @@
1
+ import { z } from 'zod';
2
+ export declare const MAX_GOAL_NAME_LENGTH = 60;
3
+ export declare const MAX_GOAL_DESCRIPTION_LENGTH = 200;
4
+ export declare const MAX_GOAL_SEARCH_LENGTH = 60;
5
+ export declare const MAX_GOAL_SORT_ORDER = 9999;
6
+ export declare const goalIdParamSchema: z.ZodUUID;
7
+ /** Lower case with spaces collapsed: what duplicate name checks compare. */
8
+ export declare const normalizeGoalName: (name: string) => string;
9
+ export declare const createGoalSchema: z.ZodObject<{
10
+ name: z.ZodString;
11
+ description: z.ZodOptional<z.ZodNullable<z.ZodString>>;
12
+ icon: z.ZodDefault<z.ZodEnum<{
13
+ target: "target";
14
+ "trending-down": "trending-down";
15
+ "trending-up": "trending-up";
16
+ dumbbell: "dumbbell";
17
+ refresh: "refresh";
18
+ activity: "activity";
19
+ trophy: "trophy";
20
+ heart: "heart";
21
+ flame: "flame";
22
+ scale: "scale";
23
+ }>>;
24
+ isActive: z.ZodDefault<z.ZodBoolean>;
25
+ sortOrder: z.ZodOptional<z.ZodNumber>;
26
+ }, z.core.$strict>;
27
+ export type CreateGoalInput = z.input<typeof createGoalSchema>;
28
+ export type CreateGoalBody = z.output<typeof createGoalSchema>;
29
+ export declare const updateGoalSchema: z.ZodObject<{
30
+ version: z.ZodNumber;
31
+ name: z.ZodOptional<z.ZodString>;
32
+ description: z.ZodOptional<z.ZodNullable<z.ZodString>>;
33
+ icon: z.ZodOptional<z.ZodEnum<{
34
+ target: "target";
35
+ "trending-down": "trending-down";
36
+ "trending-up": "trending-up";
37
+ dumbbell: "dumbbell";
38
+ refresh: "refresh";
39
+ activity: "activity";
40
+ trophy: "trophy";
41
+ heart: "heart";
42
+ flame: "flame";
43
+ scale: "scale";
44
+ }>>;
45
+ isActive: z.ZodOptional<z.ZodBoolean>;
46
+ sortOrder: z.ZodOptional<z.ZodNumber>;
47
+ }, z.core.$strict>;
48
+ export type UpdateGoalInput = z.input<typeof updateGoalSchema>;
49
+ export type UpdateGoalBody = z.output<typeof updateGoalSchema>;
50
+ export declare const goalListQuerySchema: z.ZodObject<{
51
+ page: z.ZodDefault<z.ZodCoercedNumber<unknown>>;
52
+ pageSize: z.ZodDefault<z.ZodCoercedNumber<unknown>>;
53
+ search: z.ZodOptional<z.ZodString>;
54
+ status: z.ZodOptional<z.ZodEnum<{
55
+ active: "active";
56
+ inactive: "inactive";
57
+ }>>;
58
+ sort: z.ZodDefault<z.ZodEnum<{
59
+ name: "name";
60
+ updatedAt: "updatedAt";
61
+ sortOrder: "sortOrder";
62
+ }>>;
63
+ order: z.ZodDefault<z.ZodEnum<{
64
+ asc: "asc";
65
+ desc: "desc";
66
+ }>>;
67
+ }, z.core.$strip>;
68
+ export type GoalListQuery = z.output<typeof goalListQuerySchema>;
69
+ export declare const goalResponseSchema: z.ZodObject<{
70
+ id: z.ZodUUID;
71
+ name: z.ZodString;
72
+ description: z.ZodNullable<z.ZodString>;
73
+ icon: z.ZodEnum<{
74
+ target: "target";
75
+ "trending-down": "trending-down";
76
+ "trending-up": "trending-up";
77
+ dumbbell: "dumbbell";
78
+ refresh: "refresh";
79
+ activity: "activity";
80
+ trophy: "trophy";
81
+ heart: "heart";
82
+ flame: "flame";
83
+ scale: "scale";
84
+ }>;
85
+ isActive: z.ZodBoolean;
86
+ sortOrder: z.ZodNumber;
87
+ createdBy: z.ZodObject<{
88
+ id: z.ZodUUID;
89
+ fullName: z.ZodNullable<z.ZodString>;
90
+ }, z.core.$strip>;
91
+ version: z.ZodNumber;
92
+ createdAt: z.ZodISODateTime;
93
+ updatedAt: z.ZodISODateTime;
94
+ }, z.core.$strip>;
95
+ export type GoalResponse = z.output<typeof goalResponseSchema>;
96
+ export declare const goalStatsResponseSchema: z.ZodObject<{
97
+ total: z.ZodNumber;
98
+ active: z.ZodNumber;
99
+ inactive: z.ZodNumber;
100
+ }, z.core.$strip>;
101
+ export type GoalStatsResponse = z.output<typeof goalStatsResponseSchema>;
102
+ /** What a client sees: active goals only, without who edited them. */
103
+ export declare const clientGoalResponseSchema: z.ZodObject<{
104
+ id: z.ZodUUID;
105
+ name: z.ZodString;
106
+ description: z.ZodNullable<z.ZodString>;
107
+ icon: z.ZodEnum<{
108
+ target: "target";
109
+ "trending-down": "trending-down";
110
+ "trending-up": "trending-up";
111
+ dumbbell: "dumbbell";
112
+ refresh: "refresh";
113
+ activity: "activity";
114
+ trophy: "trophy";
115
+ heart: "heart";
116
+ flame: "flame";
117
+ scale: "scale";
118
+ }>;
119
+ }, z.core.$strip>;
120
+ export type ClientGoalResponse = z.output<typeof clientGoalResponseSchema>;
121
+ export declare const clientGoalListResponseSchema: z.ZodArray<z.ZodObject<{
122
+ id: z.ZodUUID;
123
+ name: z.ZodString;
124
+ description: z.ZodNullable<z.ZodString>;
125
+ icon: z.ZodEnum<{
126
+ target: "target";
127
+ "trending-down": "trending-down";
128
+ "trending-up": "trending-up";
129
+ dumbbell: "dumbbell";
130
+ refresh: "refresh";
131
+ activity: "activity";
132
+ trophy: "trophy";
133
+ heart: "heart";
134
+ flame: "flame";
135
+ scale: "scale";
136
+ }>;
137
+ }, z.core.$strip>>;
@@ -0,0 +1,80 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.clientGoalListResponseSchema = exports.clientGoalResponseSchema = exports.goalStatsResponseSchema = exports.goalResponseSchema = exports.goalListQuerySchema = exports.updateGoalSchema = exports.createGoalSchema = exports.normalizeGoalName = exports.goalIdParamSchema = exports.MAX_GOAL_SORT_ORDER = exports.MAX_GOAL_SEARCH_LENGTH = exports.MAX_GOAL_DESCRIPTION_LENGTH = exports.MAX_GOAL_NAME_LENGTH = void 0;
4
+ const zod_1 = require("zod");
5
+ const date_time_schema_js_1 = require("../common/date-time.schema.js");
6
+ const pagination_schema_js_1 = require("../common/pagination.schema.js");
7
+ const safe_text_schema_js_1 = require("../common/safe-text.schema.js");
8
+ const index_js_1 = require("../enums/index.js");
9
+ const food_dto_js_1 = require("../food/food.dto.js");
10
+ // A goal is what a client works towards, such as "Build muscle". Coaches keep
11
+ // the list; clients pick one from the goals that are active.
12
+ //
13
+ // Each request has two types. `*Input` is what a client sends, where fields with
14
+ // a default are optional. `*Body` is what the API receives after validation.
15
+ exports.MAX_GOAL_NAME_LENGTH = 60;
16
+ exports.MAX_GOAL_DESCRIPTION_LENGTH = 200;
17
+ exports.MAX_GOAL_SEARCH_LENGTH = 60;
18
+ exports.MAX_GOAL_SORT_ORDER = 9999;
19
+ exports.goalIdParamSchema = zod_1.z.uuid();
20
+ /** Lower case with spaces collapsed: what duplicate name checks compare. */
21
+ const normalizeGoalName = (name) => name.trim().replace(/\s+/g, ' ').toLowerCase();
22
+ exports.normalizeGoalName = normalizeGoalName;
23
+ const goalNameSchema = (0, safe_text_schema_js_1.safeTextSchema)(exports.MAX_GOAL_NAME_LENGTH);
24
+ const goalDescriptionSchema = (0, safe_text_schema_js_1.safeTextSchema)(exports.MAX_GOAL_DESCRIPTION_LENGTH);
25
+ const sortOrderSchema = zod_1.z.number().int().min(0).max(exports.MAX_GOAL_SORT_ORDER);
26
+ // Strict objects reject unknown fields, so an attempt to set `createdBy`
27
+ // through the API fails loudly.
28
+ exports.createGoalSchema = zod_1.z.strictObject({
29
+ name: goalNameSchema,
30
+ /** One line under the name, such as "Focus on hypertrophy and progressive overload." */
31
+ description: goalDescriptionSchema.nullable().optional(),
32
+ icon: zod_1.z.enum(index_js_1.GOAL_ICONS).default('target'),
33
+ /** Inactive goals stay in the list but are not offered to clients. */
34
+ isActive: zod_1.z.boolean().default(true),
35
+ /** Lower comes first. Left out, the goal goes after the others. */
36
+ sortOrder: sortOrderSchema.optional(),
37
+ });
38
+ exports.updateGoalSchema = zod_1.z
39
+ .strictObject({
40
+ // The version last read, so that a stale edit is refused rather than lost.
41
+ version: zod_1.z.number().int().min(1),
42
+ name: goalNameSchema.optional(),
43
+ description: goalDescriptionSchema.nullable().optional(),
44
+ icon: zod_1.z.enum(index_js_1.GOAL_ICONS).optional(),
45
+ isActive: zod_1.z.boolean().optional(),
46
+ sortOrder: sortOrderSchema.optional(),
47
+ })
48
+ .refine(({ version: _version, ...changes }) => Object.values(changes).some((value) => value !== undefined), { error: 'Provide at least one field to change' });
49
+ exports.goalListQuerySchema = pagination_schema_js_1.paginationQuerySchema.extend({
50
+ search: zod_1.z.string().trim().min(1).max(exports.MAX_GOAL_SEARCH_LENGTH).optional(),
51
+ status: zod_1.z.enum(index_js_1.GOAL_STATUSES).optional(),
52
+ sort: zod_1.z.enum(index_js_1.GOAL_SORT_FIELDS).default('sortOrder'),
53
+ order: zod_1.z.enum(index_js_1.SORT_ORDERS).default('asc'),
54
+ });
55
+ // Responses
56
+ exports.goalResponseSchema = zod_1.z.object({
57
+ id: zod_1.z.uuid(),
58
+ name: zod_1.z.string(),
59
+ description: zod_1.z.string().nullable(),
60
+ icon: zod_1.z.enum(index_js_1.GOAL_ICONS),
61
+ isActive: zod_1.z.boolean(),
62
+ sortOrder: zod_1.z.number().int(),
63
+ createdBy: food_dto_js_1.userRefSchema,
64
+ version: zod_1.z.number().int(),
65
+ createdAt: date_time_schema_js_1.utcTimestampSchema,
66
+ updatedAt: date_time_schema_js_1.utcTimestampSchema,
67
+ });
68
+ exports.goalStatsResponseSchema = zod_1.z.object({
69
+ total: zod_1.z.number().int(),
70
+ active: zod_1.z.number().int(),
71
+ inactive: zod_1.z.number().int(),
72
+ });
73
+ /** What a client sees: active goals only, without who edited them. */
74
+ exports.clientGoalResponseSchema = zod_1.z.object({
75
+ id: zod_1.z.uuid(),
76
+ name: zod_1.z.string(),
77
+ description: zod_1.z.string().nullable(),
78
+ icon: zod_1.z.enum(index_js_1.GOAL_ICONS),
79
+ });
80
+ exports.clientGoalListResponseSchema = zod_1.z.array(exports.clientGoalResponseSchema);
@@ -0,0 +1 @@
1
+ export * from './goal.dto.js';
@@ -0,0 +1,17 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __exportStar = (this && this.__exportStar) || function(m, exports) {
14
+ for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
15
+ };
16
+ Object.defineProperty(exports, "__esModule", { value: true });
17
+ __exportStar(require("./goal.dto.js"), exports);
@@ -32,6 +32,11 @@ export declare const PERMISSIONS: {
32
32
  readonly foodsSearchSelf: "foods:search:self";
33
33
  readonly foodSuggestionsSubmitSelf: "food_suggestions:submit:self";
34
34
  readonly foodSuggestionsReadSelf: "food_suggestions:read:self";
35
+ readonly goalsRead: "goals:read";
36
+ readonly goalsCreate: "goals:create";
37
+ readonly goalsUpdate: "goals:update";
38
+ readonly goalsDelete: "goals:delete";
39
+ readonly goalsReadSelf: "goals:read:self";
35
40
  };
36
41
  export type Permission = (typeof PERMISSIONS)[keyof typeof PERMISSIONS];
37
42
  /**
@@ -124,3 +129,11 @@ export declare const FOOD_SORT_FIELDS: readonly ["name", "updatedAt", "calories"
124
129
  export type FoodSortField = (typeof FOOD_SORT_FIELDS)[number];
125
130
  export declare const SORT_ORDERS: readonly ["asc", "desc"];
126
131
  export type SortOrder = (typeof SORT_ORDERS)[number];
132
+ /** The icons a goal can show, by name. Each app maps a name to its own icon. */
133
+ export declare const GOAL_ICONS: readonly ["target", "trending-down", "trending-up", "dumbbell", "refresh", "activity", "trophy", "heart", "flame", "scale"];
134
+ export type GoalIcon = (typeof GOAL_ICONS)[number];
135
+ /** Whether clients can pick a goal: live goals are either shown or hidden. */
136
+ export declare const GOAL_STATUSES: readonly ["active", "inactive"];
137
+ export type GoalStatus = (typeof GOAL_STATUSES)[number];
138
+ export declare const GOAL_SORT_FIELDS: readonly ["sortOrder", "name", "updatedAt"];
139
+ export type GoalSortField = (typeof GOAL_SORT_FIELDS)[number];
@@ -36,6 +36,11 @@ export const PERMISSIONS = {
36
36
  foodsSearchSelf: 'foods:search:self',
37
37
  foodSuggestionsSubmitSelf: 'food_suggestions:submit:self',
38
38
  foodSuggestionsReadSelf: 'food_suggestions:read:self',
39
+ goalsRead: 'goals:read',
40
+ goalsCreate: 'goals:create',
41
+ goalsUpdate: 'goals:update',
42
+ goalsDelete: 'goals:delete',
43
+ goalsReadSelf: 'goals:read:self',
39
44
  };
40
45
  /**
41
46
  * `pending_verification` is a self-registered user who has not yet proved they
@@ -153,3 +158,19 @@ export const FOOD_SORT_FIELDS = [
153
158
  'protein',
154
159
  ];
155
160
  export const SORT_ORDERS = ['asc', 'desc'];
161
+ /** The icons a goal can show, by name. Each app maps a name to its own icon. */
162
+ export const GOAL_ICONS = [
163
+ 'target',
164
+ 'trending-down',
165
+ 'trending-up',
166
+ 'dumbbell',
167
+ 'refresh',
168
+ 'activity',
169
+ 'trophy',
170
+ 'heart',
171
+ 'flame',
172
+ 'scale',
173
+ ];
174
+ /** Whether clients can pick a goal: live goals are either shown or hidden. */
175
+ export const GOAL_STATUSES = ['active', 'inactive'];
176
+ export const GOAL_SORT_FIELDS = ['sortOrder', 'name', 'updatedAt'];
@@ -0,0 +1,137 @@
1
+ import { z } from 'zod';
2
+ export declare const MAX_GOAL_NAME_LENGTH = 60;
3
+ export declare const MAX_GOAL_DESCRIPTION_LENGTH = 200;
4
+ export declare const MAX_GOAL_SEARCH_LENGTH = 60;
5
+ export declare const MAX_GOAL_SORT_ORDER = 9999;
6
+ export declare const goalIdParamSchema: z.ZodUUID;
7
+ /** Lower case with spaces collapsed: what duplicate name checks compare. */
8
+ export declare const normalizeGoalName: (name: string) => string;
9
+ export declare const createGoalSchema: z.ZodObject<{
10
+ name: z.ZodString;
11
+ description: z.ZodOptional<z.ZodNullable<z.ZodString>>;
12
+ icon: z.ZodDefault<z.ZodEnum<{
13
+ target: "target";
14
+ "trending-down": "trending-down";
15
+ "trending-up": "trending-up";
16
+ dumbbell: "dumbbell";
17
+ refresh: "refresh";
18
+ activity: "activity";
19
+ trophy: "trophy";
20
+ heart: "heart";
21
+ flame: "flame";
22
+ scale: "scale";
23
+ }>>;
24
+ isActive: z.ZodDefault<z.ZodBoolean>;
25
+ sortOrder: z.ZodOptional<z.ZodNumber>;
26
+ }, z.core.$strict>;
27
+ export type CreateGoalInput = z.input<typeof createGoalSchema>;
28
+ export type CreateGoalBody = z.output<typeof createGoalSchema>;
29
+ export declare const updateGoalSchema: z.ZodObject<{
30
+ version: z.ZodNumber;
31
+ name: z.ZodOptional<z.ZodString>;
32
+ description: z.ZodOptional<z.ZodNullable<z.ZodString>>;
33
+ icon: z.ZodOptional<z.ZodEnum<{
34
+ target: "target";
35
+ "trending-down": "trending-down";
36
+ "trending-up": "trending-up";
37
+ dumbbell: "dumbbell";
38
+ refresh: "refresh";
39
+ activity: "activity";
40
+ trophy: "trophy";
41
+ heart: "heart";
42
+ flame: "flame";
43
+ scale: "scale";
44
+ }>>;
45
+ isActive: z.ZodOptional<z.ZodBoolean>;
46
+ sortOrder: z.ZodOptional<z.ZodNumber>;
47
+ }, z.core.$strict>;
48
+ export type UpdateGoalInput = z.input<typeof updateGoalSchema>;
49
+ export type UpdateGoalBody = z.output<typeof updateGoalSchema>;
50
+ export declare const goalListQuerySchema: z.ZodObject<{
51
+ page: z.ZodDefault<z.ZodCoercedNumber<unknown>>;
52
+ pageSize: z.ZodDefault<z.ZodCoercedNumber<unknown>>;
53
+ search: z.ZodOptional<z.ZodString>;
54
+ status: z.ZodOptional<z.ZodEnum<{
55
+ active: "active";
56
+ inactive: "inactive";
57
+ }>>;
58
+ sort: z.ZodDefault<z.ZodEnum<{
59
+ name: "name";
60
+ updatedAt: "updatedAt";
61
+ sortOrder: "sortOrder";
62
+ }>>;
63
+ order: z.ZodDefault<z.ZodEnum<{
64
+ asc: "asc";
65
+ desc: "desc";
66
+ }>>;
67
+ }, z.core.$strip>;
68
+ export type GoalListQuery = z.output<typeof goalListQuerySchema>;
69
+ export declare const goalResponseSchema: z.ZodObject<{
70
+ id: z.ZodUUID;
71
+ name: z.ZodString;
72
+ description: z.ZodNullable<z.ZodString>;
73
+ icon: z.ZodEnum<{
74
+ target: "target";
75
+ "trending-down": "trending-down";
76
+ "trending-up": "trending-up";
77
+ dumbbell: "dumbbell";
78
+ refresh: "refresh";
79
+ activity: "activity";
80
+ trophy: "trophy";
81
+ heart: "heart";
82
+ flame: "flame";
83
+ scale: "scale";
84
+ }>;
85
+ isActive: z.ZodBoolean;
86
+ sortOrder: z.ZodNumber;
87
+ createdBy: z.ZodObject<{
88
+ id: z.ZodUUID;
89
+ fullName: z.ZodNullable<z.ZodString>;
90
+ }, z.core.$strip>;
91
+ version: z.ZodNumber;
92
+ createdAt: z.ZodISODateTime;
93
+ updatedAt: z.ZodISODateTime;
94
+ }, z.core.$strip>;
95
+ export type GoalResponse = z.output<typeof goalResponseSchema>;
96
+ export declare const goalStatsResponseSchema: z.ZodObject<{
97
+ total: z.ZodNumber;
98
+ active: z.ZodNumber;
99
+ inactive: z.ZodNumber;
100
+ }, z.core.$strip>;
101
+ export type GoalStatsResponse = z.output<typeof goalStatsResponseSchema>;
102
+ /** What a client sees: active goals only, without who edited them. */
103
+ export declare const clientGoalResponseSchema: z.ZodObject<{
104
+ id: z.ZodUUID;
105
+ name: z.ZodString;
106
+ description: z.ZodNullable<z.ZodString>;
107
+ icon: z.ZodEnum<{
108
+ target: "target";
109
+ "trending-down": "trending-down";
110
+ "trending-up": "trending-up";
111
+ dumbbell: "dumbbell";
112
+ refresh: "refresh";
113
+ activity: "activity";
114
+ trophy: "trophy";
115
+ heart: "heart";
116
+ flame: "flame";
117
+ scale: "scale";
118
+ }>;
119
+ }, z.core.$strip>;
120
+ export type ClientGoalResponse = z.output<typeof clientGoalResponseSchema>;
121
+ export declare const clientGoalListResponseSchema: z.ZodArray<z.ZodObject<{
122
+ id: z.ZodUUID;
123
+ name: z.ZodString;
124
+ description: z.ZodNullable<z.ZodString>;
125
+ icon: z.ZodEnum<{
126
+ target: "target";
127
+ "trending-down": "trending-down";
128
+ "trending-up": "trending-up";
129
+ dumbbell: "dumbbell";
130
+ refresh: "refresh";
131
+ activity: "activity";
132
+ trophy: "trophy";
133
+ heart: "heart";
134
+ flame: "flame";
135
+ scale: "scale";
136
+ }>;
137
+ }, z.core.$strip>>;
@@ -0,0 +1,76 @@
1
+ import { z } from 'zod';
2
+ import { utcTimestampSchema } from '../common/date-time.schema.js';
3
+ import { paginationQuerySchema } from '../common/pagination.schema.js';
4
+ import { safeTextSchema } from '../common/safe-text.schema.js';
5
+ import { GOAL_ICONS, GOAL_SORT_FIELDS, GOAL_STATUSES, SORT_ORDERS, } from '../enums/index.js';
6
+ import { userRefSchema } from '../food/food.dto.js';
7
+ // A goal is what a client works towards, such as "Build muscle". Coaches keep
8
+ // the list; clients pick one from the goals that are active.
9
+ //
10
+ // Each request has two types. `*Input` is what a client sends, where fields with
11
+ // a default are optional. `*Body` is what the API receives after validation.
12
+ export const MAX_GOAL_NAME_LENGTH = 60;
13
+ export const MAX_GOAL_DESCRIPTION_LENGTH = 200;
14
+ export const MAX_GOAL_SEARCH_LENGTH = 60;
15
+ export const MAX_GOAL_SORT_ORDER = 9999;
16
+ export const goalIdParamSchema = z.uuid();
17
+ /** Lower case with spaces collapsed: what duplicate name checks compare. */
18
+ export const normalizeGoalName = (name) => name.trim().replace(/\s+/g, ' ').toLowerCase();
19
+ const goalNameSchema = safeTextSchema(MAX_GOAL_NAME_LENGTH);
20
+ const goalDescriptionSchema = safeTextSchema(MAX_GOAL_DESCRIPTION_LENGTH);
21
+ const sortOrderSchema = z.number().int().min(0).max(MAX_GOAL_SORT_ORDER);
22
+ // Strict objects reject unknown fields, so an attempt to set `createdBy`
23
+ // through the API fails loudly.
24
+ export const createGoalSchema = z.strictObject({
25
+ name: goalNameSchema,
26
+ /** One line under the name, such as "Focus on hypertrophy and progressive overload." */
27
+ description: goalDescriptionSchema.nullable().optional(),
28
+ icon: z.enum(GOAL_ICONS).default('target'),
29
+ /** Inactive goals stay in the list but are not offered to clients. */
30
+ isActive: z.boolean().default(true),
31
+ /** Lower comes first. Left out, the goal goes after the others. */
32
+ sortOrder: sortOrderSchema.optional(),
33
+ });
34
+ export const updateGoalSchema = z
35
+ .strictObject({
36
+ // The version last read, so that a stale edit is refused rather than lost.
37
+ version: z.number().int().min(1),
38
+ name: goalNameSchema.optional(),
39
+ description: goalDescriptionSchema.nullable().optional(),
40
+ icon: z.enum(GOAL_ICONS).optional(),
41
+ isActive: z.boolean().optional(),
42
+ sortOrder: sortOrderSchema.optional(),
43
+ })
44
+ .refine(({ version: _version, ...changes }) => Object.values(changes).some((value) => value !== undefined), { error: 'Provide at least one field to change' });
45
+ export const goalListQuerySchema = paginationQuerySchema.extend({
46
+ search: z.string().trim().min(1).max(MAX_GOAL_SEARCH_LENGTH).optional(),
47
+ status: z.enum(GOAL_STATUSES).optional(),
48
+ sort: z.enum(GOAL_SORT_FIELDS).default('sortOrder'),
49
+ order: z.enum(SORT_ORDERS).default('asc'),
50
+ });
51
+ // Responses
52
+ export const goalResponseSchema = z.object({
53
+ id: z.uuid(),
54
+ name: z.string(),
55
+ description: z.string().nullable(),
56
+ icon: z.enum(GOAL_ICONS),
57
+ isActive: z.boolean(),
58
+ sortOrder: z.number().int(),
59
+ createdBy: userRefSchema,
60
+ version: z.number().int(),
61
+ createdAt: utcTimestampSchema,
62
+ updatedAt: utcTimestampSchema,
63
+ });
64
+ export const goalStatsResponseSchema = z.object({
65
+ total: z.number().int(),
66
+ active: z.number().int(),
67
+ inactive: z.number().int(),
68
+ });
69
+ /** What a client sees: active goals only, without who edited them. */
70
+ export const clientGoalResponseSchema = z.object({
71
+ id: z.uuid(),
72
+ name: z.string(),
73
+ description: z.string().nullable(),
74
+ icon: z.enum(GOAL_ICONS),
75
+ });
76
+ export const clientGoalListResponseSchema = z.array(clientGoalResponseSchema);
@@ -0,0 +1 @@
1
+ export * from './goal.dto.js';
@@ -0,0 +1 @@
1
+ export * from './goal.dto.js';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tbs-fitness/contract",
3
- "version": "0.8.0",
3
+ "version": "0.9.0",
4
4
  "description": "TBS API contract: Zod schemas, DTO types and enums shared by the backend, web and mobile apps",
5
5
  "license": "UNLICENSED",
6
6
  "type": "module",
@@ -113,6 +113,16 @@
113
113
  "default": "./dist/cjs/food/index.js"
114
114
  }
115
115
  },
116
+ "./goals": {
117
+ "import": {
118
+ "types": "./dist/esm/goals/index.d.ts",
119
+ "default": "./dist/esm/goals/index.js"
120
+ },
121
+ "require": {
122
+ "types": "./dist/cjs/goals/index.d.ts",
123
+ "default": "./dist/cjs/goals/index.js"
124
+ }
125
+ },
116
126
  "./package.json": "./package.json"
117
127
  },
118
128
  "scripts": {