@tbs-fitness/contract 0.5.0 → 0.6.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
@@ -15,6 +15,7 @@ It is published as `@tbs-fitness/contract`. The package holds definitions only:
15
15
  | `@tbs-fitness/contract/coach` | Coach sign-up, one-time code verification and resend, coach sign-in and profile, password recovery |
16
16
  | `@tbs-fitness/contract/audit` | The audit log query and response |
17
17
  | `@tbs-fitness/contract/feature-flags` | Feature flag requests and responses |
18
+ | `@tbs-fitness/contract/food` | Food library, metadata, CSV/XLSX import, client food suggestions and their review, and the shared nutrition helpers |
18
19
 
19
20
  Each domain has its own entry point, so names only need to be unique within a domain. `auditLogResponseSchema`, for example, exists in both `/audit` and `/feature-flags`.
20
21
 
@@ -42,6 +43,20 @@ const result = createFlagSchema.safeParse(input); // The same rules as the API.
42
43
 
43
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>`.
44
45
 
46
+ ## Version 0.6.0
47
+
48
+ The food library. Additive; consumers install `^0.6.0` explicitly.
49
+
50
+ - `/enums`: `FOOD_SOURCES`, `SERVING_UNITS` (`g`, `ml`), `PORTION_UNITS`, `NUTRITION_BASES`, `FOOD_SUGGESTION_STATUSES`, `FOOD_SUGGESTION_RESOLUTIONS`, `FOOD_IMPORT_MODES`, `FOOD_IMPORT_STATUSES`, `FOOD_IMPORT_ROW_STATUSES`, `FOOD_IMPORT_FILE_TYPES`, `FOOD_IMPORT_ISSUE_SEVERITIES`, `FOOD_METADATA_SELECTIONS`, `FOOD_SORT_FIELDS` and `SORT_ORDERS`.
51
+ - New `/food`:
52
+ - Library: `createFoodSchema`, `updateFoodSchema` (with `version`), `foodListQuerySchema`, `foodResponseSchema` (values per 100 and per serving, plus `formulaCalories`), `similarFoodResponseSchema`, `foodStatsResponseSchema`.
53
+ - Metadata: `metadataTypeResponseSchema`, `metadataValueResponseSchema`, `createMetadataValueSchema`, `updateMetadataValueSchema`.
54
+ - Import: `foodImportRowSchema` (one spreadsheet row, shared by the API and the preview), `commitFoodImportSchema`, `foodImportBatchResponseSchema`, `foodImportRowResponseSchema`, `FOOD_IMPORT_LIMITS`.
55
+ - Suggestions: `submitFoodSuggestionSchema`, `suggestionPhotoUploadSchema`, `presignedUploadResponseSchema`, `approveFoodSuggestionSchema` (one shape per resolution), `rejectFoodSuggestionSchema`, `foodSuggestionResponseSchema` (coach view), `clientFoodSuggestionResponseSchema` (client view), `FOOD_SUGGESTION_LIMITS`.
56
+ - Client search: `clientFoodSearchQuerySchema`, `clientFoodResponseSchema`.
57
+ - Helpers: `toPer100`, `perServing`, `formulaCalories` (Biki's formula, (protein + carbs) × 4 + fat × 9), `nutritionIssues`, `normalizeFoodName`, `slugifyMetadataKey`, `parseServingLabel`, `roundNutrient`.
58
+ - Library nutrients are per 100 of the serving unit: per 100 g, or per 100 ml for a serving in ml. Values have at most 2 decimals.
59
+
45
60
  ## Version 0.5.0 (breaking)
46
61
 
47
62
  - `ROLES` is now `ADMIN`, `COACH` and `CLIENT`: `CUSTOMER` is renamed to `CLIENT`. The backend migrates stored roles in step.
@@ -44,3 +44,41 @@ export type DeliveryStatus = (typeof DELIVERY_STATUSES)[number];
44
44
  /** Only one version of a template per message type and channel is active. */
45
45
  export declare const TEMPLATE_STATUSES: readonly ["draft", "active", "archived"];
46
46
  export type TemplateStatus = (typeof TEMPLATE_STATUSES)[number];
47
+ /** Who put a food into the library. */
48
+ export declare const FOOD_SOURCES: readonly ["ADMIN", "COACH", "CLIENT_SUGGESTION", "IMPORT"];
49
+ export type FoodSource = (typeof FOOD_SOURCES)[number];
50
+ /**
51
+ * The unit a library serving is measured in. Nutrients are stored per 100 of
52
+ * this unit, so a food in `ml` holds values per 100 ml.
53
+ */
54
+ export declare const SERVING_UNITS: readonly ["g", "ml"];
55
+ export type ServingUnit = (typeof SERVING_UNITS)[number];
56
+ /** The portion units a client can pick when logging a food. */
57
+ export declare const PORTION_UNITS: readonly ["BOWL", "GRAM", "PIECE", "CUP"];
58
+ export type PortionUnit = (typeof PORTION_UNITS)[number];
59
+ /** Whether submitted nutrient values are per 100 units or per serving. */
60
+ export declare const NUTRITION_BASES: readonly ["PER_100", "PER_SERVING"];
61
+ export type NutritionBasis = (typeof NUTRITION_BASES)[number];
62
+ export declare const FOOD_SUGGESTION_STATUSES: readonly ["pending", "approved", "rejected", "withdrawn"];
63
+ export type FoodSuggestionStatus = (typeof FOOD_SUGGESTION_STATUSES)[number];
64
+ /** How an approved suggestion was resolved. */
65
+ export declare const FOOD_SUGGESTION_RESOLUTIONS: readonly ["ADDED_TO_LIBRARY", "LINKED_TO_EXISTING", "APPROVED_WITHOUT_LIBRARY"];
66
+ export type FoodSuggestionResolution = (typeof FOOD_SUGGESTION_RESOLUTIONS)[number];
67
+ /** What a bulk import does with a row whose food is already in the library. */
68
+ export declare const FOOD_IMPORT_MODES: readonly ["SKIP_DUPLICATES", "UPDATE_EXISTING"];
69
+ export type FoodImportMode = (typeof FOOD_IMPORT_MODES)[number];
70
+ export declare const FOOD_IMPORT_STATUSES: readonly ["previewed", "completed", "expired"];
71
+ export type FoodImportStatus = (typeof FOOD_IMPORT_STATUSES)[number];
72
+ export declare const FOOD_IMPORT_ROW_STATUSES: readonly ["valid", "invalid", "duplicate_in_file", "duplicate_existing"];
73
+ export type FoodImportRowStatus = (typeof FOOD_IMPORT_ROW_STATUSES)[number];
74
+ export declare const FOOD_IMPORT_FILE_TYPES: readonly ["csv", "xlsx"];
75
+ export type FoodImportFileType = (typeof FOOD_IMPORT_FILE_TYPES)[number];
76
+ export declare const FOOD_IMPORT_ISSUE_SEVERITIES: readonly ["error", "warning"];
77
+ export type FoodImportIssueSeverity = (typeof FOOD_IMPORT_ISSUE_SEVERITIES)[number];
78
+ /** Whether a food takes one value of a metadata type or several. */
79
+ export declare const FOOD_METADATA_SELECTIONS: readonly ["single", "multiple"];
80
+ export type FoodMetadataSelection = (typeof FOOD_METADATA_SELECTIONS)[number];
81
+ export declare const FOOD_SORT_FIELDS: readonly ["name", "updatedAt", "calories", "protein"];
82
+ export type FoodSortField = (typeof FOOD_SORT_FIELDS)[number];
83
+ export declare const SORT_ORDERS: readonly ["asc", "desc"];
84
+ export type SortOrder = (typeof SORT_ORDERS)[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.TEMPLATE_STATUSES = exports.DELIVERY_STATUSES = exports.MESSAGE_TYPES = exports.COMMUNICATION_CHANNELS = exports.FEATURE_FLAG_RULE_TYPES = exports.AUDIT_OUTCOMES = exports.TOKEN_TRANSPORTS = exports.TIMEZONE_SOURCES = exports.ADMIN_SETTABLE_ACCOUNT_STATUSES = exports.ACCOUNT_STATUSES = exports.ROLES = exports.APP_ENVIRONMENTS = void 0;
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.TOKEN_TRANSPORTS = exports.TIMEZONE_SOURCES = exports.ADMIN_SETTABLE_ACCOUNT_STATUSES = exports.ACCOUNT_STATUSES = exports.ROLES = exports.APP_ENVIRONMENTS = void 0;
6
6
  exports.APP_ENVIRONMENTS = [
7
7
  'development',
8
8
  'staging',
@@ -60,3 +60,58 @@ exports.DELIVERY_STATUSES = [
60
60
  ];
61
61
  /** Only one version of a template per message type and channel is active. */
62
62
  exports.TEMPLATE_STATUSES = ['draft', 'active', 'archived'];
63
+ /** Who put a food into the library. */
64
+ exports.FOOD_SOURCES = [
65
+ 'ADMIN',
66
+ 'COACH',
67
+ 'CLIENT_SUGGESTION',
68
+ 'IMPORT',
69
+ ];
70
+ /**
71
+ * The unit a library serving is measured in. Nutrients are stored per 100 of
72
+ * this unit, so a food in `ml` holds values per 100 ml.
73
+ */
74
+ exports.SERVING_UNITS = ['g', 'ml'];
75
+ /** The portion units a client can pick when logging a food. */
76
+ exports.PORTION_UNITS = ['BOWL', 'GRAM', 'PIECE', 'CUP'];
77
+ /** Whether submitted nutrient values are per 100 units or per serving. */
78
+ exports.NUTRITION_BASES = ['PER_100', 'PER_SERVING'];
79
+ exports.FOOD_SUGGESTION_STATUSES = [
80
+ 'pending',
81
+ 'approved',
82
+ 'rejected',
83
+ 'withdrawn',
84
+ ];
85
+ /** How an approved suggestion was resolved. */
86
+ exports.FOOD_SUGGESTION_RESOLUTIONS = [
87
+ 'ADDED_TO_LIBRARY',
88
+ 'LINKED_TO_EXISTING',
89
+ 'APPROVED_WITHOUT_LIBRARY',
90
+ ];
91
+ /** What a bulk import does with a row whose food is already in the library. */
92
+ exports.FOOD_IMPORT_MODES = [
93
+ 'SKIP_DUPLICATES',
94
+ 'UPDATE_EXISTING',
95
+ ];
96
+ exports.FOOD_IMPORT_STATUSES = [
97
+ 'previewed',
98
+ 'completed',
99
+ 'expired',
100
+ ];
101
+ exports.FOOD_IMPORT_ROW_STATUSES = [
102
+ 'valid',
103
+ 'invalid',
104
+ 'duplicate_in_file',
105
+ 'duplicate_existing',
106
+ ];
107
+ exports.FOOD_IMPORT_FILE_TYPES = ['csv', 'xlsx'];
108
+ exports.FOOD_IMPORT_ISSUE_SEVERITIES = ['error', 'warning'];
109
+ /** Whether a food takes one value of a metadata type or several. */
110
+ exports.FOOD_METADATA_SELECTIONS = ['single', 'multiple'];
111
+ exports.FOOD_SORT_FIELDS = [
112
+ 'name',
113
+ 'updatedAt',
114
+ 'calories',
115
+ 'protein',
116
+ ];
117
+ exports.SORT_ORDERS = ['asc', 'desc'];
@@ -0,0 +1,49 @@
1
+ import { z } from 'zod';
2
+ export declare const MIN_CLIENT_FOOD_SEARCH_LENGTH = 2;
3
+ export declare const clientFoodSearchQuerySchema: z.ZodObject<{
4
+ page: z.ZodDefault<z.ZodCoercedNumber<unknown>>;
5
+ pageSize: z.ZodDefault<z.ZodCoercedNumber<unknown>>;
6
+ search: z.ZodString;
7
+ }, z.core.$strip>;
8
+ export type ClientFoodSearchQuery = z.output<typeof clientFoodSearchQuerySchema>;
9
+ /** A library food as a client sees it: what they need to log it, and nothing about who added it. */
10
+ export declare const clientFoodResponseSchema: z.ZodObject<{
11
+ id: z.ZodUUID;
12
+ name: z.ZodString;
13
+ nameHi: z.ZodNullable<z.ZodString>;
14
+ namePa: z.ZodNullable<z.ZodString>;
15
+ englishAlias: z.ZodNullable<z.ZodString>;
16
+ serving: z.ZodObject<{
17
+ label: z.ZodString;
18
+ quantity: z.ZodNumber;
19
+ unit: z.ZodEnum<{
20
+ g: "g";
21
+ ml: "ml";
22
+ }>;
23
+ }, z.core.$strip>;
24
+ per100: z.ZodObject<{
25
+ calories: z.ZodNumber;
26
+ protein: z.ZodNumber;
27
+ carbs: z.ZodNumber;
28
+ fat: z.ZodNumber;
29
+ sugar: z.ZodNullable<z.ZodNumber>;
30
+ fiber: z.ZodNullable<z.ZodNumber>;
31
+ solubleFiber: z.ZodNullable<z.ZodNumber>;
32
+ insolubleFiber: z.ZodNullable<z.ZodNumber>;
33
+ }, z.core.$strip>;
34
+ perServing: z.ZodObject<{
35
+ calories: z.ZodNumber;
36
+ protein: z.ZodNumber;
37
+ carbs: z.ZodNumber;
38
+ fat: z.ZodNumber;
39
+ sugar: z.ZodNullable<z.ZodNumber>;
40
+ fiber: z.ZodNullable<z.ZodNumber>;
41
+ solubleFiber: z.ZodNullable<z.ZodNumber>;
42
+ insolubleFiber: z.ZodNullable<z.ZodNumber>;
43
+ }, z.core.$strip>;
44
+ tags: z.ZodArray<z.ZodObject<{
45
+ typeKey: z.ZodString;
46
+ label: z.ZodString;
47
+ }, z.core.$strip>>;
48
+ }, z.core.$strip>;
49
+ export type ClientFoodResponse = z.output<typeof clientFoodResponseSchema>;
@@ -0,0 +1,26 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.clientFoodResponseSchema = exports.clientFoodSearchQuerySchema = exports.MIN_CLIENT_FOOD_SEARCH_LENGTH = void 0;
4
+ const zod_1 = require("zod");
5
+ const pagination_schema_js_1 = require("../common/pagination.schema.js");
6
+ const food_dto_js_1 = require("./food.dto.js");
7
+ exports.MIN_CLIENT_FOOD_SEARCH_LENGTH = 2;
8
+ exports.clientFoodSearchQuerySchema = pagination_schema_js_1.paginationQuerySchema.extend({
9
+ search: zod_1.z
10
+ .string()
11
+ .trim()
12
+ .min(exports.MIN_CLIENT_FOOD_SEARCH_LENGTH)
13
+ .max(food_dto_js_1.MAX_FOOD_SEARCH_LENGTH),
14
+ });
15
+ /** A library food as a client sees it: what they need to log it, and nothing about who added it. */
16
+ exports.clientFoodResponseSchema = zod_1.z.object({
17
+ id: zod_1.z.uuid(),
18
+ name: zod_1.z.string(),
19
+ nameHi: zod_1.z.string().nullable(),
20
+ namePa: zod_1.z.string().nullable(),
21
+ englishAlias: zod_1.z.string().nullable(),
22
+ serving: food_dto_js_1.servingResponseSchema,
23
+ per100: food_dto_js_1.nutrientValuesResponseSchema,
24
+ perServing: food_dto_js_1.nutrientValuesResponseSchema,
25
+ tags: zod_1.z.array(zod_1.z.object({ typeKey: zod_1.z.string(), label: zod_1.z.string() })),
26
+ });
@@ -0,0 +1,160 @@
1
+ import { z } from 'zod';
2
+ /** The file limits the API enforces, shared so the upload screen can say them up front. */
3
+ export declare const FOOD_IMPORT_LIMITS: {
4
+ readonly maxFileBytes: number;
5
+ readonly maxRows: 5000;
6
+ readonly extensions: readonly [".csv", ".xlsx"];
7
+ };
8
+ export declare const importIdParamSchema: z.ZodUUID;
9
+ /**
10
+ * One spreadsheet row after its cells are read. Values per 100 g (or per 100 ml
11
+ * for a serving in ml). Metadata are keys such as `protein` or `high-protein`,
12
+ * matched to the library's values. Has no defaults, so it also serves as the
13
+ * preview response.
14
+ */
15
+ export declare const foodImportRowSchema: z.ZodObject<{
16
+ name: z.ZodString;
17
+ nameHi: z.ZodNullable<z.ZodString>;
18
+ namePa: z.ZodNullable<z.ZodString>;
19
+ englishAlias: z.ZodNullable<z.ZodString>;
20
+ category: z.ZodString;
21
+ subcategory: z.ZodNullable<z.ZodString>;
22
+ servingLabel: z.ZodString;
23
+ servingQuantity: z.ZodNumber;
24
+ servingUnit: z.ZodEnum<{
25
+ g: "g";
26
+ ml: "ml";
27
+ }>;
28
+ calories: z.ZodNumber;
29
+ protein: z.ZodNumber;
30
+ carbs: z.ZodNumber;
31
+ fat: z.ZodNumber;
32
+ sugar: z.ZodNullable<z.ZodNumber>;
33
+ fiber: z.ZodNullable<z.ZodNumber>;
34
+ solubleFiber: z.ZodNullable<z.ZodNumber>;
35
+ insolubleFiber: z.ZodNullable<z.ZodNumber>;
36
+ tags: z.ZodArray<z.ZodString>;
37
+ mealTimings: z.ZodArray<z.ZodString>;
38
+ notes: z.ZodNullable<z.ZodString>;
39
+ }, z.core.$strict>;
40
+ export type FoodImportRow = z.output<typeof foodImportRowSchema>;
41
+ export declare const commitFoodImportSchema: z.ZodObject<{
42
+ mode: z.ZodDefault<z.ZodEnum<{
43
+ SKIP_DUPLICATES: "SKIP_DUPLICATES";
44
+ UPDATE_EXISTING: "UPDATE_EXISTING";
45
+ }>>;
46
+ createMissingMetadata: z.ZodDefault<z.ZodBoolean>;
47
+ }, z.core.$strict>;
48
+ export type CommitFoodImportInput = z.input<typeof commitFoodImportSchema>;
49
+ export type CommitFoodImportBody = z.output<typeof commitFoodImportSchema>;
50
+ export declare const foodImportListQuerySchema: z.ZodObject<{
51
+ page: z.ZodDefault<z.ZodCoercedNumber<unknown>>;
52
+ pageSize: z.ZodDefault<z.ZodCoercedNumber<unknown>>;
53
+ }, z.core.$strip>;
54
+ export declare const foodImportRowsQuerySchema: z.ZodObject<{
55
+ page: z.ZodDefault<z.ZodCoercedNumber<unknown>>;
56
+ pageSize: z.ZodDefault<z.ZodCoercedNumber<unknown>>;
57
+ status: z.ZodOptional<z.ZodEnum<{
58
+ valid: "valid";
59
+ invalid: "invalid";
60
+ duplicate_in_file: "duplicate_in_file";
61
+ duplicate_existing: "duplicate_existing";
62
+ }>>;
63
+ }, z.core.$strip>;
64
+ export type FoodImportRowsQuery = z.output<typeof foodImportRowsQuerySchema>;
65
+ export declare const foodImportIssueSchema: z.ZodObject<{
66
+ column: z.ZodNullable<z.ZodString>;
67
+ code: z.ZodString;
68
+ message: z.ZodString;
69
+ severity: z.ZodEnum<{
70
+ error: "error";
71
+ warning: "warning";
72
+ }>;
73
+ }, z.core.$strip>;
74
+ export type FoodImportIssue = z.output<typeof foodImportIssueSchema>;
75
+ export declare const foodImportBatchResponseSchema: z.ZodObject<{
76
+ id: z.ZodUUID;
77
+ fileName: z.ZodString;
78
+ fileType: z.ZodEnum<{
79
+ csv: "csv";
80
+ xlsx: "xlsx";
81
+ }>;
82
+ sheetName: z.ZodNullable<z.ZodString>;
83
+ status: z.ZodEnum<{
84
+ previewed: "previewed";
85
+ completed: "completed";
86
+ expired: "expired";
87
+ }>;
88
+ mode: z.ZodNullable<z.ZodEnum<{
89
+ SKIP_DUPLICATES: "SKIP_DUPLICATES";
90
+ UPDATE_EXISTING: "UPDATE_EXISTING";
91
+ }>>;
92
+ createMissingMetadata: z.ZodBoolean;
93
+ totalRows: z.ZodNumber;
94
+ validRows: z.ZodNumber;
95
+ invalidRows: z.ZodNumber;
96
+ duplicateRows: z.ZodNumber;
97
+ emptyRows: z.ZodNumber;
98
+ createdCount: z.ZodNumber;
99
+ updatedCount: z.ZodNumber;
100
+ skippedCount: z.ZodNumber;
101
+ unknownMetadata: z.ZodArray<z.ZodObject<{
102
+ typeKey: z.ZodString;
103
+ values: z.ZodArray<z.ZodString>;
104
+ }, z.core.$strip>>;
105
+ ignoredColumns: z.ZodArray<z.ZodString>;
106
+ ignoredSheets: z.ZodArray<z.ZodString>;
107
+ uploadedBy: z.ZodUUID;
108
+ expiresAt: z.ZodISODateTime;
109
+ committedAt: z.ZodNullable<z.ZodISODateTime>;
110
+ createdAt: z.ZodISODateTime;
111
+ }, z.core.$strip>;
112
+ export type FoodImportBatchResponse = z.output<typeof foodImportBatchResponseSchema>;
113
+ export declare const foodImportRowResponseSchema: z.ZodObject<{
114
+ id: z.ZodUUID;
115
+ rowNumber: z.ZodNumber;
116
+ status: z.ZodEnum<{
117
+ valid: "valid";
118
+ invalid: "invalid";
119
+ duplicate_in_file: "duplicate_in_file";
120
+ duplicate_existing: "duplicate_existing";
121
+ }>;
122
+ raw: z.ZodRecord<z.ZodString, z.ZodString>;
123
+ parsed: z.ZodNullable<z.ZodObject<{
124
+ name: z.ZodString;
125
+ nameHi: z.ZodNullable<z.ZodString>;
126
+ namePa: z.ZodNullable<z.ZodString>;
127
+ englishAlias: z.ZodNullable<z.ZodString>;
128
+ category: z.ZodString;
129
+ subcategory: z.ZodNullable<z.ZodString>;
130
+ servingLabel: z.ZodString;
131
+ servingQuantity: z.ZodNumber;
132
+ servingUnit: z.ZodEnum<{
133
+ g: "g";
134
+ ml: "ml";
135
+ }>;
136
+ calories: z.ZodNumber;
137
+ protein: z.ZodNumber;
138
+ carbs: z.ZodNumber;
139
+ fat: z.ZodNumber;
140
+ sugar: z.ZodNullable<z.ZodNumber>;
141
+ fiber: z.ZodNullable<z.ZodNumber>;
142
+ solubleFiber: z.ZodNullable<z.ZodNumber>;
143
+ insolubleFiber: z.ZodNullable<z.ZodNumber>;
144
+ tags: z.ZodArray<z.ZodString>;
145
+ mealTimings: z.ZodArray<z.ZodString>;
146
+ notes: z.ZodNullable<z.ZodString>;
147
+ }, z.core.$strict>>;
148
+ issues: z.ZodArray<z.ZodObject<{
149
+ column: z.ZodNullable<z.ZodString>;
150
+ code: z.ZodString;
151
+ message: z.ZodString;
152
+ severity: z.ZodEnum<{
153
+ error: "error";
154
+ warning: "warning";
155
+ }>;
156
+ }, z.core.$strip>>;
157
+ matchedFoodId: z.ZodNullable<z.ZodUUID>;
158
+ resultFoodId: z.ZodNullable<z.ZodUUID>;
159
+ }, z.core.$strip>;
160
+ export type FoodImportRowResponse = z.output<typeof foodImportRowResponseSchema>;
@@ -0,0 +1,110 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.foodImportRowResponseSchema = exports.foodImportBatchResponseSchema = exports.foodImportIssueSchema = exports.foodImportRowsQuerySchema = exports.foodImportListQuerySchema = exports.commitFoodImportSchema = exports.foodImportRowSchema = exports.importIdParamSchema = exports.FOOD_IMPORT_LIMITS = 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.dto.js");
10
+ const nutrition_js_1 = require("./nutrition.js");
11
+ /** The file limits the API enforces, shared so the upload screen can say them up front. */
12
+ exports.FOOD_IMPORT_LIMITS = {
13
+ maxFileBytes: 2 * 1024 * 1024,
14
+ maxRows: 5000,
15
+ extensions: ['.csv', '.xlsx'],
16
+ };
17
+ exports.importIdParamSchema = zod_1.z.uuid();
18
+ const metadataKeyListSchema = zod_1.z.array(zod_1.z.string().min(1).max(60)).max(50);
19
+ /**
20
+ * One spreadsheet row after its cells are read. Values per 100 g (or per 100 ml
21
+ * for a serving in ml). Metadata are keys such as `protein` or `high-protein`,
22
+ * matched to the library's values. Has no defaults, so it also serves as the
23
+ * preview response.
24
+ */
25
+ exports.foodImportRowSchema = zod_1.z
26
+ .strictObject({
27
+ name: (0, safe_text_schema_js_1.safeTextSchema)(food_dto_js_1.MAX_FOOD_NAME_LENGTH),
28
+ nameHi: (0, safe_text_schema_js_1.safeTextSchema)(food_dto_js_1.MAX_FOOD_NAME_LENGTH).nullable(),
29
+ namePa: (0, safe_text_schema_js_1.safeTextSchema)(food_dto_js_1.MAX_FOOD_NAME_LENGTH).nullable(),
30
+ englishAlias: (0, safe_text_schema_js_1.safeTextSchema)(200).nullable(),
31
+ category: zod_1.z.string().min(1).max(60),
32
+ subcategory: zod_1.z.string().min(1).max(60).nullable(),
33
+ servingLabel: (0, safe_text_schema_js_1.safeTextSchema)(food_dto_js_1.MAX_FOOD_NAME_LENGTH),
34
+ servingQuantity: (0, nutrition_js_1.quantitySchema)(),
35
+ servingUnit: zod_1.z.enum(index_js_1.SERVING_UNITS),
36
+ calories: (0, nutrition_js_1.nutrientAmountSchema)(),
37
+ protein: (0, nutrition_js_1.nutrientAmountSchema)(),
38
+ carbs: (0, nutrition_js_1.nutrientAmountSchema)(),
39
+ fat: (0, nutrition_js_1.nutrientAmountSchema)(),
40
+ sugar: (0, nutrition_js_1.nutrientAmountSchema)().nullable(),
41
+ fiber: (0, nutrition_js_1.nutrientAmountSchema)().nullable(),
42
+ solubleFiber: (0, nutrition_js_1.nutrientAmountSchema)().nullable(),
43
+ insolubleFiber: (0, nutrition_js_1.nutrientAmountSchema)().nullable(),
44
+ tags: metadataKeyListSchema,
45
+ mealTimings: metadataKeyListSchema,
46
+ notes: (0, safe_text_schema_js_1.safeTextSchema)(food_dto_js_1.MAX_FOOD_NOTES_LENGTH).nullable(),
47
+ })
48
+ .superRefine((row, ctx) => {
49
+ for (const issue of (0, nutrition_js_1.nutritionIssues)(row)) {
50
+ ctx.addIssue({
51
+ code: 'custom',
52
+ message: issue.message,
53
+ path: [issue.path],
54
+ });
55
+ }
56
+ });
57
+ exports.commitFoodImportSchema = zod_1.z.strictObject({
58
+ mode: zod_1.z.enum(index_js_1.FOOD_IMPORT_MODES).default('SKIP_DUPLICATES'),
59
+ /** Create categories, tags and other values the file uses but the library lacks. */
60
+ createMissingMetadata: zod_1.z.boolean().default(false),
61
+ });
62
+ exports.foodImportListQuerySchema = pagination_schema_js_1.paginationQuerySchema;
63
+ exports.foodImportRowsQuerySchema = pagination_schema_js_1.paginationQuerySchema.extend({
64
+ status: zod_1.z.enum(index_js_1.FOOD_IMPORT_ROW_STATUSES).optional(),
65
+ });
66
+ // Responses
67
+ exports.foodImportIssueSchema = zod_1.z.object({
68
+ /** The spreadsheet column, or null for a problem with the whole row. */
69
+ column: zod_1.z.string().nullable(),
70
+ code: zod_1.z.string(),
71
+ message: zod_1.z.string(),
72
+ severity: zod_1.z.enum(index_js_1.FOOD_IMPORT_ISSUE_SEVERITIES),
73
+ });
74
+ exports.foodImportBatchResponseSchema = zod_1.z.object({
75
+ id: zod_1.z.uuid(),
76
+ fileName: zod_1.z.string(),
77
+ fileType: zod_1.z.enum(index_js_1.FOOD_IMPORT_FILE_TYPES),
78
+ sheetName: zod_1.z.string().nullable(),
79
+ status: zod_1.z.enum(index_js_1.FOOD_IMPORT_STATUSES),
80
+ mode: zod_1.z.enum(index_js_1.FOOD_IMPORT_MODES).nullable(),
81
+ createMissingMetadata: zod_1.z.boolean(),
82
+ totalRows: zod_1.z.number().int(),
83
+ validRows: zod_1.z.number().int(),
84
+ invalidRows: zod_1.z.number().int(),
85
+ duplicateRows: zod_1.z.number().int(),
86
+ emptyRows: zod_1.z.number().int(),
87
+ createdCount: zod_1.z.number().int(),
88
+ updatedCount: zod_1.z.number().int(),
89
+ skippedCount: zod_1.z.number().int(),
90
+ /** Values the file uses that the library does not have yet, by type key. */
91
+ unknownMetadata: zod_1.z.array(zod_1.z.object({ typeKey: zod_1.z.string(), values: zod_1.z.array(zod_1.z.string()) })),
92
+ ignoredColumns: zod_1.z.array(zod_1.z.string()),
93
+ ignoredSheets: zod_1.z.array(zod_1.z.string()),
94
+ uploadedBy: zod_1.z.uuid(),
95
+ expiresAt: date_time_schema_js_1.utcTimestampSchema,
96
+ committedAt: date_time_schema_js_1.utcTimestampSchema.nullable(),
97
+ createdAt: date_time_schema_js_1.utcTimestampSchema,
98
+ });
99
+ exports.foodImportRowResponseSchema = zod_1.z.object({
100
+ id: zod_1.z.uuid(),
101
+ /** The row number as the spreadsheet shows it, counting the header as row 1. */
102
+ rowNumber: zod_1.z.number().int(),
103
+ status: zod_1.z.enum(index_js_1.FOOD_IMPORT_ROW_STATUSES),
104
+ /** The cells as read, by header. */
105
+ raw: zod_1.z.record(zod_1.z.string(), zod_1.z.string()),
106
+ parsed: exports.foodImportRowSchema.nullable(),
107
+ issues: zod_1.z.array(exports.foodImportIssueSchema),
108
+ matchedFoodId: zod_1.z.uuid().nullable(),
109
+ resultFoodId: zod_1.z.uuid().nullable(),
110
+ });