@cravery/core 0.0.2 → 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/index.d.ts +1 -0
- package/dist/config/index.d.ts.map +1 -1
- package/dist/config/index.js +1 -0
- package/dist/config/index.js.map +1 -1
- package/dist/enums/cuisine.d.ts +1 -1
- package/dist/enums/difficulty.d.ts +1 -1
- package/dist/enums/moderation_status.d.ts +1 -1
- package/dist/enums/priority.d.ts +1 -1
- package/dist/enums/profile_status.d.ts +2 -2
- package/dist/enums/recipe_source.d.ts +2 -2
- package/dist/enums/recipe_status.d.ts +2 -2
- package/dist/enums/role.d.ts +1 -1
- package/dist/enums/severity.d.ts +1 -1
- package/dist/enums/spiciness.d.ts +1 -1
- package/dist/enums/status.d.ts +2 -2
- package/dist/enums/unit.d.ts +5 -5
- package/dist/enums/user_status.d.ts +2 -2
- 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 +9 -1
- package/dist/lib/index.d.ts.map +1 -1
- package/dist/lib/index.js +23 -0
- package/dist/lib/index.js.map +1 -1
- 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 +2 -2
- package/dist/types/ai/config.d.ts.map +1 -1
- package/dist/types/ai/filters.d.ts +3 -3
- package/dist/types/ai/recipe.d.ts +10 -10
- package/dist/types/equipment.d.ts +2 -2
- package/dist/types/equipment.js +1 -1
- 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 +1 -0
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/index.js +1 -0
- package/dist/types/index.js.map +1 -1
- package/dist/types/ingredient.d.ts +24 -24
- package/dist/types/ingredient.js +1 -1
- package/dist/types/ingredient.js.map +1 -1
- package/dist/types/moderation.d.ts +5 -5
- package/dist/types/profile.d.ts +2 -2
- package/dist/types/recipe.d.ts +28 -28
- package/dist/types/user.d.ts +3 -3
- package/package.json +2 -2
- package/src/config/ai.ts +34 -0
- package/src/config/index.ts +1 -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 +3 -2
- package/src/types/equipment.ts +1 -1
- package/src/types/error.ts +43 -0
- package/src/types/index.ts +1 -0
- package/src/types/ingredient.ts +1 -1
package/src/lib/cost.ts
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { getDatabase, ServerValue } from "firebase-admin/database";
|
|
2
|
+
import { AIModelConfig } from "../types";
|
|
3
|
+
|
|
4
|
+
interface BaseUsage {
|
|
5
|
+
flowName: string;
|
|
6
|
+
model: AIModelConfig;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
interface MultimodalUsage extends BaseUsage {
|
|
10
|
+
type: "multimodal";
|
|
11
|
+
inputTokens: number;
|
|
12
|
+
outputTokens: number;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
interface ImageUsage extends BaseUsage {
|
|
16
|
+
type: "image";
|
|
17
|
+
imageCount: number;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
interface EmbeddingUsage extends BaseUsage {
|
|
21
|
+
type: "embedding";
|
|
22
|
+
tokenCount: number;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export type AIUsage = MultimodalUsage | ImageUsage | EmbeddingUsage;
|
|
26
|
+
|
|
27
|
+
export const logAIUsage = (usage: AIUsage): number => {
|
|
28
|
+
const { flowName, model } = usage;
|
|
29
|
+
|
|
30
|
+
let cost: number;
|
|
31
|
+
let metrics: Record<string, number>;
|
|
32
|
+
|
|
33
|
+
switch (usage.type) {
|
|
34
|
+
case "multimodal":
|
|
35
|
+
cost =
|
|
36
|
+
(usage.inputTokens / 1_000_000) * model.cost.input +
|
|
37
|
+
(usage.outputTokens / 1_000_000) * model.cost.output;
|
|
38
|
+
metrics = {
|
|
39
|
+
inputTokens: usage.inputTokens,
|
|
40
|
+
outputTokens: usage.outputTokens,
|
|
41
|
+
};
|
|
42
|
+
break;
|
|
43
|
+
case "image":
|
|
44
|
+
cost = usage.imageCount * model.cost.output;
|
|
45
|
+
metrics = { images: usage.imageCount };
|
|
46
|
+
break;
|
|
47
|
+
case "embedding":
|
|
48
|
+
cost = (usage.tokenCount / 1_000) * model.cost.input;
|
|
49
|
+
metrics = { tokens: usage.tokenCount };
|
|
50
|
+
break;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const db = getDatabase();
|
|
54
|
+
const date = new Date().toISOString().split("T")[0];
|
|
55
|
+
const base = `ai_usage/live/${date}`;
|
|
56
|
+
|
|
57
|
+
const updates: Record<string, unknown> = {
|
|
58
|
+
[`${base}/total`]: ServerValue.increment(cost),
|
|
59
|
+
[`${base}/count`]: ServerValue.increment(1),
|
|
60
|
+
[`${base}/flows/${flowName}/cost`]: ServerValue.increment(cost),
|
|
61
|
+
[`${base}/flows/${flowName}/count`]: ServerValue.increment(1),
|
|
62
|
+
[`${base}/models/${model.id}/cost`]: ServerValue.increment(cost),
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
for (const [key, value] of Object.entries(metrics)) {
|
|
66
|
+
updates[`${base}/models/${model.id}/${key}`] = ServerValue.increment(value);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
db.ref().update(updates).catch(console.error);
|
|
70
|
+
|
|
71
|
+
return cost;
|
|
72
|
+
};
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { FieldValue } from "firebase-admin/firestore";
|
|
2
|
+
import { defineFirestoreRetriever } from "@genkit-ai/firebase";
|
|
3
|
+
import { firestore } from "./firebase";
|
|
4
|
+
import { ai, geminiEmbedding001 } from "./genkit";
|
|
5
|
+
import { logAIUsage } from "./cost";
|
|
6
|
+
import { AI_MODELS } from "../config/ai";
|
|
7
|
+
|
|
8
|
+
export const generateEmbedding = async (text: string, flowName: string) => {
|
|
9
|
+
if (text.length < 1 || text.length > 7500) {
|
|
10
|
+
throw new Error("Validation error: Text out of range");
|
|
11
|
+
}
|
|
12
|
+
const response = await ai.embed({
|
|
13
|
+
embedder: geminiEmbedding001,
|
|
14
|
+
content: text,
|
|
15
|
+
});
|
|
16
|
+
logAIUsage({
|
|
17
|
+
type: "embedding",
|
|
18
|
+
flowName,
|
|
19
|
+
model: AI_MODELS.GeminiEmbedding001,
|
|
20
|
+
tokenCount: text.length,
|
|
21
|
+
});
|
|
22
|
+
return response[0]?.embedding;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
export const storeEmbedding = async (
|
|
26
|
+
collection: string,
|
|
27
|
+
docId: string,
|
|
28
|
+
text: string,
|
|
29
|
+
flowName?: string,
|
|
30
|
+
vectorField: string = "embedding",
|
|
31
|
+
) => {
|
|
32
|
+
const embedding = await generateEmbedding(
|
|
33
|
+
text,
|
|
34
|
+
flowName || `${collection}:embedding`,
|
|
35
|
+
);
|
|
36
|
+
await firestore
|
|
37
|
+
.collection(collection)
|
|
38
|
+
.doc(docId)
|
|
39
|
+
.update({
|
|
40
|
+
[vectorField]: FieldValue.vector(embedding),
|
|
41
|
+
});
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
export const recipesRetriever = defineFirestoreRetriever(ai, {
|
|
45
|
+
name: "recipesRetriever",
|
|
46
|
+
firestore,
|
|
47
|
+
collection: "recipes",
|
|
48
|
+
contentField: "searchTerms",
|
|
49
|
+
vectorField: "titleEmbedding",
|
|
50
|
+
embedder: geminiEmbedding001,
|
|
51
|
+
distanceMeasure: "COSINE",
|
|
52
|
+
distanceResultField: "distance",
|
|
53
|
+
});
|
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
CHANGED
|
@@ -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
|
+
};
|
package/src/types/ai/config.ts
CHANGED
|
@@ -1,14 +1,15 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* AI model configuration, costs are in USD:
|
|
3
3
|
* - multimodal: cost per 1,000,000 input/output tokens
|
|
4
|
-
* - image: cost per image
|
|
4
|
+
* - image: cost per image (stored in cost.output)
|
|
5
5
|
* - embedding: cost per 1,000 input tokens
|
|
6
6
|
*/
|
|
7
7
|
export type AIModel =
|
|
8
8
|
| "Gemini25Flash"
|
|
9
9
|
| "Gemini25Pro"
|
|
10
|
+
| "Gemini3Flash"
|
|
10
11
|
| "Imagen4Fast"
|
|
11
|
-
| "
|
|
12
|
+
| "GeminiEmbedding001";
|
|
12
13
|
export type AIModelType = "multimodal" | "image" | "embedding";
|
|
13
14
|
|
|
14
15
|
export interface AIModelCost {
|
package/src/types/equipment.ts
CHANGED
|
@@ -2,7 +2,7 @@ import { z } from "zod";
|
|
|
2
2
|
import { SlugRegex } from "../config";
|
|
3
3
|
|
|
4
4
|
export const EquipmentMetaSchema = z.object({
|
|
5
|
-
|
|
5
|
+
required: z.boolean().optional(),
|
|
6
6
|
slug: z.string().regex(SlugRegex),
|
|
7
7
|
});
|
|
8
8
|
export type EquipmentMeta = z.infer<typeof EquipmentMetaSchema>;
|
|
@@ -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
|
+
}
|
package/src/types/index.ts
CHANGED
package/src/types/ingredient.ts
CHANGED
|
@@ -3,8 +3,8 @@ import { SlugRegex } from "../config";
|
|
|
3
3
|
import { UnitSchema } from "../enums";
|
|
4
4
|
|
|
5
5
|
export const IngredientMetaSchema = z.object({
|
|
6
|
-
optional: z.boolean().optional(),
|
|
7
6
|
quantity: z.number().positive().optional(),
|
|
7
|
+
required: z.boolean().optional(),
|
|
8
8
|
slug: z.string().regex(SlugRegex),
|
|
9
9
|
unit: UnitSchema.optional(),
|
|
10
10
|
});
|