Skip to content

Commit 5a6c9b2

Browse files
authored
Merge pull request #3914 from LiteFarmOrg/LF-4995/Create_POST_endpoint_to_add_Market_directory_info
LF-4995: Create post endpoint to add market directory info
2 parents b16d304 + 74e1b14 commit 5a6c9b2

17 files changed

+708
-8
lines changed
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
/*
2+
* Copyright 2025 LiteFarm.org
3+
* This file is part of LiteFarm.
4+
*
5+
* LiteFarm is free software: you can redistribute it and/or modify
6+
* it under the terms of the GNU General Public License as published by
7+
* the Free Software Foundation, either version 3 of the License, or
8+
* (at your option) any later version.
9+
*
10+
* LiteFarm is distributed in the hope that it will be useful,
11+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
12+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13+
* GNU General Public License for more details, see <https://www.gnu.org/licenses/>.
14+
*/
15+
16+
/**
17+
* @param { import("knex").Knex } knex
18+
* @returns { Promise<void> }
19+
*/
20+
21+
export const up = async function (knex) {
22+
await knex('permissions').insert([
23+
{
24+
permission_id: 179,
25+
name: 'add:market_directory_info',
26+
description: 'add market_directory_info',
27+
},
28+
{
29+
permission_id: 180,
30+
name: 'edit:market_directory_info',
31+
description: 'edit market_directory_info',
32+
},
33+
{
34+
permission_id: 181,
35+
name: 'delete:market_directory_info',
36+
description: 'delete market_directory_info',
37+
},
38+
{
39+
permission_id: 182,
40+
name: 'get:market_directory_info',
41+
description: 'get market_directory_info',
42+
},
43+
]);
44+
await knex('rolePermissions').insert([
45+
{ role_id: 1, permission_id: 179 },
46+
{ role_id: 2, permission_id: 179 },
47+
{ role_id: 5, permission_id: 179 },
48+
{ role_id: 1, permission_id: 180 },
49+
{ role_id: 2, permission_id: 180 },
50+
{ role_id: 5, permission_id: 180 },
51+
{ role_id: 1, permission_id: 181 },
52+
{ role_id: 2, permission_id: 181 },
53+
{ role_id: 5, permission_id: 181 },
54+
{ role_id: 1, permission_id: 182 },
55+
{ role_id: 2, permission_id: 182 },
56+
{ role_id: 5, permission_id: 182 },
57+
]);
58+
};
59+
60+
/**
61+
* @param { import("knex").Knex } knex
62+
* @returns { Promise<void> }
63+
*/
64+
export const down = function (knex) {
65+
const permissions = [179, 180, 181, 182];
66+
return Promise.all([
67+
knex('rolePermissions').whereIn('permission_id', permissions).del(),
68+
knex('permissions').whereIn('permission_id', permissions).del(),
69+
]);
70+
};
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
/*
2+
* Copyright 2025 LiteFarm.org
3+
* This file is part of LiteFarm.
4+
*
5+
* LiteFarm is free software: you can redistribute it and/or modify
6+
* it under the terms of the GNU General Public License as published by
7+
* the Free Software Foundation, either version 3 of the License, or
8+
* (at your option) any later version.
9+
*
10+
* LiteFarm is distributed in the hope that it will be useful,
11+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
12+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13+
* GNU General Public License for more details, see <https://www.gnu.org/licenses/>.
14+
*/
15+
16+
import { Response } from 'express';
17+
import baseController from './baseController.js';
18+
import MarketDirectoryInfoModel from '../models/marketDirectoryInfoModel.js';
19+
import { MarketDirectoryInfoReqBody } from '../middleware/validation/checkMarketDirectoryInfo.js';
20+
import { HttpError, LiteFarmRequest } from '../types.js';
21+
22+
const marketDirectoryInfoController = {
23+
addMarketDirectoryInfo() {
24+
return async (
25+
req: LiteFarmRequest<unknown, unknown, unknown, MarketDirectoryInfoReqBody>,
26+
res: Response,
27+
) => {
28+
const { farm_id } = req.headers;
29+
30+
try {
31+
const result = await baseController.post(
32+
MarketDirectoryInfoModel,
33+
{ ...req.body, farm_id },
34+
req,
35+
);
36+
37+
return res.status(201).send(result);
38+
} catch (error: unknown) {
39+
console.error(error);
40+
const err = error as HttpError;
41+
const status = err.status || err.code || 500;
42+
return res.status(status).json({
43+
error: err.message || err,
44+
});
45+
}
46+
};
47+
},
48+
};
49+
50+
export default marketDirectoryInfoController;
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
/*
2+
* Copyright 2025 LiteFarm.org
3+
* This file is part of LiteFarm.
4+
*
5+
* LiteFarm is free software: you can redistribute it and/or modify
6+
* it under the terms of the GNU General Public License as published by
7+
* the Free Software Foundation, either version 3 of the License, or
8+
* (at your option) any later version.
9+
*
10+
* LiteFarm is distributed in the hope that it will be useful,
11+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
12+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13+
* GNU General Public License for more details, see <https://www.gnu.org/licenses/>.
14+
*/
15+
16+
import { Response, NextFunction } from 'express';
17+
import { LiteFarmRequest } from '../../types.js';
18+
import { isValidAddress, isValidEmail } from '../../util/validation.js';
19+
import { isValidUrl } from '../../util/url.js';
20+
import { SOCIALS, validateSocialAndExtractUsername } from '../../util/socials.js';
21+
import MarketDirectoryInfoModel from '../../models/marketDirectoryInfoModel.js';
22+
23+
export interface MarketDirectoryInfoReqBody {
24+
farm_name?: string;
25+
logo?: string;
26+
about?: string;
27+
contact_first_name?: string;
28+
contact_last_name?: string;
29+
contact_email?: string;
30+
email?: string;
31+
country_code?: number;
32+
phone_number?: string;
33+
address?: string;
34+
website?: string;
35+
instagram?: string;
36+
facebook?: string;
37+
x?: string;
38+
}
39+
40+
export function checkAndTransformMarketDirectoryInfo() {
41+
return async (
42+
req: LiteFarmRequest<unknown, unknown, unknown, MarketDirectoryInfoReqBody>,
43+
res: Response,
44+
next: NextFunction,
45+
) => {
46+
if (req.method === 'POST') {
47+
// @ts-expect-error: TS doesn't see query() through softDelete HOC; safe at runtime
48+
const record = await MarketDirectoryInfoModel.query()
49+
.where({ farm_id: req.headers.farm_id })
50+
.first();
51+
52+
if (record) {
53+
return res.status(409).send('Market directory info for this farm already exists');
54+
}
55+
}
56+
57+
const { address, website } = req.body;
58+
59+
for (const emailProperty of ['contact_email', 'email'] as const) {
60+
if (req.body[emailProperty] && !isValidEmail(req.body[emailProperty])) {
61+
return res.status(400).send(`Invalid ${emailProperty}`);
62+
}
63+
}
64+
65+
if (address && !(await isValidAddress(address))) {
66+
return res.status(400).send('Invalid address');
67+
}
68+
69+
if (website && !(await isValidUrl(website)) /* TODO: LF-5011 */) {
70+
return res.status(400).send('Invalid website');
71+
}
72+
73+
for (const social of SOCIALS.filter((social) => req.body[social]?.trim())) {
74+
const socialUsername = validateSocialAndExtractUsername(social, req.body[social]!);
75+
76+
if (!socialUsername) {
77+
return res.status(400).send(`Invalid ${social}`);
78+
}
79+
req.body[social] = socialUsername;
80+
}
81+
82+
next();
83+
};
84+
}

