@cravery/core 0.0.1 → 0.0.3
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/config/ai.d.ts +3 -0
- package/dist/config/ai.d.ts.map +1 -0
- package/dist/config/ai.js +36 -0
- package/dist/config/ai.js.map +1 -0
- package/dist/config/collections.d.ts +8 -0
- package/dist/config/collections.d.ts.map +1 -0
- package/dist/config/collections.js +11 -0
- package/dist/config/collections.js.map +1 -0
- package/dist/config/common.d.ts +2 -0
- package/dist/config/common.d.ts.map +1 -0
- package/dist/config/common.js +5 -0
- package/dist/config/common.js.map +1 -0
- package/dist/config/credits.d.ts +2 -0
- package/dist/config/credits.d.ts.map +1 -0
- package/dist/config/credits.js +5 -0
- package/dist/config/credits.js.map +1 -0
- package/dist/config/index.d.ts +4 -0
- package/dist/config/index.d.ts.map +1 -0
- package/dist/config/index.js +20 -0
- package/dist/config/index.js.map +1 -0
- package/dist/enums/allergen.d.ts +19 -0
- package/dist/enums/allergen.d.ts.map +1 -0
- package/dist/enums/allergen.js +21 -0
- package/dist/enums/allergen.js.map +1 -0
- package/dist/enums/cuisine.d.ts +39 -0
- package/dist/enums/cuisine.d.ts.map +1 -0
- package/dist/enums/cuisine.js +41 -0
- package/dist/enums/cuisine.js.map +1 -0
- package/dist/enums/dietary_tag.d.ts +21 -0
- package/dist/enums/dietary_tag.d.ts.map +1 -0
- package/dist/enums/dietary_tag.js +23 -0
- package/dist/enums/dietary_tag.js.map +1 -0
- package/dist/enums/difficulty.d.ts +10 -0
- package/dist/enums/difficulty.d.ts.map +1 -0
- package/dist/enums/difficulty.js +12 -0
- package/dist/enums/difficulty.js.map +1 -0
- package/dist/enums/image_type.d.ts +8 -0
- package/dist/enums/image_type.d.ts.map +1 -0
- package/dist/enums/image_type.js +7 -0
- package/dist/enums/image_type.js.map +1 -0
- package/dist/enums/index.d.ts +22 -0
- package/dist/enums/index.d.ts.map +1 -0
- package/dist/enums/index.js +38 -0
- package/dist/enums/index.js.map +1 -0
- package/dist/enums/locale.d.ts +11 -0
- package/dist/enums/locale.d.ts.map +1 -0
- package/dist/enums/locale.js +7 -0
- package/dist/enums/locale.js.map +1 -0
- package/dist/enums/meal_type.d.ts +14 -0
- package/dist/enums/meal_type.d.ts.map +1 -0
- package/dist/enums/meal_type.js +16 -0
- package/dist/enums/meal_type.js.map +1 -0
- package/dist/enums/moderation_status.d.ts +9 -0
- package/dist/enums/moderation_status.d.ts.map +1 -0
- package/dist/enums/moderation_status.js +11 -0
- package/dist/enums/moderation_status.js.map +1 -0
- package/dist/enums/priority.d.ts +9 -0
- package/dist/enums/priority.d.ts.map +1 -0
- package/dist/enums/priority.js +7 -0
- package/dist/enums/priority.js.map +1 -0
- package/dist/enums/profile_status.d.ts +10 -0
- package/dist/enums/profile_status.d.ts.map +1 -0
- package/dist/enums/profile_status.js +12 -0
- package/dist/enums/profile_status.js.map +1 -0
- package/dist/enums/recipe_source.d.ts +10 -0
- package/dist/enums/recipe_source.d.ts.map +1 -0
- package/dist/enums/recipe_source.js +7 -0
- package/dist/enums/recipe_source.js.map +1 -0
- package/dist/enums/recipe_status.d.ts +10 -0
- package/dist/enums/recipe_status.d.ts.map +1 -0
- package/dist/enums/recipe_status.js +12 -0
- package/dist/enums/recipe_status.js.map +1 -0
- package/dist/enums/role.d.ts +11 -0
- package/dist/enums/role.d.ts.map +1 -0
- package/dist/enums/role.js +13 -0
- package/dist/enums/role.js.map +1 -0
- package/dist/enums/severity.d.ts +9 -0
- package/dist/enums/severity.d.ts.map +1 -0
- package/dist/enums/severity.js +7 -0
- package/dist/enums/severity.js.map +1 -0
- package/dist/enums/spiciness.d.ts +10 -0
- package/dist/enums/spiciness.d.ts.map +1 -0
- package/dist/enums/spiciness.js +7 -0
- package/dist/enums/spiciness.js.map +1 -0
- package/dist/enums/status.d.ts +11 -0
- package/dist/enums/status.d.ts.map +1 -0
- package/dist/enums/status.js +13 -0
- package/dist/enums/status.js.map +1 -0
- package/dist/enums/subscription_tier.d.ts +8 -0
- package/dist/enums/subscription_tier.d.ts.map +1 -0
- package/dist/enums/subscription_tier.js +6 -0
- package/dist/enums/subscription_tier.js.map +1 -0
- package/dist/enums/suggestion_category.d.ts +11 -0
- package/dist/enums/suggestion_category.d.ts.map +1 -0
- package/dist/enums/suggestion_category.js +13 -0
- package/dist/enums/suggestion_category.js.map +1 -0
- package/dist/enums/temperature_unit.d.ts +8 -0
- package/dist/enums/temperature_unit.d.ts.map +1 -0
- package/dist/enums/temperature_unit.js +7 -0
- package/dist/enums/temperature_unit.js.map +1 -0
- package/dist/enums/unit.d.ts +43 -0
- package/dist/enums/unit.d.ts.map +1 -0
- package/dist/enums/unit.js +53 -0
- package/dist/enums/unit.js.map +1 -0
- package/dist/enums/url_type.d.ts +13 -0
- package/dist/enums/url_type.d.ts.map +1 -0
- package/dist/enums/url_type.js +15 -0
- package/dist/enums/url_type.js.map +1 -0
- package/dist/enums/user_status.d.ts +10 -0
- package/dist/enums/user_status.d.ts.map +1 -0
- package/dist/enums/user_status.js +12 -0
- package/dist/enums/user_status.js.map +1 -0
- package/dist/index.d.ts +4 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +18 -0
- package/dist/index.js.map +1 -1
- package/dist/lib/api.d.ts +18 -0
- package/dist/lib/api.d.ts.map +1 -0
- package/dist/lib/api.js +69 -0
- package/dist/lib/api.js.map +1 -0
- package/dist/lib/cost.d.ts +22 -0
- package/dist/lib/cost.d.ts.map +1 -0
- package/dist/lib/cost.js +45 -0
- package/dist/lib/cost.js.map +1 -0
- package/dist/lib/embedding.d.ts +4 -0
- package/dist/lib/embedding.d.ts.map +1 -0
- package/dist/lib/embedding.js +48 -0
- package/dist/lib/embedding.js.map +1 -0
- package/dist/lib/firebase.d.ts +3 -0
- package/dist/lib/firebase.d.ts.map +1 -0
- package/dist/lib/firebase.js +8 -0
- package/dist/lib/firebase.js.map +1 -0
- package/dist/lib/flow.d.ts +53 -0
- package/dist/lib/flow.d.ts.map +1 -0
- package/dist/lib/flow.js +60 -0
- package/dist/lib/flow.js.map +1 -0
- package/dist/lib/genkit.d.ts +4 -0
- package/dist/lib/genkit.d.ts.map +1 -0
- package/dist/lib/genkit.js +16 -0
- package/dist/lib/genkit.js.map +1 -0
- package/dist/lib/iam.d.ts +3 -0
- package/dist/lib/iam.d.ts.map +1 -0
- package/dist/lib/iam.js +57 -0
- package/dist/lib/iam.js.map +1 -0
- package/dist/lib/image.d.ts +7 -0
- package/dist/lib/image.d.ts.map +1 -0
- package/dist/lib/image.js +24 -0
- package/dist/lib/image.js.map +1 -0
- package/dist/lib/index.d.ts +10 -0
- package/dist/lib/index.d.ts.map +1 -0
- package/dist/lib/index.js +26 -0
- package/dist/lib/index.js.map +1 -0
- package/dist/lib/storage.d.ts +6 -0
- package/dist/lib/storage.d.ts.map +1 -0
- package/dist/lib/storage.js +34 -0
- package/dist/lib/storage.js.map +1 -0
- package/dist/types/ai/config.d.ts +20 -0
- package/dist/types/ai/config.d.ts.map +1 -0
- package/dist/types/ai/config.js +3 -0
- package/dist/types/ai/config.js.map +1 -0
- package/dist/types/ai/filters.d.ts +82 -0
- package/dist/types/ai/filters.d.ts.map +1 -0
- package/dist/types/ai/filters.js +16 -0
- package/dist/types/ai/filters.js.map +1 -0
- package/dist/types/ai/index.d.ts +5 -0
- package/dist/types/ai/index.d.ts.map +1 -0
- package/dist/types/ai/index.js +21 -0
- package/dist/types/ai/index.js.map +1 -0
- package/dist/types/ai/recipe.d.ts +179 -0
- package/dist/types/ai/recipe.d.ts.map +1 -0
- package/dist/types/ai/recipe.js +28 -0
- package/dist/types/ai/recipe.js.map +1 -0
- package/dist/types/ai/translation.d.ts +26 -0
- package/dist/types/ai/translation.d.ts.map +1 -0
- package/dist/types/ai/translation.js +16 -0
- package/dist/types/ai/translation.js.map +1 -0
- package/dist/types/equipment.d.ts +20 -0
- package/dist/types/equipment.d.ts.map +1 -0
- package/dist/types/equipment.js +16 -0
- package/dist/types/equipment.js.map +1 -0
- package/dist/types/error.d.ts +14 -0
- package/dist/types/error.d.ts.map +1 -0
- package/dist/types/error.js +40 -0
- package/dist/types/error.js.map +1 -0
- package/dist/types/index.d.ts +13 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +29 -0
- package/dist/types/index.js.map +1 -0
- package/dist/types/ingredient.d.ts +209 -0
- package/dist/types/ingredient.d.ts.map +1 -0
- package/dist/types/ingredient.js +33 -0
- package/dist/types/ingredient.js.map +1 -0
- package/dist/types/instruction.d.ts +32 -0
- package/dist/types/instruction.d.ts.map +1 -0
- package/dist/types/instruction.js +16 -0
- package/dist/types/instruction.js.map +1 -0
- package/dist/types/moderation.d.ts +87 -0
- package/dist/types/moderation.d.ts.map +1 -0
- package/dist/types/moderation.js +38 -0
- package/dist/types/moderation.js.map +1 -0
- package/dist/types/nutrition.d.ts +18 -0
- package/dist/types/nutrition.d.ts.map +1 -0
- package/dist/types/nutrition.js +20 -0
- package/dist/types/nutrition.js.map +1 -0
- package/dist/types/profile.d.ts +30 -0
- package/dist/types/profile.d.ts.map +1 -0
- package/dist/types/profile.js +26 -0
- package/dist/types/profile.js.map +1 -0
- package/dist/types/recipe.d.ts +448 -0
- package/dist/types/recipe.d.ts.map +1 -0
- package/dist/types/recipe.js +82 -0
- package/dist/types/recipe.js.map +1 -0
- package/dist/types/subscription.d.ts +16 -0
- package/dist/types/subscription.d.ts.map +1 -0
- package/dist/types/subscription.js +14 -0
- package/dist/types/subscription.js.map +1 -0
- package/dist/types/temperature.d.ts +10 -0
- package/dist/types/temperature.d.ts.map +1 -0
- package/dist/types/temperature.js +10 -0
- package/dist/types/temperature.js.map +1 -0
- package/dist/types/user.d.ts +26 -0
- package/dist/types/user.d.ts.map +1 -0
- package/dist/types/user.js +17 -0
- package/dist/types/user.js.map +1 -0
- package/package.json +3 -1
- package/src/config/ai.ts +34 -0
- package/src/config/collections.ts +7 -0
- package/src/config/common.ts +1 -0
- package/src/config/index.ts +3 -0
- package/src/enums/allergen.ts +19 -0
- package/src/enums/cuisine.ts +39 -0
- package/src/enums/dietary_tag.ts +21 -0
- package/src/enums/difficulty.ts +10 -0
- package/src/enums/image_type.ts +5 -0
- package/src/enums/index.ts +21 -0
- package/src/enums/locale.ts +5 -0
- package/src/enums/meal_type.ts +14 -0
- package/src/enums/moderation_status.ts +9 -0
- package/src/enums/priority.ts +5 -0
- package/src/enums/profile_status.ts +10 -0
- package/src/enums/recipe_source.ts +5 -0
- package/src/enums/recipe_status.ts +10 -0
- package/src/enums/role.ts +11 -0
- package/src/enums/severity.ts +5 -0
- package/src/enums/spiciness.ts +5 -0
- package/src/enums/status.ts +11 -0
- package/src/enums/subscription_tier.ts +4 -0
- package/src/enums/suggestion_category.ts +11 -0
- package/src/enums/temperature_unit.ts +5 -0
- package/src/enums/unit.ts +51 -0
- package/src/enums/url_type.ts +13 -0
- package/src/enums/user_status.ts +10 -0
- package/src/index.ts +4 -0
- package/src/lib/api.ts +90 -0
- package/src/lib/cost.ts +72 -0
- package/src/lib/embedding.ts +53 -0
- package/src/lib/firebase.ts +5 -0
- package/src/lib/flow.ts +88 -0
- package/src/lib/genkit.ts +18 -0
- package/src/lib/iam.ts +22 -0
- package/src/lib/image.ts +28 -0
- package/src/lib/index.ts +9 -0
- package/src/lib/storage.ts +42 -0
- package/src/types/ai/config.ts +27 -0
- package/src/types/ai/filters.ts +20 -0
- package/src/types/ai/index.ts +4 -0
- package/src/types/ai/recipe.ts +33 -0
- package/src/types/ai/translation.ts +14 -0
- package/src/types/equipment.ts +21 -0
- package/src/types/error.ts +43 -0
- package/src/types/index.ts +12 -0
- package/src/types/ingredient.ts +46 -0
- package/src/types/instruction.ts +21 -0
- package/src/types/moderation.ts +48 -0
- package/src/types/nutrition.ts +18 -0
- package/src/types/profile.ts +27 -0
- package/src/types/recipe.ts +109 -0
- package/src/types/subscription.ts +13 -0
- package/src/types/temperature.ts +8 -0
- package/src/types/user.ts +16 -0
package/src/lib/flow.ts
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { z } from "genkit";
|
|
2
|
+
import { AI_MODELS } from "../config/ai";
|
|
3
|
+
import { logAIUsage } from "./cost";
|
|
4
|
+
import { ai } from "./genkit";
|
|
5
|
+
import { uploadImageToStorage } from "./storage";
|
|
6
|
+
import { AIModelConfig } from "../types";
|
|
7
|
+
|
|
8
|
+
export const ImagenOutputSchema = z.object({
|
|
9
|
+
images: z.array(
|
|
10
|
+
z.object({
|
|
11
|
+
url: z.string(),
|
|
12
|
+
path: z.string(),
|
|
13
|
+
}),
|
|
14
|
+
),
|
|
15
|
+
});
|
|
16
|
+
export type FlowHandler<T, R> = (input: T, output: R | null) => Promise<R>;
|
|
17
|
+
export type ImageFlowHandler<T> = (
|
|
18
|
+
input: T,
|
|
19
|
+
prompt: string,
|
|
20
|
+
) => Promise<{ base64Data: string; storagePath: string }[]>;
|
|
21
|
+
|
|
22
|
+
export const createGeminiFlow = <T, R>(
|
|
23
|
+
name: string,
|
|
24
|
+
inputSchema: z.ZodSchema<T>,
|
|
25
|
+
outputSchema: z.ZodSchema<R>,
|
|
26
|
+
handler: FlowHandler<T, R>,
|
|
27
|
+
model: AIModelConfig = AI_MODELS.Gemini25Flash,
|
|
28
|
+
) => {
|
|
29
|
+
const FlowInputSchema = ai.defineSchema(`${name}InputSchema`, inputSchema);
|
|
30
|
+
const FlowOutputSchema = ai.defineSchema(`${name}OutputSchema`, outputSchema);
|
|
31
|
+
const prompt = ai.prompt<
|
|
32
|
+
typeof FlowInputSchema,
|
|
33
|
+
typeof FlowOutputSchema,
|
|
34
|
+
z.ZodTypeAny
|
|
35
|
+
>(name);
|
|
36
|
+
|
|
37
|
+
return ai.defineFlow(
|
|
38
|
+
{
|
|
39
|
+
name: name,
|
|
40
|
+
inputSchema: inputSchema,
|
|
41
|
+
outputSchema: outputSchema,
|
|
42
|
+
},
|
|
43
|
+
async (input: T) => {
|
|
44
|
+
const response = await prompt(input);
|
|
45
|
+
logAIUsage({
|
|
46
|
+
type: "multimodal",
|
|
47
|
+
flowName: name,
|
|
48
|
+
model,
|
|
49
|
+
inputTokens: response.usage?.inputTokens || 0,
|
|
50
|
+
outputTokens: response.usage?.outputTokens || 0,
|
|
51
|
+
});
|
|
52
|
+
return handler(input, response.output);
|
|
53
|
+
},
|
|
54
|
+
);
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
export const createImagenFlow = <T>(
|
|
58
|
+
name: string,
|
|
59
|
+
inputSchema: z.ZodSchema<T>,
|
|
60
|
+
handler: ImageFlowHandler<T>,
|
|
61
|
+
model: AIModelConfig = AI_MODELS.Imagen4Fast,
|
|
62
|
+
) => {
|
|
63
|
+
return ai.defineFlow(
|
|
64
|
+
{
|
|
65
|
+
name: name,
|
|
66
|
+
inputSchema: inputSchema,
|
|
67
|
+
outputSchema: ImagenOutputSchema,
|
|
68
|
+
},
|
|
69
|
+
async (input: T) => {
|
|
70
|
+
const imageData = await handler(input, name);
|
|
71
|
+
const uploadPromises = imageData.map(({ base64Data, storagePath }) =>
|
|
72
|
+
uploadImageToStorage(base64Data, storagePath).catch((err) => {
|
|
73
|
+
throw err;
|
|
74
|
+
}),
|
|
75
|
+
);
|
|
76
|
+
const uploadedImages = await Promise.all(uploadPromises);
|
|
77
|
+
logAIUsage({
|
|
78
|
+
type: "image",
|
|
79
|
+
flowName: name,
|
|
80
|
+
model,
|
|
81
|
+
imageCount: imageData.length,
|
|
82
|
+
});
|
|
83
|
+
return {
|
|
84
|
+
images: uploadedImages,
|
|
85
|
+
};
|
|
86
|
+
},
|
|
87
|
+
);
|
|
88
|
+
};
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { genkit, EmbedderReference } from "genkit";
|
|
2
|
+
import { vertexAI } from "@genkit-ai/google-genai";
|
|
3
|
+
|
|
4
|
+
const projectId =
|
|
5
|
+
process.env.GCLOUD_PROJECT || process.env.GCP_PROJECT || "canary-cravery";
|
|
6
|
+
|
|
7
|
+
export const ai = genkit({
|
|
8
|
+
plugins: [
|
|
9
|
+
vertexAI({
|
|
10
|
+
location: "us-central1",
|
|
11
|
+
projectId,
|
|
12
|
+
}),
|
|
13
|
+
],
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
export const geminiEmbedding001: EmbedderReference = vertexAI.embedder(
|
|
17
|
+
"gemini-embedding-001",
|
|
18
|
+
);
|
package/src/lib/iam.ts
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import * as admin from "firebase-admin";
|
|
2
|
+
import { Role } from "../enums";
|
|
3
|
+
|
|
4
|
+
const getUserRole = async (uid: string): Promise<Role> => {
|
|
5
|
+
let role: Role = "guest";
|
|
6
|
+
const userDoc = await admin.firestore().collection("users").doc(uid).get();
|
|
7
|
+
if (userDoc.exists) {
|
|
8
|
+
const userData = userDoc.data();
|
|
9
|
+
role = userData?.role || "user";
|
|
10
|
+
}
|
|
11
|
+
return role;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export const isAdmin = async (uid: string): Promise<boolean> => {
|
|
15
|
+
const role = await getUserRole(uid);
|
|
16
|
+
return role === "admin";
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
export const isModerator = async (uid: string): Promise<boolean> => {
|
|
20
|
+
const role = await getUserRole(uid);
|
|
21
|
+
return role === "admin" || role === "moderator";
|
|
22
|
+
};
|
package/src/lib/image.ts
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { GenerateResponse } from "genkit";
|
|
2
|
+
import { AIFlowError } from "../types";
|
|
3
|
+
|
|
4
|
+
export interface ImageData {
|
|
5
|
+
base64: string;
|
|
6
|
+
contentType: string;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export const getImageData = (response: GenerateResponse): ImageData => {
|
|
10
|
+
const media = response.media;
|
|
11
|
+
|
|
12
|
+
if (!media?.url) {
|
|
13
|
+
throw new AIFlowError("model_error", "No image data in response", [
|
|
14
|
+
"Try with a different recipe description",
|
|
15
|
+
]);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const url = media.url;
|
|
19
|
+
const contentType = media.contentType ?? "image/png";
|
|
20
|
+
|
|
21
|
+
// Extract base64 from data URI if present
|
|
22
|
+
if (url.startsWith("data:")) {
|
|
23
|
+
const base64Part = url.split(",")[1];
|
|
24
|
+
if (base64Part) return { base64: base64Part, contentType };
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
return { base64: url, contentType };
|
|
28
|
+
};
|
package/src/lib/index.ts
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { getStorage } from "firebase-admin/storage";
|
|
2
|
+
|
|
3
|
+
export interface UploadImageResult {
|
|
4
|
+
url: string;
|
|
5
|
+
path: string;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export const uploadImageToStorage = async (
|
|
9
|
+
base64Data: string,
|
|
10
|
+
path: string,
|
|
11
|
+
contentType: string = "image/png",
|
|
12
|
+
): Promise<UploadImageResult> => {
|
|
13
|
+
try {
|
|
14
|
+
const bucket = getStorage().bucket();
|
|
15
|
+
const buffer = Buffer.from(base64Data, "base64");
|
|
16
|
+
const file = bucket.file(path);
|
|
17
|
+
|
|
18
|
+
const fileExists = await file.exists();
|
|
19
|
+
if (fileExists[0]) {
|
|
20
|
+
await file.delete();
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
await file.save(buffer, {
|
|
24
|
+
metadata: {
|
|
25
|
+
contentType,
|
|
26
|
+
cacheControl: "public",
|
|
27
|
+
},
|
|
28
|
+
resumable: false,
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
const publicUrl = `https://storage.googleapis.com/${bucket.name}/${path}`;
|
|
32
|
+
|
|
33
|
+
return {
|
|
34
|
+
url: publicUrl,
|
|
35
|
+
path,
|
|
36
|
+
};
|
|
37
|
+
} catch {
|
|
38
|
+
const uploadError = new Error("Image upload failed");
|
|
39
|
+
uploadError.name = "UPLOAD_FAILED";
|
|
40
|
+
throw uploadError;
|
|
41
|
+
}
|
|
42
|
+
};
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AI model configuration, costs are in USD:
|
|
3
|
+
* - multimodal: cost per 1,000,000 input/output tokens
|
|
4
|
+
* - image: cost per image (stored in cost.output)
|
|
5
|
+
* - embedding: cost per 1,000 input tokens
|
|
6
|
+
*/
|
|
7
|
+
export type AIModel =
|
|
8
|
+
| "Gemini25Flash"
|
|
9
|
+
| "Gemini25Pro"
|
|
10
|
+
| "Gemini3Flash"
|
|
11
|
+
| "Imagen4Fast"
|
|
12
|
+
| "GeminiEmbedding001";
|
|
13
|
+
export type AIModelType = "multimodal" | "image" | "embedding";
|
|
14
|
+
|
|
15
|
+
export interface AIModelCost {
|
|
16
|
+
input: number;
|
|
17
|
+
output: number;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface AIModelConfig {
|
|
21
|
+
id: string;
|
|
22
|
+
name: string;
|
|
23
|
+
type: AIModelType;
|
|
24
|
+
cost: AIModelCost;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export type AIConfig = Record<AIModel, AIModelConfig>;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import {
|
|
3
|
+
CuisineSchema,
|
|
4
|
+
DietaryTagSchema,
|
|
5
|
+
DifficultySchema,
|
|
6
|
+
MealTypeSchema,
|
|
7
|
+
SpicinessSchema,
|
|
8
|
+
} from "../../enums";
|
|
9
|
+
|
|
10
|
+
export const RecipeFiltersSchema = z.object({
|
|
11
|
+
cuisines: z.array(CuisineSchema).optional(),
|
|
12
|
+
dietaryTags: z.array(DietaryTagSchema).optional(),
|
|
13
|
+
difficulty: DifficultySchema.optional(),
|
|
14
|
+
excludeIngredients: z.array(z.string()).optional(),
|
|
15
|
+
includeIngredients: z.array(z.string()).optional(),
|
|
16
|
+
mealTypes: z.array(MealTypeSchema).optional(),
|
|
17
|
+
spiciness: SpicinessSchema.optional(),
|
|
18
|
+
timeLimit: z.number().int().min(5).max(180).optional(),
|
|
19
|
+
});
|
|
20
|
+
export type RecipeFilters = z.infer<typeof RecipeFiltersSchema>;
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import {
|
|
3
|
+
AllergenSchema,
|
|
4
|
+
CuisineSchema,
|
|
5
|
+
DietaryTagSchema,
|
|
6
|
+
DifficultySchema,
|
|
7
|
+
MealTypeSchema,
|
|
8
|
+
SpicinessSchema,
|
|
9
|
+
} from "../../enums";
|
|
10
|
+
import { EquipmentSchema } from "../equipment";
|
|
11
|
+
import { IngredientSectionSchema } from "../ingredient";
|
|
12
|
+
import { InstructionSchema } from "../instruction";
|
|
13
|
+
import { NutritionSchema } from "../nutrition";
|
|
14
|
+
|
|
15
|
+
export const AIRecipeSchema = z.object({
|
|
16
|
+
allergens: z.array(AllergenSchema),
|
|
17
|
+
confidence: z.number().min(0).max(1),
|
|
18
|
+
cuisine: CuisineSchema,
|
|
19
|
+
description: z.string().min(10).max(2000),
|
|
20
|
+
dietaryTags: z.array(DietaryTagSchema),
|
|
21
|
+
difficulty: DifficultySchema,
|
|
22
|
+
equipment: z.array(EquipmentSchema).max(20).optional(),
|
|
23
|
+
ingredientSections: z.array(IngredientSectionSchema).min(1).max(10),
|
|
24
|
+
instructions: z.array(InstructionSchema).min(1).max(50),
|
|
25
|
+
mealTypes: z.array(MealTypeSchema).min(1),
|
|
26
|
+
nutrition: NutritionSchema.optional(),
|
|
27
|
+
servings: z.number().int().min(1).max(100),
|
|
28
|
+
spiciness: SpicinessSchema,
|
|
29
|
+
time: z.number().int().min(1).max(1440),
|
|
30
|
+
tips: z.array(z.string().max(500)).max(10).optional(),
|
|
31
|
+
title: z.string().min(3).max(200),
|
|
32
|
+
});
|
|
33
|
+
export type AIRecipe = z.infer<typeof AIRecipeSchema>;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { EquipmentContentSchema } from "../equipment";
|
|
3
|
+
import { IngredientSectionContentSchema } from "../ingredient";
|
|
4
|
+
import { InstructionContentSchema } from "../instruction";
|
|
5
|
+
|
|
6
|
+
export const AIRecipeTranslationSchema = z.object({
|
|
7
|
+
description: z.string().min(10).max(2000),
|
|
8
|
+
equipment: z.array(EquipmentContentSchema).optional(),
|
|
9
|
+
ingredientSections: z.array(IngredientSectionContentSchema).min(1),
|
|
10
|
+
instructions: z.array(InstructionContentSchema).min(1),
|
|
11
|
+
tips: z.array(z.string().max(500)).optional(),
|
|
12
|
+
title: z.string().min(3).max(200),
|
|
13
|
+
});
|
|
14
|
+
export type AIRecipeTranslation = z.infer<typeof AIRecipeTranslationSchema>;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { SlugRegex } from "../config";
|
|
3
|
+
|
|
4
|
+
export const EquipmentMetaSchema = z.object({
|
|
5
|
+
required: z.boolean().optional(),
|
|
6
|
+
slug: z.string().regex(SlugRegex),
|
|
7
|
+
});
|
|
8
|
+
export type EquipmentMeta = z.infer<typeof EquipmentMetaSchema>;
|
|
9
|
+
|
|
10
|
+
export const EquipmentContentSchema = z.object({
|
|
11
|
+
name: z.string().min(1).max(100),
|
|
12
|
+
notes: z.string().max(200).optional(),
|
|
13
|
+
slug: z.string().regex(SlugRegex),
|
|
14
|
+
});
|
|
15
|
+
export type EquipmentContent = z.infer<typeof EquipmentContentSchema>;
|
|
16
|
+
|
|
17
|
+
export const EquipmentSchema = z.object({
|
|
18
|
+
...EquipmentMetaSchema.shape,
|
|
19
|
+
...EquipmentContentSchema.shape,
|
|
20
|
+
});
|
|
21
|
+
export type Equipment = z.infer<typeof EquipmentSchema>;
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
export const AI_ERROR_CODE_VALUES = [
|
|
2
|
+
"hallucination_detected",
|
|
3
|
+
"image_unclear",
|
|
4
|
+
"impossible_constraint",
|
|
5
|
+
"invalid_input",
|
|
6
|
+
"invalid_url",
|
|
7
|
+
"low_confidence",
|
|
8
|
+
"model_error",
|
|
9
|
+
"no_recipe_found",
|
|
10
|
+
"nonsense_input",
|
|
11
|
+
"rate_limit",
|
|
12
|
+
"timeout",
|
|
13
|
+
"translation_failed",
|
|
14
|
+
"unsafe_input",
|
|
15
|
+
"url_not_accessible",
|
|
16
|
+
] as const;
|
|
17
|
+
|
|
18
|
+
export type AIErrorCode = (typeof AI_ERROR_CODE_VALUES)[number];
|
|
19
|
+
|
|
20
|
+
export class AIFlowError extends Error {
|
|
21
|
+
public readonly code: AIErrorCode;
|
|
22
|
+
public readonly suggestions?: string[];
|
|
23
|
+
|
|
24
|
+
constructor(code: AIErrorCode, message: string, suggestions?: string[]) {
|
|
25
|
+
super(message);
|
|
26
|
+
this.name = "AIFlowError";
|
|
27
|
+
this.code = code;
|
|
28
|
+
this.suggestions = suggestions;
|
|
29
|
+
|
|
30
|
+
if (Error.captureStackTrace) {
|
|
31
|
+
Error.captureStackTrace(this, AIFlowError);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
toJSON() {
|
|
36
|
+
return {
|
|
37
|
+
name: this.name,
|
|
38
|
+
code: this.code,
|
|
39
|
+
message: this.message,
|
|
40
|
+
suggestions: this.suggestions,
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export * from "./ai";
|
|
2
|
+
export * from "./equipment";
|
|
3
|
+
export * from "./error";
|
|
4
|
+
export * from "./ingredient";
|
|
5
|
+
export * from "./instruction";
|
|
6
|
+
export * from "./moderation";
|
|
7
|
+
export * from "./nutrition";
|
|
8
|
+
export * from "./profile";
|
|
9
|
+
export * from "./recipe";
|
|
10
|
+
export * from "./subscription";
|
|
11
|
+
export * from "./temperature";
|
|
12
|
+
export * from "./user";
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { SlugRegex } from "../config";
|
|
3
|
+
import { UnitSchema } from "../enums";
|
|
4
|
+
|
|
5
|
+
export const IngredientMetaSchema = z.object({
|
|
6
|
+
quantity: z.number().positive().optional(),
|
|
7
|
+
required: z.boolean().optional(),
|
|
8
|
+
slug: z.string().regex(SlugRegex),
|
|
9
|
+
unit: UnitSchema.optional(),
|
|
10
|
+
});
|
|
11
|
+
export type IngredientMeta = z.infer<typeof IngredientMetaSchema>;
|
|
12
|
+
|
|
13
|
+
export const IngredientContentSchema = z.object({
|
|
14
|
+
name: z.string().min(1).max(100),
|
|
15
|
+
notes: z.string().max(200).optional(),
|
|
16
|
+
slug: z.string().regex(SlugRegex),
|
|
17
|
+
});
|
|
18
|
+
export type IngredientContent = z.infer<typeof IngredientContentSchema>;
|
|
19
|
+
|
|
20
|
+
export const IngredientSchema = z.object({
|
|
21
|
+
...IngredientMetaSchema.shape,
|
|
22
|
+
...IngredientContentSchema.shape,
|
|
23
|
+
});
|
|
24
|
+
export type Ingredient = z.infer<typeof IngredientSchema>;
|
|
25
|
+
|
|
26
|
+
export const IngredientSectionMetaSchema = z.object({
|
|
27
|
+
ingredients: z.array(IngredientMetaSchema).min(1),
|
|
28
|
+
slug: z.string().regex(SlugRegex),
|
|
29
|
+
});
|
|
30
|
+
export type IngredientSectionMeta = z.infer<typeof IngredientSectionMetaSchema>;
|
|
31
|
+
|
|
32
|
+
export const IngredientSectionContentSchema = z.object({
|
|
33
|
+
ingredients: z.array(IngredientContentSchema).min(1),
|
|
34
|
+
slug: z.string().regex(SlugRegex),
|
|
35
|
+
title: z.string().max(100).optional(),
|
|
36
|
+
});
|
|
37
|
+
export type IngredientSectionContent = z.infer<
|
|
38
|
+
typeof IngredientSectionContentSchema
|
|
39
|
+
>;
|
|
40
|
+
|
|
41
|
+
export const IngredientSectionSchema = z.object({
|
|
42
|
+
ingredients: z.array(IngredientSchema).min(1),
|
|
43
|
+
slug: z.string().regex(SlugRegex),
|
|
44
|
+
title: z.string().max(100).optional(),
|
|
45
|
+
});
|
|
46
|
+
export type IngredientSection = z.infer<typeof IngredientSectionSchema>;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { TemperatureSchema } from "./temperature";
|
|
3
|
+
|
|
4
|
+
export const InstructionMetaSchema = z.object({
|
|
5
|
+
duration: z.number().int().positive().optional(),
|
|
6
|
+
step: z.number().int().positive(),
|
|
7
|
+
temperature: TemperatureSchema.optional(),
|
|
8
|
+
});
|
|
9
|
+
export type InstructionMeta = z.infer<typeof InstructionMetaSchema>;
|
|
10
|
+
|
|
11
|
+
export const InstructionContentSchema = z.object({
|
|
12
|
+
step: z.number().int().positive(),
|
|
13
|
+
text: z.string().min(1).max(1000),
|
|
14
|
+
});
|
|
15
|
+
export type InstructionContent = z.infer<typeof InstructionContentSchema>;
|
|
16
|
+
|
|
17
|
+
export const InstructionSchema = z.object({
|
|
18
|
+
...InstructionMetaSchema.shape,
|
|
19
|
+
...InstructionContentSchema.shape,
|
|
20
|
+
});
|
|
21
|
+
export type Instruction = z.infer<typeof InstructionSchema>;
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { Timestamp } from "firebase-admin/firestore";
|
|
3
|
+
import {
|
|
4
|
+
ModerationStatusSchema,
|
|
5
|
+
PrioritySchema,
|
|
6
|
+
SeveritySchema,
|
|
7
|
+
SuggestionCategorySchema,
|
|
8
|
+
} from "../enums";
|
|
9
|
+
|
|
10
|
+
export const ModerationSuggestionSchema = z.object({
|
|
11
|
+
category: SuggestionCategorySchema,
|
|
12
|
+
field: z.string(),
|
|
13
|
+
severity: SeveritySchema,
|
|
14
|
+
suggestion: z.string(),
|
|
15
|
+
});
|
|
16
|
+
export type ModerationSuggestion = z.infer<typeof ModerationSuggestionSchema>;
|
|
17
|
+
|
|
18
|
+
export const ModerationSchema = z.object({
|
|
19
|
+
id: z.string(),
|
|
20
|
+
recipeId: z.string(),
|
|
21
|
+
userId: z.string(),
|
|
22
|
+
status: ModerationStatusSchema,
|
|
23
|
+
priority: PrioritySchema,
|
|
24
|
+
qualityScore: z.number().min(0).max(1).optional(),
|
|
25
|
+
aiSuggestions: z.array(ModerationSuggestionSchema).optional(),
|
|
26
|
+
rejectionReason: z.string().optional(),
|
|
27
|
+
createdAt: z.custom<Timestamp>(),
|
|
28
|
+
assessedAt: z.custom<Timestamp>().optional(),
|
|
29
|
+
reviewedBy: z.string().optional(),
|
|
30
|
+
reviewedAt: z.custom<Timestamp>().optional(),
|
|
31
|
+
});
|
|
32
|
+
export type Moderation = z.infer<typeof ModerationSchema>;
|
|
33
|
+
|
|
34
|
+
export const ModerateRecipeInputSchema = z.object({
|
|
35
|
+
moderationId: z.string(),
|
|
36
|
+
recipeId: z.string(),
|
|
37
|
+
userId: z.string(),
|
|
38
|
+
});
|
|
39
|
+
export type ModerateRecipeInput = z.infer<typeof ModerateRecipeInputSchema>;
|
|
40
|
+
|
|
41
|
+
export const ModerateRecipeOutputSchema = z.object({
|
|
42
|
+
qualityScore: z.number().min(0).max(1),
|
|
43
|
+
suggestions: z.array(ModerationSuggestionSchema),
|
|
44
|
+
autoApproved: z.boolean(),
|
|
45
|
+
autoRejected: z.boolean(),
|
|
46
|
+
rejectionReason: z.string().optional(),
|
|
47
|
+
});
|
|
48
|
+
export type ModerateRecipeOutput = z.infer<typeof ModerateRecipeOutputSchema>;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Nutrition data per serving.
|
|
5
|
+
* Units: calories (kcal), fat/carbs/protein/fiber/sugar (g), cholesterol/sodium (mg)
|
|
6
|
+
*/
|
|
7
|
+
export const NutritionSchema = z.object({
|
|
8
|
+
calories: z.number().nonnegative(),
|
|
9
|
+
carbs: z.number().nonnegative().optional(),
|
|
10
|
+
cholesterol: z.number().nonnegative().optional(),
|
|
11
|
+
fat: z.number().nonnegative().optional(),
|
|
12
|
+
fiber: z.number().nonnegative().optional(),
|
|
13
|
+
protein: z.number().nonnegative().optional(),
|
|
14
|
+
saturatedFat: z.number().nonnegative().optional(),
|
|
15
|
+
sodium: z.number().nonnegative().optional(),
|
|
16
|
+
sugar: z.number().nonnegative().optional(),
|
|
17
|
+
});
|
|
18
|
+
export type Nutrition = z.infer<typeof NutritionSchema>;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { Timestamp } from "firebase-admin/firestore";
|
|
3
|
+
import { ProfileStatusSchema } from "../enums";
|
|
4
|
+
|
|
5
|
+
export const ProfileSchema = z.object({
|
|
6
|
+
bio: z.string().max(280).optional(),
|
|
7
|
+
createdAt: z.custom<Timestamp>(),
|
|
8
|
+
handle: z.string().min(3).max(20),
|
|
9
|
+
id: z.string(),
|
|
10
|
+
imageUrl: z.url().optional(),
|
|
11
|
+
name: z.string().min(1).max(50),
|
|
12
|
+
public: z.boolean(),
|
|
13
|
+
status: ProfileStatusSchema,
|
|
14
|
+
updatedAt: z.custom<Timestamp>(),
|
|
15
|
+
verified: z.boolean(),
|
|
16
|
+
});
|
|
17
|
+
export type Profile = z.infer<typeof ProfileSchema>;
|
|
18
|
+
|
|
19
|
+
export const ProfileStatsSchema = z.object({
|
|
20
|
+
followers: z.number().int().nonnegative(),
|
|
21
|
+
following: z.number().int().nonnegative(),
|
|
22
|
+
likes: z.number().int().nonnegative(),
|
|
23
|
+
recipes: z.number().int().nonnegative(),
|
|
24
|
+
saves: z.number().int().nonnegative(),
|
|
25
|
+
updatedAt: z.number().int(),
|
|
26
|
+
});
|
|
27
|
+
export type ProfileStats = z.infer<typeof ProfileStatsSchema>;
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { Timestamp } from "firebase-admin/firestore";
|
|
3
|
+
import {
|
|
4
|
+
AllergenSchema,
|
|
5
|
+
CuisineSchema,
|
|
6
|
+
DietaryTagSchema,
|
|
7
|
+
DifficultySchema,
|
|
8
|
+
LocaleSchema,
|
|
9
|
+
MealTypeSchema,
|
|
10
|
+
RecipeSourceSchema,
|
|
11
|
+
RecipeStatusSchema,
|
|
12
|
+
SpicinessSchema,
|
|
13
|
+
} from "../enums";
|
|
14
|
+
import {
|
|
15
|
+
EquipmentContentSchema,
|
|
16
|
+
EquipmentMetaSchema,
|
|
17
|
+
EquipmentSchema,
|
|
18
|
+
} from "./equipment";
|
|
19
|
+
import {
|
|
20
|
+
IngredientSectionContentSchema,
|
|
21
|
+
IngredientSectionMetaSchema,
|
|
22
|
+
IngredientSectionSchema,
|
|
23
|
+
} from "./ingredient";
|
|
24
|
+
import {
|
|
25
|
+
InstructionContentSchema,
|
|
26
|
+
InstructionMetaSchema,
|
|
27
|
+
InstructionSchema,
|
|
28
|
+
} from "./instruction";
|
|
29
|
+
import { NutritionSchema } from "./nutrition";
|
|
30
|
+
|
|
31
|
+
export const RecipeMetaSchema = z.object({
|
|
32
|
+
allergens: z.array(AllergenSchema),
|
|
33
|
+
confidence: z.number().min(0).max(1),
|
|
34
|
+
createdAt: z.custom<Timestamp>(),
|
|
35
|
+
createdBy: z.string(),
|
|
36
|
+
cuisine: CuisineSchema,
|
|
37
|
+
deletedAt: z.custom<Timestamp>().optional(),
|
|
38
|
+
dietaryTags: z.array(DietaryTagSchema),
|
|
39
|
+
difficulty: DifficultySchema,
|
|
40
|
+
equipment: z.array(EquipmentMetaSchema).optional(),
|
|
41
|
+
id: z.string(),
|
|
42
|
+
imageUrl: z.string().optional(),
|
|
43
|
+
ingredientSections: z.array(IngredientSectionMetaSchema).min(1),
|
|
44
|
+
instructions: z.array(InstructionMetaSchema).min(1),
|
|
45
|
+
mealTypes: z.array(MealTypeSchema).min(1),
|
|
46
|
+
nutrition: NutritionSchema.optional(),
|
|
47
|
+
originalLocale: LocaleSchema,
|
|
48
|
+
servings: z.number().int().min(1).max(100),
|
|
49
|
+
source: RecipeSourceSchema,
|
|
50
|
+
sourceUrl: z.string().optional(),
|
|
51
|
+
spiciness: SpicinessSchema,
|
|
52
|
+
status: RecipeStatusSchema,
|
|
53
|
+
time: z.number().int().min(1).max(1440),
|
|
54
|
+
updatedAt: z.custom<Timestamp>(),
|
|
55
|
+
});
|
|
56
|
+
export type RecipeMeta = z.infer<typeof RecipeMetaSchema>;
|
|
57
|
+
|
|
58
|
+
export const RecipeContentSchema = z.object({
|
|
59
|
+
description: z.string().min(10).max(2000),
|
|
60
|
+
equipment: z.array(EquipmentContentSchema).optional(),
|
|
61
|
+
ingredientSections: z.array(IngredientSectionContentSchema).min(1),
|
|
62
|
+
instructions: z.array(InstructionContentSchema).min(1),
|
|
63
|
+
locale: LocaleSchema,
|
|
64
|
+
tips: z.array(z.string().max(500)).optional(),
|
|
65
|
+
title: z.string().min(3).max(200),
|
|
66
|
+
});
|
|
67
|
+
export type RecipeContent = z.infer<typeof RecipeContentSchema>;
|
|
68
|
+
|
|
69
|
+
export const RecipeSchema = z.object({
|
|
70
|
+
allergens: z.array(AllergenSchema),
|
|
71
|
+
confidence: z.number().min(0).max(1),
|
|
72
|
+
createdAt: z.custom<Timestamp>(),
|
|
73
|
+
createdBy: z.string(),
|
|
74
|
+
cuisine: CuisineSchema,
|
|
75
|
+
deletedAt: z.custom<Timestamp>().optional(),
|
|
76
|
+
description: z.string().min(10).max(2000),
|
|
77
|
+
dietaryTags: z.array(DietaryTagSchema),
|
|
78
|
+
difficulty: DifficultySchema,
|
|
79
|
+
equipment: z.array(EquipmentSchema).optional(),
|
|
80
|
+
id: z.string(),
|
|
81
|
+
imageUrl: z.string().optional(),
|
|
82
|
+
ingredientSections: z.array(IngredientSectionSchema).min(1),
|
|
83
|
+
instructions: z.array(InstructionSchema).min(1),
|
|
84
|
+
locale: LocaleSchema,
|
|
85
|
+
mealTypes: z.array(MealTypeSchema).min(1),
|
|
86
|
+
nutrition: NutritionSchema.optional(),
|
|
87
|
+
originalLocale: LocaleSchema,
|
|
88
|
+
servings: z.number().int().min(1).max(100),
|
|
89
|
+
source: RecipeSourceSchema,
|
|
90
|
+
sourceUrl: z.string().optional(),
|
|
91
|
+
spiciness: SpicinessSchema,
|
|
92
|
+
status: RecipeStatusSchema,
|
|
93
|
+
time: z.number().int().min(1).max(1440),
|
|
94
|
+
tips: z.array(z.string().max(500)).optional(),
|
|
95
|
+
title: z.string().min(3).max(200),
|
|
96
|
+
updatedAt: z.custom<Timestamp>(),
|
|
97
|
+
});
|
|
98
|
+
export type Recipe = z.infer<typeof RecipeSchema>;
|
|
99
|
+
|
|
100
|
+
export const RecipeStatsSchema = z.object({
|
|
101
|
+
comments: z.number().int().nonnegative(),
|
|
102
|
+
likes: z.number().int().nonnegative(),
|
|
103
|
+
rating: z.number().min(0).max(5),
|
|
104
|
+
ratingCount: z.number().int().nonnegative(),
|
|
105
|
+
saves: z.number().int().nonnegative(),
|
|
106
|
+
updatedAt: z.number().int(),
|
|
107
|
+
views: z.number().int().nonnegative(),
|
|
108
|
+
});
|
|
109
|
+
export type RecipeStats = z.infer<typeof RecipeStatsSchema>;
|