feat: add bulk import endpoint
This commit is contained in:
@@ -29,6 +29,7 @@
|
|||||||
"fastify": "^5.2.0",
|
"fastify": "^5.2.0",
|
||||||
"fastify-type-provider-zod": "^4.0.2",
|
"fastify-type-provider-zod": "^4.0.2",
|
||||||
"fastify-zod": "^1.4.0",
|
"fastify-zod": "^1.4.0",
|
||||||
|
"json-2-csv": "^5.5.10",
|
||||||
"lru-cache": "^11.0.2",
|
"lru-cache": "^11.0.2",
|
||||||
"mongoose": "^8.9.0",
|
"mongoose": "^8.9.0",
|
||||||
"openai": "^5.19.1",
|
"openai": "^5.19.1",
|
||||||
|
|||||||
24
pnpm-lock.yaml
generated
24
pnpm-lock.yaml
generated
@@ -53,6 +53,9 @@ importers:
|
|||||||
fastify-zod:
|
fastify-zod:
|
||||||
specifier: ^1.4.0
|
specifier: ^1.4.0
|
||||||
version: 1.4.0(fastify@5.2.0)
|
version: 1.4.0(fastify@5.2.0)
|
||||||
|
json-2-csv:
|
||||||
|
specifier: ^5.5.10
|
||||||
|
version: 5.5.10
|
||||||
lru-cache:
|
lru-cache:
|
||||||
specifier: ^11.0.2
|
specifier: ^11.0.2
|
||||||
version: 11.0.2
|
version: 11.0.2
|
||||||
@@ -691,6 +694,10 @@ packages:
|
|||||||
supports-color:
|
supports-color:
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
deeks@3.1.0:
|
||||||
|
resolution: {integrity: sha512-e7oWH1LzIdv/prMQ7pmlDlaVoL64glqzvNgkgQNgyec9ORPHrT2jaOqMtRyqJuwWjtfb6v+2rk9pmaHj+F137A==}
|
||||||
|
engines: {node: '>= 16'}
|
||||||
|
|
||||||
delayed-stream@1.0.0:
|
delayed-stream@1.0.0:
|
||||||
resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==}
|
resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==}
|
||||||
engines: {node: '>=0.4.0'}
|
engines: {node: '>=0.4.0'}
|
||||||
@@ -699,6 +706,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==}
|
resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==}
|
||||||
engines: {node: '>= 0.8'}
|
engines: {node: '>= 0.8'}
|
||||||
|
|
||||||
|
doc-path@4.1.1:
|
||||||
|
resolution: {integrity: sha512-h1ErTglQAVv2gCnOpD3sFS6uolDbOKHDU1BZq+Kl3npPqroU3dYL42lUgMfd5UimlwtRgp7C9dLGwqQ5D2HYgQ==}
|
||||||
|
engines: {node: '>=16'}
|
||||||
|
|
||||||
dot-case@3.0.4:
|
dot-case@3.0.4:
|
||||||
resolution: {integrity: sha512-Kv5nKlh6yRrdrGvxeJ2e5y2eRUpkUosIW4A2AS38zwSz27zu7ufDwQPi5Jhs3XAlGNetl3bmnGhQsMtkKJnj3w==}
|
resolution: {integrity: sha512-Kv5nKlh6yRrdrGvxeJ2e5y2eRUpkUosIW4A2AS38zwSz27zu7ufDwQPi5Jhs3XAlGNetl3bmnGhQsMtkKJnj3w==}
|
||||||
|
|
||||||
@@ -855,6 +866,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==}
|
resolution: {integrity: sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
|
json-2-csv@5.5.10:
|
||||||
|
resolution: {integrity: sha512-Dep8wO3Fr5wNjQevO2Z8Y7yeee/nYSGRsi7q6zJDKEVHxXkXT+v21vxHmDX923UzmCXXkSo62HaTz6eTWzFLaw==}
|
||||||
|
engines: {node: '>= 16'}
|
||||||
|
|
||||||
json-schema-ref-resolver@1.0.1:
|
json-schema-ref-resolver@1.0.1:
|
||||||
resolution: {integrity: sha512-EJAj1pgHc1hxF6vo2Z3s69fMjO1INq6eGHXZ8Z6wCQeldCuwxGK9Sxf4/cScGn3FZubCVUehfWtcDM/PLteCQw==}
|
resolution: {integrity: sha512-EJAj1pgHc1hxF6vo2Z3s69fMjO1INq6eGHXZ8Z6wCQeldCuwxGK9Sxf4/cScGn3FZubCVUehfWtcDM/PLteCQw==}
|
||||||
|
|
||||||
@@ -2371,10 +2386,14 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
ms: 2.1.3
|
ms: 2.1.3
|
||||||
|
|
||||||
|
deeks@3.1.0: {}
|
||||||
|
|
||||||
delayed-stream@1.0.0: {}
|
delayed-stream@1.0.0: {}
|
||||||
|
|
||||||
depd@2.0.0: {}
|
depd@2.0.0: {}
|
||||||
|
|
||||||
|
doc-path@4.1.1: {}
|
||||||
|
|
||||||
dot-case@3.0.4:
|
dot-case@3.0.4:
|
||||||
dependencies:
|
dependencies:
|
||||||
no-case: 3.0.4
|
no-case: 3.0.4
|
||||||
@@ -2572,6 +2591,11 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
argparse: 2.0.1
|
argparse: 2.0.1
|
||||||
|
|
||||||
|
json-2-csv@5.5.10:
|
||||||
|
dependencies:
|
||||||
|
deeks: 3.1.0
|
||||||
|
doc-path: 4.1.1
|
||||||
|
|
||||||
json-schema-ref-resolver@1.0.1:
|
json-schema-ref-resolver@1.0.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
fast-deep-equal: 3.1.3
|
fast-deep-equal: 3.1.3
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { FastifyReply, FastifyRequest } from "fastify";
|
import { FastifyReply, FastifyRequest } from "fastify";
|
||||||
import { CreatePermitInput, UpdatePermitInput } from "./permit.schema";
|
import { CreatePermitInput, UpdatePermitInput } from "./permit.schema";
|
||||||
import {
|
import {
|
||||||
|
bulkImport,
|
||||||
createPermit,
|
createPermit,
|
||||||
deletePermit,
|
deletePermit,
|
||||||
getPermit,
|
getPermit,
|
||||||
@@ -10,6 +11,7 @@ import {
|
|||||||
updatePermit,
|
updatePermit,
|
||||||
} from "./permit.service";
|
} from "./permit.service";
|
||||||
import { PageQueryParams } from "../pagination";
|
import { PageQueryParams } from "../pagination";
|
||||||
|
import { csv2json } from "json-2-csv";
|
||||||
|
|
||||||
export async function createPermitHandler(
|
export async function createPermitHandler(
|
||||||
req: FastifyRequest,
|
req: FastifyRequest,
|
||||||
@@ -117,3 +119,20 @@ export async function searchPermitByAddressHandler(
|
|||||||
return err;
|
return err;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function bulkImportHandler(
|
||||||
|
req: FastifyRequest,
|
||||||
|
res: FastifyReply
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
const data = await req.file();
|
||||||
|
const csvString = (await data.toBuffer()).toString("utf-8");
|
||||||
|
|
||||||
|
const parsedCSV = csv2json(csvString, { delimiter: { eol: "\r\n" } });
|
||||||
|
const result = await bulkImport(parsedCSV, req.user);
|
||||||
|
|
||||||
|
return res.code(200).send(result);
|
||||||
|
} catch (err) {
|
||||||
|
return err;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { FastifyInstance } from "fastify";
|
import { FastifyInstance } from "fastify";
|
||||||
import {
|
import {
|
||||||
|
bulkImportHandler,
|
||||||
createPermitHandler,
|
createPermitHandler,
|
||||||
deletePermitHandler,
|
deletePermitHandler,
|
||||||
getPermitHandler,
|
getPermitHandler,
|
||||||
@@ -138,6 +139,15 @@ export async function permitRoutes(fastify: FastifyInstance) {
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
fastify.post(
|
||||||
|
"/bulkImport",
|
||||||
|
{
|
||||||
|
config: { requiredClaims: ["permit:write"] },
|
||||||
|
preHandler: [fastify.authorize],
|
||||||
|
},
|
||||||
|
bulkImportHandler
|
||||||
|
);
|
||||||
|
|
||||||
await noteRoutes(fastify);
|
await noteRoutes(fastify);
|
||||||
|
|
||||||
fastify.addHook("onSend", hideFields("permits"));
|
fastify.addHook("onSend", hideFields("permits"));
|
||||||
|
|||||||
@@ -110,6 +110,7 @@ const permitSchema = new mongoose.Schema({
|
|||||||
relationship: String,
|
relationship: String,
|
||||||
type_text: String,
|
type_text: String,
|
||||||
},
|
},
|
||||||
|
importFlag: Boolean,
|
||||||
});
|
});
|
||||||
|
|
||||||
permitSchema.index({ tenantId: 1, permitNumber: 1 }, { unique: true });
|
permitSchema.index({ tenantId: 1, permitNumber: 1 }, { unique: true });
|
||||||
|
|||||||
@@ -518,3 +518,96 @@ export async function searchPermitByAddress(address: string) {
|
|||||||
.sort({ score: { $meta: "textScore" } })
|
.sort({ score: { $meta: "textScore" } })
|
||||||
.limit(1);
|
.limit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function bulkImport(csvData: Object[], user: AuthenticatedUser) {
|
||||||
|
const allowedFields = [
|
||||||
|
"Permit Number",
|
||||||
|
"County",
|
||||||
|
"Client",
|
||||||
|
"Address",
|
||||||
|
"Open Date",
|
||||||
|
"County Status",
|
||||||
|
"Record Type",
|
||||||
|
"Lot",
|
||||||
|
"Block",
|
||||||
|
"Job Number",
|
||||||
|
"Community Name",
|
||||||
|
];
|
||||||
|
|
||||||
|
const failed = [];
|
||||||
|
const created = [];
|
||||||
|
|
||||||
|
for (const [index, record] of csvData.entries()) {
|
||||||
|
try {
|
||||||
|
if (!record["Permit Number"]) {
|
||||||
|
failed.push(index);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const permitInDb = await permitModel.findOne({
|
||||||
|
permitNumber: record["Permit Number"],
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!permitInDb) {
|
||||||
|
let clientData = null;
|
||||||
|
let countyData = null;
|
||||||
|
|
||||||
|
if (record["Client"]) {
|
||||||
|
const clientInDb = await orgModel.findOne({ name: record["Client"] });
|
||||||
|
if (clientInDb) {
|
||||||
|
clientData = {
|
||||||
|
id: clientInDb._id,
|
||||||
|
pid: clientInDb.pid,
|
||||||
|
licenseNumber: clientInDb.licenseNumber,
|
||||||
|
name: clientInDb.name,
|
||||||
|
avatar: clientInDb.avatar,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (record["County"]) {
|
||||||
|
const countyInDb = await orgModel.findOne({ name: record["County"] });
|
||||||
|
if (countyInDb) {
|
||||||
|
countyData = {
|
||||||
|
id: countyInDb._id,
|
||||||
|
pid: countyInDb.pid,
|
||||||
|
name: countyInDb.name,
|
||||||
|
avatar: countyInDb.avatar,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const newPermit = await permitModel.create({
|
||||||
|
tenantId: user.tenantId,
|
||||||
|
pid: generateId(),
|
||||||
|
permitNumber: record["Permit Number"],
|
||||||
|
county: countyData,
|
||||||
|
client: clientData?.id,
|
||||||
|
clientData: clientData,
|
||||||
|
cleanStatus: record["County Status"],
|
||||||
|
address: record["Address"],
|
||||||
|
recordType: record["Record Type"],
|
||||||
|
lot: record["Lot"],
|
||||||
|
block: record["Block"],
|
||||||
|
jobNumber: record["Job Number"],
|
||||||
|
communityName: record["Community Name"],
|
||||||
|
createdAt: new Date(),
|
||||||
|
createdBy: user.userId,
|
||||||
|
importFlag: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
const populatedPermit = await newPermit.populate({
|
||||||
|
path: "assignedTo createdBy",
|
||||||
|
select: "pid name avatar",
|
||||||
|
});
|
||||||
|
|
||||||
|
created.push(populatedPermit);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.log(err);
|
||||||
|
failed.push(index + 2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { created, failed, allowedFields };
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user