packages/api/src/models/marketDirectoryInfoModel.js

Lines changed: 31 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
*/
1515

1616
import baseModel from './baseModel.js';
17+
import { checkAndTrimString } from '../util/util.js';
1718

1819
class MarketDirectoryInfo extends baseModel {
1920
static get tableName() {
@@ -24,6 +25,34 @@ class MarketDirectoryInfo extends baseModel {
2425
return 'id';
2526
}
2627

28+
static get stringProperties() {
29+
const stringProperties = [];
30+
for (const [key, value] of Object.entries(this.jsonSchema.properties)) {
31+
if (value.type.includes('string')) {
32+
stringProperties.push(key);
33+
}
34+
}
35+
return stringProperties;
36+
}
37+
38+
async $beforeInsert(queryContext) {
39+
await super.$beforeInsert(queryContext);
40+
this.trimStringProperties();
41+
}
42+
43+
async $beforeUpdate(opt, queryContext) {
44+
await super.$beforeUpdate(opt, queryContext);
45+
this.trimStringProperties();
46+
}
47+
48+
trimStringProperties() {
49+
for (const key of this.constructor.stringProperties) {
50+
if (key in this) {
51+
this[key] = checkAndTrimString(this[key]);
52+
}
53+
}
54+
}
55+
2756
// Optional JSON schema. This is not the database schema! Nothing is generated
2857
// based on this. This is only used for validation. Whenever a model instance
2958
// is created it is checked against this schema. http://json-schema.org/.
@@ -36,15 +65,15 @@ class MarketDirectoryInfo extends baseModel {
3665
farm_id: { type: 'string' },
3766
farm_name: { type: 'string', minLength: 1, maxLength: 255 },
3867
logo: { type: ['string', 'null'], maxLength: 255 },
39-
about: { type: ['string', 'null'] },
68+
about: { type: ['string', 'null'], maxLength: 3000 },
4069
contact_first_name: { type: 'string', minLength: 1, maxLength: 255 },
4170
contact_last_name: { type: ['string', 'null'], maxLength: 255 },
4271
contact_email: { type: 'string', minLength: 1, maxLength: 255 },
4372
email: { type: ['string', 'null'], maxLength: 255 },
4473
country_code: { type: ['integer', 'null'], minimum: 1, maximum: 999 },
4574
phone_number: { type: ['string', 'null'], maxLength: 255 },
4675
address: { type: 'string', minLength: 1, maxLength: 255 },
47-
website: { type: ['string', 'null'] },
76+
website: { type: ['string', 'null'], maxLength: 2000 },
4877
instagram: { type: ['string', 'null'], maxLength: 255 },
4978
facebook: { type: ['string', 'null'], maxLength: 255 },
5079
x: { type: ['string', 'null'], maxLength: 255 },
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
/*
2+
* Copyright 2025 LiteFarm.org
3+
* This file is part of LiteFarm.
4+
*
5+
* LiteFarm is free software: you can redistribute it and/or modify
6+
* it under the terms of the GNU General Public License as published by
7+
* the Free Software Foundation, either version 3 of the License, or
8+
* (at your option) any later version.
9+
*
10+
* LiteFarm is distributed in the hope that it will be useful,
11+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
12+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13+
* GNU General Public License for more details, see <https://www.gnu.org/licenses/>.
14+
*/
15+
16+
import express from 'express';
17+
import checkScope from '../middleware/acl/checkScope.js';
18+
import { checkAndTransformMarketDirectoryInfo } from '../middleware/validation/checkMarketDirectoryInfo.js';
19+
import MarketDirectoryInfoController from '../controllers/marketDirectoryInfoController.js';
20+
21+
const router = express.Router();
22+
23+
router.post(
24+
'/',
25+
checkScope(['add:market_directory_info']),
26+
checkAndTransformMarketDirectoryInfo(),
27+
MarketDirectoryInfoController.addMarketDirectoryInfo(),
28+
);
29+
30+
export default router;

packages/api/src/server.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -175,6 +175,7 @@ import farmAddonRoute from './routes/farmAddonRoute.js';
175175
import weatherRoute from './routes/weatherRoute.js';
176176
import irrigationPrescriptionRoute from './routes/irrigationPrescriptionRoute.js';
177177
import irrigationPrescriptionRequestRoute from './routes/irrigationPrescriptionRequestRoute.js';
178+
import marketDirectoryInfoRoute from './routes/marketDirectoryInfoRoute.js';
178179

179180
// register API
180181
const router = promiseRouter();
@@ -343,7 +344,8 @@ app
343344
.use('/farm_addon', farmAddonRoute)
344345
.use('/weather', weatherRoute)
345346
.use('/irrigation_prescriptions', irrigationPrescriptionRoute)
346-
.use('/irrigation_prescription_request', irrigationPrescriptionRequestRoute);
347+
.use('/irrigation_prescription_request', irrigationPrescriptionRequestRoute)
348+
.use('/market_directory_info', marketDirectoryInfoRoute);
347349

348350
// Allow a 1MB limit on sensors to match incoming Ensemble data
349351
app.use('/sensor', express.json({ limit: '1MB' }), rejectBodyInGetAndDelete, sensorRoute);

packages/api/src/types.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,12 @@ export interface HttpError extends Error {
2121
}
2222

2323
// TODO: Remove farm_id conditional and cast this in a checkScope() that takes the function and casts this to req
24-
export interface LiteFarmRequest<QueryParams = unknown, RouteParams = unknown>
25-
extends Request<RouteParams, unknown, unknown, QueryParams> {
24+
export interface LiteFarmRequest<
25+
QueryParams = unknown,
26+
RouteParams = unknown,
27+
ResBody = unknown,
28+
ReqBody = unknown,
29+
> extends Request<RouteParams, ResBody, ReqBody, QueryParams> {
2630
headers: Request['headers'] & {
2731
farm_id?: string;
2832
};
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
/*
2+
* Copyright 2025 LiteFarm.org
3+
* This file is part of LiteFarm.
4+
*
5+
* LiteFarm is free software: you can redistribute it and/or modify
6+
* it under the terms of the GNU General Public License as published by
7+
* the Free Software Foundation, either version 3 of the License, or
8+
* (at your option) any later version.
9+
*
10+
* LiteFarm is distributed in the hope that it will be useful,
11+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
12+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13+
* GNU General Public License for more details, see <https://www.gnu.org/licenses/>.
14+
*/
15+
16+
import { Client } from '@googlemaps/google-maps-services-js';
17+
const googleClient = new Client({});
18+
19+
export async function getAddressComponents(address: string) {
20+
try {
21+
const response = await googleClient.geocode({
22+
params: {
23+
address,
24+
key: process.env.GOOGLE_API_KEY!,
25+
},
26+
});
27+
return response.data.results[0]?.address_components;
28+
} catch (error) {
29+
console.error(error);
30+
}
31+
}

0 commit comments

Comments
 (0)