diff --git a/main.ts b/main.ts deleted file mode 100755 index c5fa4b10..00000000 --- a/main.ts +++ /dev/null @@ -1,348 +0,0 @@ -import axios, { Axios } from "axios"; -import { paths, components, external } from "./schemas/server/docs/api/ref/api"; -import createClient from "openapi-fetch"; -import { Product, SearchResult } from "./types"; - -type OffOptions = { - country: string; -}; - -/** Wrapper of OFF API */ -export class OpenFoodFacts { - private readonly axios: Axios; - private readonly client: ReturnType>; - - /** - * Create OFF object - * @param options - Options for the OFF Object - */ - constructor( - options: OffOptions = { - country: "world", - } - ) { - const baseUrl = `https://${options.country}.openfoodfacts.org`; - this.axios = axios.create({ - baseURL: baseUrl, - }); - this.client = createClient({ - baseUrl: baseUrl, - headers: { - "User-Agent": - "OpenFoodFacts NodeJS Client v" + require("../package.json").version, - }, - }); - } - - /** - * @deprecated - */ - country(country: string): OpenFoodFacts { - return new OpenFoodFacts({ country }); - } - - /** - * It is used to get all brands. - * @return {Object} It returns a JSON containing all brands - * @example - * const worldOFF = new OFF() - * const indiaOFF = worldOFF.country('in') - * indiaOFF.getBrands().then(brands => { - * // use brands - * }) - */ - async getBrands(): Promise { - return this.axios.get(`/brands.json`).then((res) => res.data); - } - - /** - * It is used to get a specific product using barcode - * @param {string} barcode - Barcode of the product you want to fetch details - * @example - * const worldOFF = new OFF() - * worldOFF.getProduct(7622210288257).then(product => { - * // use product - * }) - */ - async getProduct(barcode: string): Promise { - const res = await this.client.get("/api/v2/product/{barcode}", { - params: { path: { barcode } }, - }); - - return res.data?.product; - } - - async performOCR( - barcode: string, - photoId: string, - ocrEngine: "google_cloud_vision" = "google_cloud_vision" - ): Promise<{ status?: number | undefined } | undefined> { - const res = await this.client.get("/cgi/ingredients.pl", { - params: { - query: { - code: barcode, - id: photoId, - ocr_engine: ocrEngine, - process_image: "1", - }, - }, - }); - - return res.data; - } - - async getProductImages(barcode: string): Promise { - const res = await this.client.get( - "/api/v2/product/{barcode}?fields=images", - { params: { path: { barcode } } } - ); - - if (!res.data?.product) { - throw new Error("Product not found"); - } else if (!res.data?.product?.images) { - throw new Error("Images not found"); - } - - const imgObj = res.data?.product?.images; - return Object.keys(imgObj); - } - - async search( - fields: string, - sort_by: components["parameters"]["sort_by"] - ): Promise { - const res = await this.client.get("/api/v2/search", { - params: { query: { fields, sort_by } }, - }); - - return res.data; - } - - /** - * It is used to get all details of a specific brand - * @param {string} brandName - Brand name of the brand you want to fetch details - * @return {Object} It returns a JSON with all details of the brand - * @example - * const worldOFF = new OFF() - * worldOFF.getBrand('monoprix').then(brand => { - * // use brand - * }) - */ - async getBrand(brandName: string): Promise { - const res = await this.axios.get(`/brand/${brandName}.json`); - return res.data; - } - - /** - * It is used to get all languages on the labels - * @return {Object} It returns a JSON with list of all languages - * @example - * const worldOFF = new OFF() - * worldOFF.getLanguages().then(languages => { - * // use languages - * }) - */ - async getLanguages(): Promise { - const res = await this.axios.get(`/languages.json`); - return res.data; - } - - /** - * It is used to get all Labels from the API - * @return {Object} It returns a JSON with all labels present on the API - * @example - * const worldOFF = new OFF() - * worldOFF.getLabels().then(labels => { - * // use labels - * }) - */ - async getLabels(): Promise { - const res = await this.axios.get(`/labels.json`); - return res.data; - } - - /** - * It is used to get all additives from the API - * @return {Object} It returns a JSON with all additives present in the API - * @example - * const worldOFF = new OFF() - * worldOFF.getAdditives().then(additives =>{ - * //use additives - * }) - */ - async getAdditives(): Promise { - const res = await this.axios.get(`/additives.json`); - return res.data; - } - - /** - * It is used to get all allergens from the API - * @return {Object} It returns a JSON with all allergens present in the API - * @example - * const worldOFF = new OFF() - * worldOFF.getAllergens().then(allergens =>{ - * //use allergens - * }) - */ - async getAllergens(): Promise { - const res = await this.axios.get(`/allergens.json`); - return res.data; - } - - /** - * It is used to get all categories from the API - * @return {Object} It returns a JSON with all categories present in the API - * @example - * const worldOFF = new OFF() - * worldOFF.getCategories().then(categories =>{ - * //use categories - * }) - */ - async getCategories(): Promise { - const res = await this.axios.get(`/categories.json`); - return res.data; - } - - /** - * It is used to get all countries from the API - * @return {Object} It returns a JSON with all categories present in the API - * @example - * const worldOFF = new OFF() - * worldOFF.getCountries().then(countries =>{ - * //use countries - * }) - */ - async getCountries(): Promise { - const res = await this.axios.get(`/countries.json`); - return res.data; - } - - /** - * It is used to get all entry dates from the API - * @return {Object} It returns a JSON with all entry dates present in the API - * @example - * const worldOFF = new OFF() - * worldOFF.getEntryDates().then(entry_dates =>{ - * //use entry_dates - * }) - */ - async getEntryDates() { - const res = await this.axios.get(`/entry-dates.json`); - return res.data; - } - - /** - * It is used to get all ingredients from the API - * @return {Object} It returns a JSON with all ingredients present in the API - * @example - * const worldOFF = new OFF() - * worldOFF.getIngredients().then(ingredients =>{ - * //use ingredients - * }) - */ - async getIngredients(): Promise { - const res = await this.axios.get(`/ingredients.json`); - return res.data; - } - - /** - * It is used to get all packagings from the API - * @return {Object} It returns a JSON with all packagings present in the API - * @example - * const worldOFF = new OFF() - * worldOFF.getPackagings().then(packagings =>{ - * //use packagings - * }) - */ - async getPackagings(): Promise { - const res = await this.axios.get(`/packaging.json`); - return res.data; - } - - /** - * It is used to get packaging codes from the API - * @return {Object} It returns a JSON with all packaging codes present in the API - * @example - * const worldOFF = new OFF() - * worldOFF.getPackagingCodes().then(packaging_codes =>{ - * //use packaging_codes - * }) - */ - async getPacakgingCodes() { - const res = await this.axios.get(`/packager-codes.json`); - return res.data; - } - - /** - * It is used to get all purchase places from the API - * @return {Object} It returns a JSON with all purchase places present in the API - * @example - * const worldOFF = new OFF() - * worldOFF.getPurchasePlaces().then(purchase_places =>{ - * //use purchase_places - * }) - */ - async getPurchasePlaces(): Promise { - const res = await this.axios.get(`/purchase-places.json`); - return res.data; - } - - /** - * It is used to get all states from the API - * @return {Object} It returns a JSON with all states present in the API - * @example - * const worldOFF = new OFF() - * worldOFF.getStates().then(states =>{ - * //use states - * }) - */ - async getStates(): Promise { - const res = await this.axios.get(`/states.json`); - return res.data; - } - - /** - * It is used to get all stores from the API - * @return {Object} It returns a JSON with all stores present in the API - * @example - * const worldOFF = new OFF() - * worldOFF.getStores().then(stores =>{ - * //use stores - * }) - */ - async getStores(): Promise { - const res = await this.axios.get(`/stores.json`); - return res.data; - } - - /** - * It is used to get all trace types from the API - * @return {Object} It returns a JSON with all traces present in the API - * @example - * const worldOFF = new OFF() - * worldOFF.getTraces().then(traces =>{ - * //use traces - * }) - */ - async getTraces(): Promise { - const res = await this.axios.get(`/traces.json`); - return res.data; - } - - /** - * It is used to get all products beginning with the given barcode string - * @param {string} beginning - Barcode string from which if the barcode begins, then product is to be fetched - * @return {Object} It returns a JSON of all products that begin with the given barcode string - * @example - * const worldOFF = new OFF() - * worldOFF.getProductsByBarcodeBeginning('3596710').then(products => { - * // use products - * }) - */ - async getProductsByBarcodeBeginning(beginning: string) { - const fill = "x".repeat(13 - beginning.length); - const barcode = beginning.concat(fill); - return this.getProduct(barcode); - } -} -export default OpenFoodFacts; diff --git a/src/consts.ts b/src/consts.ts new file mode 100644 index 00000000..0c149fb6 --- /dev/null +++ b/src/consts.ts @@ -0,0 +1 @@ +export const STATIC_HOST = "https://static.openfoodfacts.org"; diff --git a/src/main.ts b/src/main.ts new file mode 100755 index 00000000..10fe926d --- /dev/null +++ b/src/main.ts @@ -0,0 +1,205 @@ +import { paths, components } from "../schemas/server/docs/api/ref/api"; +import createClient from "openapi-fetch"; +import { Product, SearchResult } from "../types"; +import { Robotoff } from "./robotoff"; +import { RequestInfo, Response } from "node-fetch"; +import { getTaxo } from "./taxonomy/api"; +import { + Additive, + Allergen, + Brand, + Category, + Country, + Ingredient, + Label, + Language, + State, + Store, + TaxoNode, + Taxonomy, +} from "./taxonomy/types"; + +type OffOptions = { + country: string; +}; + +export * from "./taxonomy/types"; + +/** Wrapper of OFF API */ +export class OpenFoodFacts { + private readonly fetch: ( + url: URL | RequestInfo, + init?: RequestInit + ) => Promise; + + private readonly client: ReturnType>; + readonly robotoff: Robotoff; + readonly baseUrl: string; + + /** + * Create OFF object + * @param options - Options for the OFF Object + */ + constructor( + fetch: (url: URL | RequestInfo, init?: RequestInit) => Promise, + + options: OffOptions = { + country: "world", + } + ) { + this.baseUrl = `https://${options.country}.openfoodfacts.org`; + this.fetch = fetch; + + this.client = createClient({ + fetch: this.fetch, + baseUrl: this.baseUrl, + }); + + this.robotoff = new Robotoff(fetch); + } + + /** + * @deprecated + */ + country(country: string): OpenFoodFacts { + return new OpenFoodFacts(fetch, { country }); + } + + private async getTaxoEntry( + taxo: string, + entry: string + ): Promise { + const res = await fetch( + `${this.baseUrl}/api/v2/taxonomy?tagtype=${taxo}&tags=${entry}` + ); + + return (await res.json()) as T; + } + + getBrands(): Promise> { + return getTaxo("brands", this.fetch); + } + + getBrand(brandName: string): Promise { + return this.getTaxoEntry("brands", brandName); + } + + getLanguages(): Promise> { + return getTaxo("languages", this.fetch); + } + + getLanguage(languageName: string): Promise { + return this.getTaxoEntry("languages", languageName); + } + + getLabels(): Promise> { + return getTaxo("labels", this.fetch); + } + + getAdditives(): Promise> { + return getTaxo("additives", this.fetch); + } + + getAllergens(): Promise> { + return getTaxo("allergens", this.fetch); + } + + getCategories(): Promise> { + return getTaxo("categories", this.fetch); + } + + getCountries(): Promise> { + return getTaxo("countries", this.fetch); + } + + async getIngredients(): Promise> { + return getTaxo("ingredients", this.fetch); + } + + async getPackagings(): Promise> { + return getTaxo("packaging", this.fetch); + } + + async getStates(): Promise> { + return getTaxo("states", this.fetch); + } + + getStores(): Promise> { + return getTaxo("stores", this.fetch); + } + + /** + * It is used to get a specific product using barcode + * @param barcode Barcode of the product you want to fetch details + */ + async getProduct(barcode: string): Promise { + const res = await this.client.get("/api/v2/product/{barcode}", { + params: { path: { barcode } }, + }); + + return res.data?.product; + } + + async performOCR( + barcode: string, + photoId: string, + ocrEngine: "google_cloud_vision" = "google_cloud_vision" + ): Promise<{ status?: number | undefined } | undefined> { + const res = await this.client.get("/cgi/ingredients.pl", { + params: { + query: { + code: barcode, + id: photoId, + ocr_engine: ocrEngine, + process_image: "1", + }, + }, + }); + + return res.data; + } + + async getProductImages(barcode: string): Promise { + const res = await this.client.get( + "/api/v2/product/{barcode}?fields=images", + { params: { path: { barcode } } } + ); + + if (!res.data?.product) { + throw new Error("Product not found"); + } else if (!res.data?.product?.images) { + throw new Error("Images not found"); + } + + const imgObj = res.data?.product?.images; + return Object.keys(imgObj); + } + + async search( + fields: string, + sort_by: components["parameters"]["sort_by"] + ): Promise { + const res = await this.client.get("/api/v2/search", { + params: { query: { fields, sort_by } }, + }); + + return res.data; + } + + /** + * It is used to get all products beginning with the given barcode string + * @param {string} beginning - Barcode string from which if the barcode begins, then product is to be fetched + * @return {Object} It returns a JSON of all products that begin with the given barcode string + * @example + * const worldOFF = new OFF() + * worldOFF.getProductsByBarcodeBeginning('3596710').then(products => { + * // use products + * }) + */ + async getProductsByBarcodeBeginning(beginning: string) { + const fill = "x".repeat(13 - beginning.length); + const barcode = beginning.concat(fill); + return this.getProduct(barcode); + } +} +export default OpenFoodFacts; diff --git a/src/robotoff.ts b/src/robotoff.ts new file mode 100644 index 00000000..ad74bd68 --- /dev/null +++ b/src/robotoff.ts @@ -0,0 +1,56 @@ +import { RequestInfo } from "node-fetch"; +import { paths, components } from "../schemas/robotoff"; +import createClient from "openapi-fetch"; + +export class Robotoff { + private readonly fetch: ( + url: URL | RequestInfo, + init?: RequestInit + ) => Promise; + + private readonly client: ReturnType>; + + constructor( + fetch: (url: URL | RequestInfo, init?: RequestInit) => Promise + ) { + this.fetch = fetch; + this.client = createClient({ + fetch: this.fetch, + baseUrl: "https://robotoff.openfoodfacts.org", + }); + } + + async annotate( + body: paths["/insights/annotate"]["post"]["requestBody"]["content"]["application/x-www-form-urlencoded"] + ) { + return this.client.post("/insights/annotate", { + body: body, + }); + } + + async questionsByProductCode(code: number) { + const result = await this.client.get("/questions/{barcode}", { + params: { + path: { barcode: code }, + }, + }); + return result.data; + } + + async insightDetail(id: string) { + const result = await this.client.get("/insights/detail/{id}", { + params: { path: { id } }, + }); + return result.data; + } + + async loadLogo(logoId: string) { + // @ts-expect-error TODO: still not documented + const result = await this.client.get("/images/logos/{logoId}", { + params: { + path: { logoId }, + }, + }); + return result.data; + } +} diff --git a/src/taxonomy/api.ts b/src/taxonomy/api.ts new file mode 100644 index 00000000..ae388fbb --- /dev/null +++ b/src/taxonomy/api.ts @@ -0,0 +1,13 @@ +import { STATIC_HOST } from "../consts"; +import { TaxoNode, Taxonomy } from "./types"; + +export const TAXONOMY_URL = (taxo: string) => + `${STATIC_HOST}/data/taxonomies/${taxo}.json`; + +export async function getTaxo( + taxo: string, + fetch: (url: string, options?: RequestInit) => Promise +): Promise> { + const res = await fetch(TAXONOMY_URL(taxo)); + return (await res.json()) as Taxonomy; +} diff --git a/src/taxonomy/types.ts b/src/taxonomy/types.ts new file mode 100644 index 00000000..821cb655 --- /dev/null +++ b/src/taxonomy/types.ts @@ -0,0 +1,45 @@ +export type LocalizedString = Record; + +export type Taxonomy = Record; + +export type TaxoNode = { + name: LocalizedString; + parents?: string[]; + children?: string[]; + wikidata_category?: LocalizedString; + wikidata?: LocalizedString; + synonyms?: Record; +}; + +export type Label = TaxoNode & { + country: LocalizedString; + auth_url: LocalizedString; +}; + +export type Country = TaxoNode & object; + +export type Ingredient = TaxoNode & object; + +export type State = TaxoNode & object; + +export type Category = TaxoNode & { + agribalyse_food_code?: LocalizedString; + ciqual_food_name?: LocalizedString; +}; + +export type Store = TaxoNode & object; + +export type Brand = TaxoNode & object; + +export type Additive = TaxoNode & object; + +export type Allergen = TaxoNode & object; + +export type Language = TaxoNode & { + language_code_2: { + en: string; + }; + language_code_3: { + en: string; + }; +}; diff --git a/tsconfig.json b/tsconfig.json index b5fa3a92..205dc8e2 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,6 +1,10 @@ { "compilerOptions": { "target": "ES2015", + "rootDirs": [ + "src", + "schemas" + ], "module": "CommonJS", "lib": [ "ES2022"