@intlayer/backend 8.7.11 → 8.7.13
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/README.md +27 -1
- package/dist/assets/utils/AI/askDocQuestion/embeddings/docs/en/benchmark/solid.json +4990 -0
- package/dist/assets/utils/AI/askDocQuestion/embeddings/docs/en/benchmark/svelte.json +4987 -0
- package/dist/assets/utils/AI/askDocQuestion/embeddings/docs/en/benchmark/vue.json +4975 -0
- package/dist/assets/utils/AI/askDocQuestion/embeddings/docs/en/intlayer_with_astro_lit.json +9936 -8930
- package/dist/assets/utils/AI/askDocQuestion/embeddings/docs/en/intlayer_with_astro_vanilla.json +7963 -6949
- package/dist/assets/utils/AI/askDocQuestion/embeddings/docs/en/readme.json +12908 -12288
- package/dist/esm/controllers/oAuth2.controller.mjs +21 -1
- package/dist/esm/controllers/oAuth2.controller.mjs.map +1 -1
- package/dist/esm/controllers/stripe.controller.mjs +86 -2
- package/dist/esm/controllers/stripe.controller.mjs.map +1 -1
- package/dist/esm/controllers/translation.controller.mjs +2 -0
- package/dist/esm/controllers/translation.controller.mjs.map +1 -1
- package/dist/esm/emails/InviteUserEmail.mjs +1541 -1
- package/dist/esm/emails/InviteUserEmail.mjs.map +1 -1
- package/dist/esm/emails/MagicLinkEmail.mjs +1128 -1
- package/dist/esm/emails/MagicLinkEmail.mjs.map +1 -1
- package/dist/esm/emails/OAuthTokenCreatedEmail.mjs +1389 -1
- package/dist/esm/emails/OAuthTokenCreatedEmail.mjs.map +1 -1
- package/dist/esm/emails/PasswordChangeConfirmation.mjs +814 -1
- package/dist/esm/emails/PasswordChangeConfirmation.mjs.map +1 -1
- package/dist/esm/emails/ResetUserPassword.mjs +1132 -1
- package/dist/esm/emails/ResetUserPassword.mjs.map +1 -1
- package/dist/esm/emails/SubscriptionPaymentCancellation.mjs +913 -1
- package/dist/esm/emails/SubscriptionPaymentCancellation.mjs.map +1 -1
- package/dist/esm/emails/SubscriptionPaymentError.mjs +908 -1
- package/dist/esm/emails/SubscriptionPaymentError.mjs.map +1 -1
- package/dist/esm/emails/SubscriptionPaymentSuccess.mjs +935 -1
- package/dist/esm/emails/SubscriptionPaymentSuccess.mjs.map +1 -1
- package/dist/esm/emails/ValidateUserEmail.mjs +1111 -1
- package/dist/esm/emails/ValidateUserEmail.mjs.map +1 -1
- package/dist/esm/emails/Welcome.mjs +1004 -1
- package/dist/esm/emails/Welcome.mjs.map +1 -1
- package/dist/esm/emails/index.mjs +7 -7
- package/dist/esm/index.mjs +8 -2
- package/dist/esm/index.mjs.map +1 -1
- package/dist/esm/middlewares/oAuth2.middleware.mjs +2 -2
- package/dist/esm/middlewares/oAuth2.middleware.mjs.map +1 -1
- package/dist/esm/routes/audit.routes.mjs +5 -4
- package/dist/esm/routes/audit.routes.mjs.map +1 -1
- package/dist/esm/routes/dictionary.routes.mjs +4 -3
- package/dist/esm/routes/dictionary.routes.mjs.map +1 -1
- package/dist/esm/routes/organization.routes.mjs +3 -2
- package/dist/esm/routes/organization.routes.mjs.map +1 -1
- package/dist/esm/routes/paramsSchemas.mjs +67 -0
- package/dist/esm/routes/paramsSchemas.mjs.map +1 -0
- package/dist/esm/routes/project.routes.mjs +2 -1
- package/dist/esm/routes/project.routes.mjs.map +1 -1
- package/dist/esm/routes/showcaseProject.routes.mjs +5 -4
- package/dist/esm/routes/showcaseProject.routes.mjs.map +1 -1
- package/dist/esm/routes/stripe.routes.mjs +19 -1
- package/dist/esm/routes/stripe.routes.mjs.map +1 -1
- package/dist/esm/routes/tags.routes.mjs +3 -2
- package/dist/esm/routes/tags.routes.mjs.map +1 -1
- package/dist/esm/routes/translate.routes.mjs +6 -5
- package/dist/esm/routes/translate.routes.mjs.map +1 -1
- package/dist/esm/routes/user.routes.mjs +5 -4
- package/dist/esm/routes/user.routes.mjs.map +1 -1
- package/dist/esm/schemas/oAuth2.schema.mjs +1 -1
- package/dist/esm/schemas/oAuth2.schema.mjs.map +1 -1
- package/dist/esm/services/email.service.mjs +338 -38
- package/dist/esm/services/email.service.mjs.map +1 -1
- package/dist/esm/services/oAuth2.service.mjs +20 -2
- package/dist/esm/services/oAuth2.service.mjs.map +1 -1
- package/dist/esm/services/subscription.service.mjs +5 -2
- package/dist/esm/services/subscription.service.mjs.map +1 -1
- package/dist/esm/utils/AI/askDocQuestion/embeddings/docs/en/benchmark/solid.json +4990 -0
- package/dist/esm/utils/AI/askDocQuestion/embeddings/docs/en/benchmark/svelte.json +4987 -0
- package/dist/esm/utils/AI/askDocQuestion/embeddings/docs/en/benchmark/vue.json +4975 -0
- package/dist/esm/utils/AI/askDocQuestion/embeddings/docs/en/intlayer_with_astro_lit.json +9936 -8930
- package/dist/esm/utils/AI/askDocQuestion/embeddings/docs/en/intlayer_with_astro_vanilla.json +7963 -6949
- package/dist/esm/utils/AI/askDocQuestion/embeddings/docs/en/readme.json +12908 -12288
- package/dist/esm/utils/auth/getAuth.mjs +6 -0
- package/dist/esm/utils/auth/getAuth.mjs.map +1 -1
- package/dist/esm/utils/errors/errorCodes.mjs +3917 -287
- package/dist/esm/utils/errors/errorCodes.mjs.map +1 -1
- package/dist/esm/utils/mongoDB/connectDB.mjs +5 -0
- package/dist/esm/utils/mongoDB/connectDB.mjs.map +1 -1
- package/dist/esm/utils/oAuth2.mjs +6 -2
- package/dist/esm/utils/oAuth2.mjs.map +1 -1
- package/dist/esm/utils/plan.mjs +13 -1
- package/dist/esm/utils/plan.mjs.map +1 -1
- package/dist/types/controllers/oAuth2.controller.d.ts +11 -1
- package/dist/types/controllers/oAuth2.controller.d.ts.map +1 -1
- package/dist/types/controllers/stripe.controller.d.ts +22 -2
- package/dist/types/controllers/stripe.controller.d.ts.map +1 -1
- package/dist/types/controllers/translation.controller.d.ts.map +1 -1
- package/dist/types/emails/InviteUserEmail.d.ts +181 -1
- package/dist/types/emails/InviteUserEmail.d.ts.map +1 -1
- package/dist/types/emails/MagicLinkEmail.d.ts +106 -1
- package/dist/types/emails/MagicLinkEmail.d.ts.map +1 -1
- package/dist/types/emails/OAuthTokenCreatedEmail.d.ts +166 -1
- package/dist/types/emails/OAuthTokenCreatedEmail.d.ts.map +1 -1
- package/dist/types/emails/PasswordChangeConfirmation.d.ts +91 -1
- package/dist/types/emails/PasswordChangeConfirmation.d.ts.map +1 -1
- package/dist/types/emails/ResetUserPassword.d.ts +106 -1
- package/dist/types/emails/ResetUserPassword.d.ts.map +1 -1
- package/dist/types/emails/SubscriptionPaymentCancellation.d.ts +151 -1
- package/dist/types/emails/SubscriptionPaymentCancellation.d.ts.map +1 -1
- package/dist/types/emails/SubscriptionPaymentError.d.ts +151 -1
- package/dist/types/emails/SubscriptionPaymentError.d.ts.map +1 -1
- package/dist/types/emails/SubscriptionPaymentSuccess.d.ts +151 -1
- package/dist/types/emails/SubscriptionPaymentSuccess.d.ts.map +1 -1
- package/dist/types/emails/ValidateUserEmail.d.ts +106 -1
- package/dist/types/emails/ValidateUserEmail.d.ts.map +1 -1
- package/dist/types/emails/Welcome.d.ts +106 -1
- package/dist/types/emails/Welcome.d.ts.map +1 -1
- package/dist/types/emails/index.d.ts +7 -7
- package/dist/types/export.d.ts +3 -3
- package/dist/types/middlewares/oAuth2.middleware.d.ts.map +1 -1
- package/dist/types/routes/audit.routes.d.ts.map +1 -1
- package/dist/types/routes/dictionary.routes.d.ts.map +1 -1
- package/dist/types/routes/organization.routes.d.ts.map +1 -1
- package/dist/types/routes/paramsSchemas.d.ts +102 -0
- package/dist/types/routes/paramsSchemas.d.ts.map +1 -0
- package/dist/types/routes/project.routes.d.ts.map +1 -1
- package/dist/types/routes/showcaseProject.routes.d.ts.map +1 -1
- package/dist/types/routes/stripe.routes.d.ts +15 -0
- package/dist/types/routes/stripe.routes.d.ts.map +1 -1
- package/dist/types/routes/tags.routes.d.ts.map +1 -1
- package/dist/types/routes/translate.routes.d.ts.map +1 -1
- package/dist/types/routes/user.routes.d.ts.map +1 -1
- package/dist/types/schemas/dictionary.schema.d.ts +6 -6
- package/dist/types/schemas/discussion.schema.d.ts +9 -9
- package/dist/types/schemas/organization.schema.d.ts +9 -9
- package/dist/types/schemas/plans.schema.d.ts +6 -6
- package/dist/types/schemas/project.schema.d.ts +12 -12
- package/dist/types/schemas/session.schema.d.ts +8 -8
- package/dist/types/schemas/showcaseProject.schema.d.ts +19 -19
- package/dist/types/schemas/tag.schema.d.ts +7 -7
- package/dist/types/services/email.service.d.ts.map +1 -1
- package/dist/types/services/oAuth2.service.d.ts +6 -1
- package/dist/types/services/oAuth2.service.d.ts.map +1 -1
- package/dist/types/types/plan.types.d.ts +2 -2
- package/dist/types/utils/errors/errorCodes.d.ts +3634 -4
- package/dist/types/utils/errors/errorCodes.d.ts.map +1 -1
- package/dist/types/utils/mongoDB/connectDB.d.ts.map +1 -1
- package/dist/types/utils/oAuth2.d.ts +3 -1
- package/dist/types/utils/oAuth2.d.ts.map +1 -1
- package/dist/types/utils/plan.d.ts +2 -1
- package/dist/types/utils/plan.d.ts.map +1 -1
- package/package.json +14 -13
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { formatResponse } from "../utils/responseData.mjs";
|
|
2
2
|
import { ErrorHandler } from "../utils/errors/ErrorHandler.mjs";
|
|
3
|
+
import { GenericError } from "../utils/errors/ErrorsClass.mjs";
|
|
4
|
+
import { extendOAuth2AccessToken } from "../services/oAuth2.service.mjs";
|
|
3
5
|
import { Request, Response } from "oauth2-server";
|
|
4
6
|
|
|
5
7
|
//#region src/controllers/oAuth2.controller.ts
|
|
@@ -18,7 +20,25 @@ const getOAuth2AccessToken = async (request, reply) => {
|
|
|
18
20
|
return ErrorHandler.handleAppErrorResponse(reply, error);
|
|
19
21
|
}
|
|
20
22
|
};
|
|
23
|
+
/**
|
|
24
|
+
* Extend the lifetime of the bearer token attached to the current request.
|
|
25
|
+
* Lets long-running clients keep using the same token instead of
|
|
26
|
+
* re-authenticating on a fixed schedule.
|
|
27
|
+
*/
|
|
28
|
+
const extendOAuth2Token = async (request, reply) => {
|
|
29
|
+
try {
|
|
30
|
+
const accessToken = request.headers.authorization?.match(/^Bearer\s+(.+)$/i)?.[1]?.trim();
|
|
31
|
+
if (!accessToken) throw new GenericError("INVALID_ACCESS_TOKEN");
|
|
32
|
+
const accessTokenExpiresAt = await extendOAuth2AccessToken(accessToken);
|
|
33
|
+
return reply.send(formatResponse({ data: {
|
|
34
|
+
accessToken,
|
|
35
|
+
accessTokenExpiresAt
|
|
36
|
+
} }));
|
|
37
|
+
} catch (error) {
|
|
38
|
+
return ErrorHandler.handleAppErrorResponse(reply, error);
|
|
39
|
+
}
|
|
40
|
+
};
|
|
21
41
|
|
|
22
42
|
//#endregion
|
|
23
|
-
export { getOAuth2AccessToken };
|
|
43
|
+
export { extendOAuth2Token, getOAuth2AccessToken };
|
|
24
44
|
//# sourceMappingURL=oAuth2.controller.mjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"oAuth2.controller.mjs","names":["OAuthRequest","OAuthResponse"],"sources":["../../../src/controllers/oAuth2.controller.ts"],"sourcesContent":["import type { RequestWithOAuth2Information } from '@middlewares/oAuth2.middleware';\nimport { type AppError, ErrorHandler } from '@utils/errors';\nimport { formatResponse, type ResponseData } from '@utils/responseData';\nimport type { FastifyReply, FastifyRequest } from 'fastify';\nimport {\n Request as OAuthRequest,\n Response as OAuthResponse,\n} from 'oauth2-server';\nimport type { OAuth2Token } from '@/types/oAuth2.types';\n\nexport type GetOAuth2TokenBody = {\n grant_type: 'client_credentials';\n client_id: string;\n client_secret: string;\n};\nexport type GetOAuth2TokenResult = ResponseData<OAuth2Token>;\n\n// Method to get the token\nexport const getOAuth2AccessToken = async (\n request: FastifyRequest<{ Body: GetOAuth2TokenBody }>,\n reply: FastifyReply\n): Promise<void> => {\n const oauthRequest = new OAuthRequest({\n headers: request.headers,\n method: request.method,\n query: request.query as any,\n body: request.body as any,\n });\n const oauthResponse = new OAuthResponse(reply.raw);\n\n try {\n const token: OAuth2Token = (await (\n request as unknown as RequestWithOAuth2Information\n ).oauth.token(oauthRequest, oauthResponse)) as OAuth2Token;\n\n const responseData = formatResponse<OAuth2Token>({\n data: token,\n });\n\n return reply.send(responseData);\n } catch (error) {\n return ErrorHandler.handleAppErrorResponse(reply, error as AppError);\n }\n};\n"],"mappings":"
|
|
1
|
+
{"version":3,"file":"oAuth2.controller.mjs","names":["OAuthRequest","OAuthResponse"],"sources":["../../../src/controllers/oAuth2.controller.ts"],"sourcesContent":["import type { RequestWithOAuth2Information } from '@middlewares/oAuth2.middleware';\nimport { extendOAuth2AccessToken } from '@services/oAuth2.service';\nimport { type AppError, ErrorHandler, GenericError } from '@utils/errors';\nimport { formatResponse, type ResponseData } from '@utils/responseData';\nimport type { FastifyReply, FastifyRequest } from 'fastify';\nimport {\n Request as OAuthRequest,\n Response as OAuthResponse,\n} from 'oauth2-server';\nimport type { OAuth2Token } from '@/types/oAuth2.types';\n\nexport type GetOAuth2TokenBody = {\n grant_type: 'client_credentials';\n client_id: string;\n client_secret: string;\n};\nexport type GetOAuth2TokenResult = ResponseData<OAuth2Token>;\n\n// Method to get the token\nexport const getOAuth2AccessToken = async (\n request: FastifyRequest<{ Body: GetOAuth2TokenBody }>,\n reply: FastifyReply\n): Promise<void> => {\n const oauthRequest = new OAuthRequest({\n headers: request.headers,\n method: request.method,\n query: request.query as any,\n body: request.body as any,\n });\n const oauthResponse = new OAuthResponse(reply.raw);\n\n try {\n const token: OAuth2Token = (await (\n request as unknown as RequestWithOAuth2Information\n ).oauth.token(oauthRequest, oauthResponse)) as OAuth2Token;\n\n const responseData = formatResponse<OAuth2Token>({\n data: token,\n });\n\n return reply.send(responseData);\n } catch (error) {\n return ErrorHandler.handleAppErrorResponse(reply, error as AppError);\n }\n};\n\nexport type ExtendOAuth2TokenResult = ResponseData<{\n accessToken: string;\n accessTokenExpiresAt: Date;\n}>;\n\n/**\n * Extend the lifetime of the bearer token attached to the current request.\n * Lets long-running clients keep using the same token instead of\n * re-authenticating on a fixed schedule.\n */\nexport const extendOAuth2Token = async (\n request: FastifyRequest,\n reply: FastifyReply\n): Promise<void> => {\n try {\n const authorization = request.headers.authorization;\n const accessToken = authorization?.match(/^Bearer\\s+(.+)$/i)?.[1]?.trim();\n\n if (!accessToken) {\n throw new GenericError('INVALID_ACCESS_TOKEN');\n }\n\n const accessTokenExpiresAt = await extendOAuth2AccessToken(accessToken);\n\n return reply.send(\n formatResponse<{ accessToken: string; accessTokenExpiresAt: Date }>({\n data: { accessToken, accessTokenExpiresAt },\n })\n );\n } catch (error) {\n return ErrorHandler.handleAppErrorResponse(reply, error as AppError);\n }\n};\n"],"mappings":";;;;;;;AAmBA,MAAa,uBAAuB,OAClC,SACA,UACkB;CAClB,MAAM,eAAe,IAAIA,QAAa;EACpC,SAAS,QAAQ;EACjB,QAAQ,QAAQ;EAChB,OAAO,QAAQ;EACf,MAAM,QAAQ;EACf,CAAC;CACF,MAAM,gBAAgB,IAAIC,SAAc,MAAM,IAAI;AAElD,KAAI;EAKF,MAAM,eAAe,eAA4B,EAC/C,MAAM,MAJN,QACA,MAAM,MAAM,cAAc,cAAc,EAIzC,CAAC;AAEF,SAAO,MAAM,KAAK,aAAa;UACxB,OAAO;AACd,SAAO,aAAa,uBAAuB,OAAO,MAAkB;;;;;;;;AAcxE,MAAa,oBAAoB,OAC/B,SACA,UACkB;AAClB,KAAI;EAEF,MAAM,cADgB,QAAQ,QAAQ,eACH,MAAM,mBAAmB,GAAG,IAAI,MAAM;AAEzE,MAAI,CAAC,YACH,OAAM,IAAI,aAAa,uBAAuB;EAGhD,MAAM,uBAAuB,MAAM,wBAAwB,YAAY;AAEvE,SAAO,MAAM,KACX,eAAoE,EAClE,MAAM;GAAE;GAAa;GAAsB,EAC5C,CAAC,CACH;UACM,OAAO;AACd,SAAO,aAAa,uBAAuB,OAAO,MAAkB"}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { formatResponse } from "../utils/responseData.mjs";
|
|
2
2
|
import { ErrorHandler } from "../utils/errors/ErrorHandler.mjs";
|
|
3
3
|
import { sendEmail } from "../services/email.service.mjs";
|
|
4
|
-
import { retrievePlanInformation } from "../utils/plan.mjs";
|
|
4
|
+
import { isLifetimePriceId, retrievePlanInformation } from "../utils/plan.mjs";
|
|
5
5
|
import { cancelSubscription as cancelSubscription$1, getCouponId, getPricing as getPricing$1 } from "../services/subscription.service.mjs";
|
|
6
6
|
import { t } from "fastify-intlayer";
|
|
7
7
|
import Stripe from "stripe";
|
|
@@ -37,6 +37,41 @@ const getSubscription = async (request, reply) => {
|
|
|
37
37
|
locale: request.session.locale
|
|
38
38
|
} })).id;
|
|
39
39
|
const promoCodeId = promoCode ? await getCouponId(promoCode) : null;
|
|
40
|
+
if (isLifetimePriceId(priceId)) {
|
|
41
|
+
const price = await stripe.prices.retrieve(priceId);
|
|
42
|
+
if (!price.unit_amount) return ErrorHandler.handleGenericErrorResponse(reply, "SUBSCRIPTION_CREATION_FAILED", {
|
|
43
|
+
user,
|
|
44
|
+
organization,
|
|
45
|
+
priceId
|
|
46
|
+
});
|
|
47
|
+
let amount = price.unit_amount;
|
|
48
|
+
if (promoCodeId) {
|
|
49
|
+
const coupon = await stripe.coupons.retrieve(promoCodeId);
|
|
50
|
+
if (coupon.percent_off) amount = Math.round(amount * (1 - coupon.percent_off / 100));
|
|
51
|
+
else if (coupon.amount_off) amount = Math.max(0, amount - coupon.amount_off);
|
|
52
|
+
}
|
|
53
|
+
const paymentIntent = await stripe.paymentIntents.create({
|
|
54
|
+
customer: customerId,
|
|
55
|
+
amount,
|
|
56
|
+
currency: price.currency,
|
|
57
|
+
payment_method_types: ["card"],
|
|
58
|
+
metadata: {
|
|
59
|
+
organizationId: String(organization.id),
|
|
60
|
+
userId: String(user.id),
|
|
61
|
+
priceId,
|
|
62
|
+
purchaseType: "lifetime"
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
if (!paymentIntent?.client_secret) return ErrorHandler.handleGenericErrorResponse(reply, "SUBSCRIPTION_CREATION_FAILED", {
|
|
66
|
+
user,
|
|
67
|
+
organization,
|
|
68
|
+
priceId
|
|
69
|
+
});
|
|
70
|
+
return reply.send(formatResponse({ data: {
|
|
71
|
+
paymentIntent,
|
|
72
|
+
clientSecret: paymentIntent.client_secret
|
|
73
|
+
} }));
|
|
74
|
+
}
|
|
40
75
|
const discounts = promoCodeId ? [{ coupon: promoCodeId }] : [];
|
|
41
76
|
const subscription = await stripe.subscriptions.create({
|
|
42
77
|
customer: customerId,
|
|
@@ -107,7 +142,56 @@ const cancelSubscription = async (_request, reply) => {
|
|
|
107
142
|
return ErrorHandler.handleAppErrorResponse(reply, error);
|
|
108
143
|
}
|
|
109
144
|
};
|
|
145
|
+
/** Pulls the Stripe customer ID from the authenticated organization's plan. */
|
|
146
|
+
const getCustomerIdFromSession = (request) => request.session?.organization?.plan?.customerId;
|
|
147
|
+
/**
|
|
148
|
+
* Lists Stripe invoices for the authenticated organization's customer.
|
|
149
|
+
*/
|
|
150
|
+
const getInvoices = async (request, reply) => {
|
|
151
|
+
try {
|
|
152
|
+
const customerId = getCustomerIdFromSession(request);
|
|
153
|
+
if (!customerId) return ErrorHandler.handleGenericErrorResponse(reply, "ORGANIZATION_PLAN_NOT_FOUND");
|
|
154
|
+
const invoices = await new Stripe(process.env.STRIPE_SECRET_KEY).invoices.list({ customer: customerId });
|
|
155
|
+
return reply.send(formatResponse({ data: invoices.data }));
|
|
156
|
+
} catch (error) {
|
|
157
|
+
return ErrorHandler.handleAppErrorResponse(reply, error);
|
|
158
|
+
}
|
|
159
|
+
};
|
|
160
|
+
/**
|
|
161
|
+
* Returns the first card payment method attached to the authenticated
|
|
162
|
+
* organization's Stripe customer (or null if none).
|
|
163
|
+
*/
|
|
164
|
+
const getPaymentMethod = async (request, reply) => {
|
|
165
|
+
try {
|
|
166
|
+
const customerId = getCustomerIdFromSession(request);
|
|
167
|
+
if (!customerId) return ErrorHandler.handleGenericErrorResponse(reply, "ORGANIZATION_PLAN_NOT_FOUND");
|
|
168
|
+
const paymentMethods = await new Stripe(process.env.STRIPE_SECRET_KEY).paymentMethods.list({
|
|
169
|
+
customer: customerId,
|
|
170
|
+
type: "card"
|
|
171
|
+
});
|
|
172
|
+
return reply.send(formatResponse({ data: paymentMethods.data[0] ?? null }));
|
|
173
|
+
} catch (error) {
|
|
174
|
+
return ErrorHandler.handleAppErrorResponse(reply, error);
|
|
175
|
+
}
|
|
176
|
+
};
|
|
177
|
+
/**
|
|
178
|
+
* Creates a Stripe Billing Portal session for the authenticated organization
|
|
179
|
+
* so the user can manage their payment method and subscription.
|
|
180
|
+
*/
|
|
181
|
+
const createPortalSession = async (request, reply) => {
|
|
182
|
+
try {
|
|
183
|
+
const customerId = getCustomerIdFromSession(request);
|
|
184
|
+
if (!customerId) return ErrorHandler.handleGenericErrorResponse(reply, "ORGANIZATION_PLAN_NOT_FOUND");
|
|
185
|
+
const session = await new Stripe(process.env.STRIPE_SECRET_KEY).billingPortal.sessions.create({
|
|
186
|
+
customer: customerId,
|
|
187
|
+
return_url: `${process.env.APP_URL ?? "http://localhost:3000"}/dashboard`
|
|
188
|
+
});
|
|
189
|
+
return reply.send(formatResponse({ data: { url: session.url } }));
|
|
190
|
+
} catch (error) {
|
|
191
|
+
return ErrorHandler.handleAppErrorResponse(reply, error);
|
|
192
|
+
}
|
|
193
|
+
};
|
|
110
194
|
|
|
111
195
|
//#endregion
|
|
112
|
-
export { cancelSubscription, getPricing, getSubscription };
|
|
196
|
+
export { cancelSubscription, createPortalSession, getInvoices, getPaymentMethod, getPricing, getSubscription };
|
|
113
197
|
//# sourceMappingURL=stripe.controller.mjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"stripe.controller.mjs","names":["subscriptionService.getPricing","subscriptionService.getCouponId","subscriptionService.cancelSubscription","emailService.sendEmail"],"sources":["../../../src/controllers/stripe.controller.ts"],"sourcesContent":["import type { Locale } from '@intlayer/types/allLocales';\nimport * as emailService from '@services/email.service';\nimport * as subscriptionService from '@services/subscription.service';\nimport { type AppError, ErrorHandler } from '@utils/errors';\nimport { retrievePlanInformation } from '@utils/plan';\nimport { formatResponse, type ResponseData } from '@utils/responseData';\nimport type { FastifyReply, FastifyRequest } from 'fastify';\nimport { t } from 'fastify-intlayer';\nimport Stripe from 'stripe';\nimport type { Organization } from '@/types/organization.types';\n\nexport type GetPricingBody = {\n priceIds: string[];\n promoCode?: string;\n};\n\nexport type GetPricingResult = ResponseData<subscriptionService.PricingResult>;\n\n/**\n * Simulate pricing for a given set of prices and a promotion code.\n *\n * @param request - The request object containing the price IDs and promotion code.\n * @param reply - The response object to send the simulated pricing result.\n */\nexport const getPricing = async (\n request: FastifyRequest<{ Body: GetPricingBody }>,\n reply: FastifyReply\n) => {\n const { priceIds, promoCode } = request.body;\n\n const pricingResult = await subscriptionService.getPricing(\n priceIds,\n promoCode\n );\n\n const formattedPricingResult =\n formatResponse<subscriptionService.PricingResult>({\n data: pricingResult,\n });\n\n reply.code(200).send(formattedPricingResult);\n};\n\nexport type GetCheckoutSessionBody = {\n priceId: string;\n promoCode?: string;\n};\n\nexport type GetCheckoutSessionResult = ResponseData<{\n subscription: Stripe.Response<Stripe.Subscription>;\n clientSecret: string;\n}>;\n\n/**\n * Handles subscription creation or update with Stripe and returns a ClientSecret.\n */\nexport const getSubscription = async (\n request: FastifyRequest<{ Body: GetCheckoutSessionBody }>,\n reply: FastifyReply\n): Promise<void> => {\n try {\n const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!);\n\n // Extract organization and user from request locals (set by authentication middleware)\n const { organization, user } = request.session || {};\n const { priceId, promoCode } = request.body;\n\n // Validate that the organization exists\n if (!organization) {\n return ErrorHandler.handleGenericErrorResponse(\n reply,\n 'ORGANIZATION_NOT_FOUND'\n );\n }\n\n // Validate that the user exists\n if (!user) {\n return ErrorHandler.handleGenericErrorResponse(reply, 'USER_NOT_FOUND');\n }\n\n const { period, type } = retrievePlanInformation(priceId);\n\n if (\n organization.plan?.subscriptionId &&\n organization.plan?.type === type &&\n organization.plan?.period === period &&\n organization.plan?.status === 'active'\n ) {\n return ErrorHandler.handleGenericErrorResponse(\n reply,\n 'ALREADY_SUBSCRIBED',\n {\n organizationId: organization.id,\n }\n );\n }\n\n // Attempt to retrieve the Stripe customer ID from the organization's plan\n let customerId = organization.plan?.customerId;\n\n if (!customerId) {\n // If no customer ID exists, create a new Stripe customer for the organization\n const customer = await stripe.customers.create({\n metadata: {\n organizationId: String(organization.id),\n userId: String(user.id),\n // Include the locale for potential localization\n locale: (request.session as unknown as { locale: Locale }).locale,\n },\n });\n customerId = customer.id;\n }\n\n const promoCodeId = promoCode\n ? await subscriptionService.getCouponId(promoCode)\n : null;\n\n const discounts: Stripe.SubscriptionCreateParams.Discount[] = promoCodeId\n ? [{ coupon: promoCodeId }]\n : [];\n\n // If no subscription exists, create a new one\n const subscription = await stripe.subscriptions.create({\n customer: customerId, // Associate the subscription with the customer\n items: [{ price: priceId }], // Set the price ID for the subscription\n expand: ['latest_invoice.confirmation_secret'],\n payment_settings: {\n payment_method_types: ['card'], // Specify payment method types\n },\n payment_behavior: 'default_incomplete', // Create the subscription in an incomplete state until payment is confirmed\n discounts,\n });\n\n // Handle subscription creation failure\n if (!subscription) {\n return ErrorHandler.handleGenericErrorResponse(\n reply,\n 'SUBSCRIPTION_CREATION_FAILED',\n {\n user,\n organization,\n priceId,\n }\n );\n }\n\n const clientSecret = (\n subscription.latest_invoice as Stripe.Invoice & {\n confirmation_secret?: { client_secret: string };\n }\n )?.confirmation_secret?.client_secret;\n\n // Handle subscription creation failure\n if (!clientSecret) {\n return ErrorHandler.handleGenericErrorResponse(\n reply,\n 'SUBSCRIPTION_CREATION_FAILED',\n {\n user,\n organization,\n priceId,\n }\n );\n }\n\n // Prepare the response data with subscription details\n const responseData = formatResponse<GetCheckoutSessionResult['data']>({\n data: { subscription, clientSecret },\n });\n\n // Send the response back to the client\n return reply.send(responseData);\n } catch (error) {\n // Handle any errors that occur during the process\n\n return ErrorHandler.handleAppErrorResponse(reply, error as AppError);\n }\n};\n\ntype CancelSubscriptionData = Organization['plan'];\nexport type CancelSubscriptionResult = ResponseData<CancelSubscriptionData>;\n\n/**\n * Cancels a subscription for an organization.\n */\nexport const cancelSubscription = async (\n _request: FastifyRequest,\n reply: FastifyReply\n): Promise<void> => {\n const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!);\n\n try {\n // Extract the organization and user from the request locals\n // These are typically set by authentication middleware earlier in the request pipeline\n const { organization, user } = _request.session || {};\n\n // Validate that the organization exists\n if (!organization) {\n return ErrorHandler.handleGenericErrorResponse(\n reply,\n 'ORGANIZATION_NOT_FOUND'\n );\n }\n\n // Validate that the user exists\n if (!user) {\n return ErrorHandler.handleGenericErrorResponse(reply, 'USER_NOT_FOUND');\n }\n\n // Try to get the subscription ID from the organization's plan\n const subscriptionId = organization.plan?.subscriptionId;\n\n if (subscriptionId) {\n // Cancel the subscription on Stripe immediately using the subscription ID\n await stripe.subscriptions.cancel(subscriptionId);\n }\n\n // Update the organization's plan in the database to reflect the cancellation\n const plan = await subscriptionService.cancelSubscription(\n subscriptionId,\n String(organization.id)\n );\n\n // If the plan could not be updated in the database, handle the error\n if (!plan) {\n return ErrorHandler.handleGenericErrorResponse(\n reply,\n 'ORGANIZATION_PLAN_NOT_FOUND'\n );\n }\n\n // Prepare a formatted response with a success message and the updated plan data\n const formattedPlan = formatResponse<CancelSubscriptionData>({\n message: t({\n en: 'Subscription cancelled successfully',\n fr: 'Souscription annulée avec succès',\n es: 'Suscripción cancelada con éxito',\n }),\n description: t({\n en: 'Your subscription has been cancelled successfully',\n fr: 'Votre souscription a été annulée avec succès',\n es: 'Su suscripción ha sido cancelada con éxito',\n }),\n data: plan!,\n });\n\n // Send the response back to the client\n reply.send(formattedPlan);\n\n await emailService.sendEmail({\n type: 'subscriptionPaymentCancellation',\n to: user.email,\n email: user.email,\n cancellationDate: new Date().toLocaleDateString(),\n reactivateLink: `${process.env.APP_URL}/pricing`,\n username: user.name,\n organizationName: organization.name,\n planName: plan.type,\n });\n } catch (error) {\n // Handle any errors that occur during the cancellation process\n return ErrorHandler.handleAppErrorResponse(reply, error as AppError);\n }\n};\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"stripe.controller.mjs","names":["subscriptionService.getPricing","subscriptionService.getCouponId","subscriptionService.cancelSubscription","emailService.sendEmail"],"sources":["../../../src/controllers/stripe.controller.ts"],"sourcesContent":["import type { Locale } from '@intlayer/types/allLocales';\nimport * as emailService from '@services/email.service';\nimport * as subscriptionService from '@services/subscription.service';\nimport { type AppError, ErrorHandler } from '@utils/errors';\nimport { isLifetimePriceId, retrievePlanInformation } from '@utils/plan';\nimport { formatResponse, type ResponseData } from '@utils/responseData';\nimport type { FastifyReply, FastifyRequest } from 'fastify';\nimport { t } from 'fastify-intlayer';\nimport Stripe from 'stripe';\nimport type { Organization } from '@/types/organization.types';\n\nexport type GetPricingBody = {\n priceIds: string[];\n promoCode?: string;\n};\n\nexport type GetPricingResult = ResponseData<subscriptionService.PricingResult>;\n\n/**\n * Simulate pricing for a given set of prices and a promotion code.\n *\n * @param request - The request object containing the price IDs and promotion code.\n * @param reply - The response object to send the simulated pricing result.\n */\nexport const getPricing = async (\n request: FastifyRequest<{ Body: GetPricingBody }>,\n reply: FastifyReply\n) => {\n const { priceIds, promoCode } = request.body;\n\n const pricingResult = await subscriptionService.getPricing(\n priceIds,\n promoCode\n );\n\n const formattedPricingResult =\n formatResponse<subscriptionService.PricingResult>({\n data: pricingResult,\n });\n\n reply.code(200).send(formattedPricingResult);\n};\n\nexport type GetCheckoutSessionBody = {\n priceId: string;\n promoCode?: string;\n};\n\nexport type GetCheckoutSessionResult = ResponseData<{\n subscription?: Stripe.Response<Stripe.Subscription>;\n paymentIntent?: Stripe.Response<Stripe.PaymentIntent>;\n clientSecret: string;\n}>;\n\n/**\n * Handles subscription creation or update with Stripe and returns a ClientSecret.\n */\nexport const getSubscription = async (\n request: FastifyRequest<{ Body: GetCheckoutSessionBody }>,\n reply: FastifyReply\n): Promise<void> => {\n try {\n const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!);\n\n // Extract organization and user from request locals (set by authentication middleware)\n const { organization, user } = request.session || {};\n const { priceId, promoCode } = request.body;\n\n // Validate that the organization exists\n if (!organization) {\n return ErrorHandler.handleGenericErrorResponse(\n reply,\n 'ORGANIZATION_NOT_FOUND'\n );\n }\n\n // Validate that the user exists\n if (!user) {\n return ErrorHandler.handleGenericErrorResponse(reply, 'USER_NOT_FOUND');\n }\n\n const { period, type } = retrievePlanInformation(priceId);\n\n if (\n organization.plan?.subscriptionId &&\n organization.plan?.type === type &&\n organization.plan?.period === period &&\n organization.plan?.status === 'active'\n ) {\n return ErrorHandler.handleGenericErrorResponse(\n reply,\n 'ALREADY_SUBSCRIBED',\n {\n organizationId: organization.id,\n }\n );\n }\n\n // Attempt to retrieve the Stripe customer ID from the organization's plan\n let customerId = organization.plan?.customerId;\n\n if (!customerId) {\n // If no customer ID exists, create a new Stripe customer for the organization\n const customer = await stripe.customers.create({\n metadata: {\n organizationId: String(organization.id),\n userId: String(user.id),\n // Include the locale for potential localization\n locale: (request.session as unknown as { locale: Locale }).locale,\n },\n });\n customerId = customer.id;\n }\n\n const promoCodeId = promoCode\n ? await subscriptionService.getCouponId(promoCode)\n : null;\n\n // Lifetime / one-time payment — handled with a PaymentIntent rather than a\n // recurring subscription so the price is charged once.\n if (isLifetimePriceId(priceId)) {\n const price = await stripe.prices.retrieve(priceId);\n\n if (!price.unit_amount) {\n return ErrorHandler.handleGenericErrorResponse(\n reply,\n 'SUBSCRIPTION_CREATION_FAILED',\n { user, organization, priceId }\n );\n }\n\n let amount = price.unit_amount;\n\n if (promoCodeId) {\n const coupon = await stripe.coupons.retrieve(promoCodeId);\n if (coupon.percent_off) {\n amount = Math.round(amount * (1 - coupon.percent_off / 100));\n } else if (coupon.amount_off) {\n amount = Math.max(0, amount - coupon.amount_off);\n }\n }\n\n const paymentIntent = await stripe.paymentIntents.create({\n customer: customerId,\n amount,\n currency: price.currency,\n payment_method_types: ['card'],\n metadata: {\n organizationId: String(organization.id),\n userId: String(user.id),\n priceId,\n purchaseType: 'lifetime',\n },\n });\n\n if (!paymentIntent?.client_secret) {\n return ErrorHandler.handleGenericErrorResponse(\n reply,\n 'SUBSCRIPTION_CREATION_FAILED',\n { user, organization, priceId }\n );\n }\n\n return reply.send(\n formatResponse<GetCheckoutSessionResult['data']>({\n data: {\n paymentIntent,\n clientSecret: paymentIntent.client_secret,\n },\n })\n );\n }\n\n const discounts: Stripe.SubscriptionCreateParams.Discount[] = promoCodeId\n ? [{ coupon: promoCodeId }]\n : [];\n\n // If no subscription exists, create a new one\n const subscription = await stripe.subscriptions.create({\n customer: customerId, // Associate the subscription with the customer\n items: [{ price: priceId }], // Set the price ID for the subscription\n expand: ['latest_invoice.confirmation_secret'],\n payment_settings: {\n payment_method_types: ['card'], // Specify payment method types\n },\n payment_behavior: 'default_incomplete', // Create the subscription in an incomplete state until payment is confirmed\n discounts,\n });\n\n // Handle subscription creation failure\n if (!subscription) {\n return ErrorHandler.handleGenericErrorResponse(\n reply,\n 'SUBSCRIPTION_CREATION_FAILED',\n {\n user,\n organization,\n priceId,\n }\n );\n }\n\n const clientSecret = (\n subscription.latest_invoice as Stripe.Invoice & {\n confirmation_secret?: { client_secret: string };\n }\n )?.confirmation_secret?.client_secret;\n\n // Handle subscription creation failure\n if (!clientSecret) {\n return ErrorHandler.handleGenericErrorResponse(\n reply,\n 'SUBSCRIPTION_CREATION_FAILED',\n {\n user,\n organization,\n priceId,\n }\n );\n }\n\n // Prepare the response data with subscription details\n const responseData = formatResponse<GetCheckoutSessionResult['data']>({\n data: { subscription, clientSecret },\n });\n\n // Send the response back to the client\n return reply.send(responseData);\n } catch (error) {\n // Handle any errors that occur during the process\n\n return ErrorHandler.handleAppErrorResponse(reply, error as AppError);\n }\n};\n\ntype CancelSubscriptionData = Organization['plan'];\nexport type CancelSubscriptionResult = ResponseData<CancelSubscriptionData>;\n\n/**\n * Cancels a subscription for an organization.\n */\nexport const cancelSubscription = async (\n _request: FastifyRequest,\n reply: FastifyReply\n): Promise<void> => {\n const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!);\n\n try {\n // Extract the organization and user from the request locals\n // These are typically set by authentication middleware earlier in the request pipeline\n const { organization, user } = _request.session || {};\n\n // Validate that the organization exists\n if (!organization) {\n return ErrorHandler.handleGenericErrorResponse(\n reply,\n 'ORGANIZATION_NOT_FOUND'\n );\n }\n\n // Validate that the user exists\n if (!user) {\n return ErrorHandler.handleGenericErrorResponse(reply, 'USER_NOT_FOUND');\n }\n\n // Try to get the subscription ID from the organization's plan\n const subscriptionId = organization.plan?.subscriptionId;\n\n if (subscriptionId) {\n // Cancel the subscription on Stripe immediately using the subscription ID\n await stripe.subscriptions.cancel(subscriptionId);\n }\n\n // Update the organization's plan in the database to reflect the cancellation\n const plan = await subscriptionService.cancelSubscription(\n subscriptionId,\n String(organization.id)\n );\n\n // If the plan could not be updated in the database, handle the error\n if (!plan) {\n return ErrorHandler.handleGenericErrorResponse(\n reply,\n 'ORGANIZATION_PLAN_NOT_FOUND'\n );\n }\n\n // Prepare a formatted response with a success message and the updated plan data\n const formattedPlan = formatResponse<CancelSubscriptionData>({\n message: t({\n en: 'Subscription cancelled successfully',\n fr: 'Souscription annulée avec succès',\n es: 'Suscripción cancelada con éxito',\n }),\n description: t({\n en: 'Your subscription has been cancelled successfully',\n fr: 'Votre souscription a été annulée avec succès',\n es: 'Su suscripción ha sido cancelada con éxito',\n }),\n data: plan!,\n });\n\n // Send the response back to the client\n reply.send(formattedPlan);\n\n await emailService.sendEmail({\n type: 'subscriptionPaymentCancellation',\n to: user.email,\n email: user.email,\n cancellationDate: new Date().toLocaleDateString(),\n reactivateLink: `${process.env.APP_URL}/pricing`,\n username: user.name,\n organizationName: organization.name,\n planName: plan.type,\n });\n } catch (error) {\n // Handle any errors that occur during the cancellation process\n return ErrorHandler.handleAppErrorResponse(reply, error as AppError);\n }\n};\n\n/** Pulls the Stripe customer ID from the authenticated organization's plan. */\nconst getCustomerIdFromSession = (\n request: FastifyRequest\n): string | undefined => request.session?.organization?.plan?.customerId;\n\nexport type GetInvoicesResult = ResponseData<Stripe.Invoice[]>;\n\n/**\n * Lists Stripe invoices for the authenticated organization's customer.\n */\nexport const getInvoices = async (\n request: FastifyRequest,\n reply: FastifyReply\n): Promise<void> => {\n try {\n const customerId = getCustomerIdFromSession(request);\n\n if (!customerId) {\n return ErrorHandler.handleGenericErrorResponse(\n reply,\n 'ORGANIZATION_PLAN_NOT_FOUND'\n );\n }\n\n const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!);\n const invoices = await stripe.invoices.list({ customer: customerId });\n\n return reply.send(\n formatResponse<GetInvoicesResult['data']>({ data: invoices.data })\n );\n } catch (error) {\n return ErrorHandler.handleAppErrorResponse(reply, error as AppError);\n }\n};\n\nexport type GetPaymentMethodResult = ResponseData<Stripe.PaymentMethod | null>;\n\n/**\n * Returns the first card payment method attached to the authenticated\n * organization's Stripe customer (or null if none).\n */\nexport const getPaymentMethod = async (\n request: FastifyRequest,\n reply: FastifyReply\n): Promise<void> => {\n try {\n const customerId = getCustomerIdFromSession(request);\n\n if (!customerId) {\n return ErrorHandler.handleGenericErrorResponse(\n reply,\n 'ORGANIZATION_PLAN_NOT_FOUND'\n );\n }\n\n const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!);\n const paymentMethods = await stripe.paymentMethods.list({\n customer: customerId,\n type: 'card',\n });\n\n return reply.send(\n formatResponse<GetPaymentMethodResult['data']>({\n data: paymentMethods.data[0] ?? null,\n })\n );\n } catch (error) {\n return ErrorHandler.handleAppErrorResponse(reply, error as AppError);\n }\n};\n\nexport type CreatePortalSessionResult = ResponseData<{ url: string }>;\n\n/**\n * Creates a Stripe Billing Portal session for the authenticated organization\n * so the user can manage their payment method and subscription.\n */\nexport const createPortalSession = async (\n request: FastifyRequest,\n reply: FastifyReply\n): Promise<void> => {\n try {\n const customerId = getCustomerIdFromSession(request);\n\n if (!customerId) {\n return ErrorHandler.handleGenericErrorResponse(\n reply,\n 'ORGANIZATION_PLAN_NOT_FOUND'\n );\n }\n\n const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!);\n const session = await stripe.billingPortal.sessions.create({\n customer: customerId,\n return_url: `${process.env.APP_URL ?? 'http://localhost:3000'}/dashboard`,\n });\n\n return reply.send(\n formatResponse<CreatePortalSessionResult['data']>({\n data: { url: session.url },\n })\n );\n } catch (error) {\n return ErrorHandler.handleAppErrorResponse(reply, error as AppError);\n }\n};\n"],"mappings":"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"}
|
|
@@ -23,6 +23,7 @@ const translateDictionaries = async (request, reply) => {
|
|
|
23
23
|
const dictionaryTargets = [];
|
|
24
24
|
const projectLocales = project.configuration?.internationalization?.locales ?? [];
|
|
25
25
|
for (const dictionary of dictionaries) {
|
|
26
|
+
if (!dictionary.content) continue;
|
|
26
27
|
const versionList = Array.from(dictionary.content.keys());
|
|
27
28
|
const lastVersion = versionList[versionList.length - 1] || "v1";
|
|
28
29
|
const node = dictionary.content.get(lastVersion);
|
|
@@ -56,6 +57,7 @@ const translateDictionaries = async (request, reply) => {
|
|
|
56
57
|
message: "Translation started"
|
|
57
58
|
}));
|
|
58
59
|
} catch (error) {
|
|
60
|
+
logger.error("Error in translateDictionaries", error);
|
|
59
61
|
return ErrorHandler.handleAppErrorResponse(reply, error);
|
|
60
62
|
}
|
|
61
63
|
};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"translation.controller.mjs","names":["dictionaryService.findDictionaries"],"sources":["../../../src/controllers/translation.controller.ts"],"sourcesContent":["import { getMissingLocalesContentFromDictionary } from '@intlayer/core/plugins';\nimport type { Locale } from '@intlayer/types/allLocales';\nimport { logger } from '@logger';\nimport * as dictionaryService from '@services/dictionary.service';\nimport {\n addTranslationJob,\n getTranslationQueue,\n isTranslationJobPaused,\n translationCancelKey,\n translationPauseKey,\n} from '@services/translationQueue.service';\nimport { type AppError, ErrorHandler } from '@utils/errors';\nimport { getRedisClient } from '@utils/redis/connectRedis';\nimport { formatResponse, type ResponseData } from '@utils/responseData';\nimport type { FastifyReply, FastifyRequest } from 'fastify';\nimport { Types } from 'mongoose';\n\nexport type TranslateDictionariesBody = {\n dictionaryIds: string[];\n targetLocales: Locale[];\n mode?: 'complete' | 'review';\n};\nexport type TranslateDictionariesResult = ResponseData<{ jobId: string }>;\n\nexport const translateDictionaries = async (\n request: FastifyRequest<{ Body: TranslateDictionariesBody }>,\n reply: FastifyReply\n): Promise<void> => {\n const { project, user } = request.session || {};\n const { dictionaryIds, targetLocales, mode = 'complete' } = request.body;\n\n if (!project) {\n return ErrorHandler.handleGenericErrorResponse(\n reply,\n 'PROJECT_NOT_DEFINED'\n );\n }\n if (!user) {\n return ErrorHandler.handleGenericErrorResponse(reply, 'USER_NOT_DEFINED');\n }\n\n try {\n const validIds = dictionaryIds.filter((id) => Types.ObjectId.isValid(id));\n\n if (validIds.length === 0) {\n return reply.send(\n formatResponse({\n data: null,\n message: 'No valid dictionary IDs provided',\n })\n );\n }\n\n const dictionaries = await dictionaryService.findDictionaries(\n { _id: { $in: validIds.map((id) => new Types.ObjectId(id)) } },\n 0,\n validIds.length,\n undefined,\n false\n );\n\n const dictionaryTargets: { dictionaryId: string; locales: Locale[] }[] = [];\n\n const projectLocales =\n project.configuration?.internationalization?.locales ?? [];\n\n for (const dictionary of dictionaries) {\n const versionList = Array.from(dictionary.content.keys());\n const lastVersion = versionList[versionList.length - 1] || 'v1';\n const node = dictionary.content.get(lastVersion);\n\n if (!node) continue;\n\n // In 'complete' mode skip locales that already have full translations.\n // In 'review' mode translate everything regardless.\n let localesToTranslate: Locale[];\n if (mode === 'review') {\n localesToTranslate = targetLocales;\n } else {\n const missingLocales = getMissingLocalesContentFromDictionary(\n { key: dictionary.key, content: node.content },\n projectLocales\n );\n localesToTranslate = targetLocales.filter((locale) =>\n missingLocales.includes(locale)\n );\n }\n\n if (localesToTranslate.length > 0) {\n dictionaryTargets.push({\n dictionaryId: String(dictionary._id),\n locales: localesToTranslate,\n });\n }\n }\n\n if (dictionaryTargets.length === 0) {\n return reply.send(\n formatResponse({\n data: null,\n message: 'All dictionaries are already translated',\n })\n );\n }\n\n const job = await addTranslationJob({\n dictionaryTargets,\n projectId: String(project.id),\n userId: String(user.id),\n mode,\n });\n\n return reply.send(\n formatResponse<{ jobId: string }>({\n data: { jobId: job.id! },\n message: 'Translation started',\n })\n );\n } catch (error) {\n return ErrorHandler.handleAppErrorResponse(reply, error as AppError);\n }\n};\n\nexport const pauseTranslationJob = async (\n request: FastifyRequest<{ Params: { jobId: string } }>,\n reply: FastifyReply\n): Promise<void> => {\n const { user } = request.session || {};\n if (!user) {\n return ErrorHandler.handleGenericErrorResponse(reply, 'USER_NOT_DEFINED');\n }\n const { jobId } = request.params;\n const redis = getRedisClient();\n await redis.set(translationPauseKey(jobId), '1', 'EX', 86400);\n return reply.send(formatResponse({ data: { jobId }, message: 'Paused' }));\n};\n\nexport const resumeTranslationJob = async (\n request: FastifyRequest<{ Params: { jobId: string } }>,\n reply: FastifyReply\n): Promise<void> => {\n const { user } = request.session || {};\n if (!user) {\n return ErrorHandler.handleGenericErrorResponse(reply, 'USER_NOT_DEFINED');\n }\n const { jobId } = request.params;\n const redis = getRedisClient();\n await redis.del(translationPauseKey(jobId));\n return reply.send(formatResponse({ data: { jobId }, message: 'Resumed' }));\n};\n\nexport const stopTranslationJob = async (\n request: FastifyRequest<{ Params: { jobId: string } }>,\n reply: FastifyReply\n): Promise<void> => {\n const { user } = request.session || {};\n if (!user) {\n return ErrorHandler.handleGenericErrorResponse(reply, 'USER_NOT_DEFINED');\n }\n const { jobId } = request.params;\n const redis = getRedisClient();\n await redis.set(translationCancelKey(jobId), '1', 'EX', 86400);\n await redis.del(translationPauseKey(jobId));\n\n const queue = getTranslationQueue();\n const job = await queue.getJob(jobId);\n if (job) {\n const state = await job.getState();\n if (state === 'waiting' || state === 'delayed') {\n await job.remove();\n }\n }\n return reply.send(formatResponse({ data: { jobId }, message: 'Stopped' }));\n};\n\nexport const retryTranslationJob = async (\n request: FastifyRequest<{ Params: { jobId: string } }>,\n reply: FastifyReply\n): Promise<void> => {\n const { user } = request.session || {};\n if (!user) {\n return ErrorHandler.handleGenericErrorResponse(reply, 'USER_NOT_DEFINED');\n }\n const { jobId } = request.params;\n const queue = getTranslationQueue();\n const job = await queue.getJob(jobId);\n if (!job) return reply.status(404).send({ error: 'Job not found' });\n const redis = getRedisClient();\n await redis.del(translationCancelKey(jobId));\n await redis.del(translationPauseKey(jobId));\n await job.retry();\n return reply.send(formatResponse({ data: { jobId }, message: 'Retrying' }));\n};\n\nexport const restartTranslationJob = async (\n request: FastifyRequest<{ Params: { jobId: string } }>,\n reply: FastifyReply\n): Promise<void> => {\n const { user } = request.session || {};\n if (!user) {\n return ErrorHandler.handleGenericErrorResponse(reply, 'USER_NOT_DEFINED');\n }\n const { jobId } = request.params;\n const queue = getTranslationQueue();\n const job = await queue.getJob(jobId);\n if (!job) return reply.status(404).send({ error: 'Job not found' });\n const newJob = await addTranslationJob(job.data);\n return reply.send(\n formatResponse({ data: { jobId: newJob.id! }, message: 'Restarted' })\n );\n};\n\nexport const getTranslationStatus = async (\n request: FastifyRequest,\n reply: FastifyReply\n): Promise<void> => {\n const { project, user } = request.session || {};\n\n if (!user) {\n reply.raw.statusCode = 401;\n reply.raw.end();\n return;\n }\n\n reply.hijack();\n\n const headers = reply.getHeaders();\n Object.entries(headers).forEach(([key, value]) => {\n if (value !== undefined) {\n reply.raw.setHeader(key, value);\n }\n });\n\n const sseHeaders = {\n 'Content-Type': 'text/event-stream; charset=utf-8',\n 'Cache-Control': 'no-cache, no-transform',\n Connection: 'keep-alive',\n 'X-Accel-Buffering': 'no',\n };\n\n Object.entries(sseHeaders).forEach(([key, value]) => {\n reply.raw.setHeader(key, value);\n });\n reply.raw.flushHeaders?.();\n\n reply.raw.write(': connected\\n\\n');\n\n const send = (data: any) => {\n if (!reply.raw.writableEnded && !reply.raw.destroyed) {\n reply.raw.write(`data: ${JSON.stringify(data)}\\n\\n`);\n }\n };\n\n const projectId = project ? String(project.id) : null;\n const userId = String(user.id);\n\n const matchesSession = (job: any) =>\n projectId\n ? String(job.data.projectId) === projectId\n : String(job.data.userId) === userId;\n\n const sendJob = async (job: any) => {\n const state = await job.getState();\n const isPaused = await isTranslationJobPaused(job.id);\n send({\n jobId: job.id,\n state,\n isPaused,\n progress: job.progress,\n data: job.data,\n });\n };\n\n try {\n const translationQueue = getTranslationQueue();\n\n const getRelevantJobs = async () => {\n const jobs = await translationQueue.getJobs([\n 'active',\n 'waiting',\n 'delayed',\n 'completed',\n 'failed',\n ]);\n return jobs.filter(matchesSession);\n };\n\n const jobs = await getRelevantJobs();\n for (const job of jobs) {\n await sendJob(job);\n }\n\n const interval = setInterval(async () => {\n try {\n const currentJobs = await translationQueue.getJobs([\n 'active',\n 'waiting',\n 'delayed',\n 'completed',\n 'failed',\n ]);\n const relevantJobs = currentJobs.filter(matchesSession);\n for (const job of relevantJobs) {\n await sendJob(job);\n }\n } catch (error) {\n logger.error('Error polling translation status', error);\n }\n }, 2000);\n\n request.raw.on('close', () => {\n clearInterval(interval);\n });\n } catch (error) {\n logger.error('Error in translation status stream', error);\n if (!reply.raw.writableEnded && !reply.raw.destroyed) {\n reply.raw.write(\n `event: error\\ndata: ${JSON.stringify({ message: 'Internal Server Error' })}\\n\\n`\n );\n reply.raw.end();\n }\n }\n};\n"],"mappings":";;;;;;;;;;AAwBA,MAAa,wBAAwB,OACnC,SACA,UACkB;CAClB,MAAM,EAAE,SAAS,SAAS,QAAQ,WAAW,EAAE;CAC/C,MAAM,EAAE,eAAe,eAAe,OAAO,eAAe,QAAQ;AAEpE,KAAI,CAAC,QACH,QAAO,aAAa,2BAClB,OACA,sBACD;AAEH,KAAI,CAAC,KACH,QAAO,aAAa,2BAA2B,OAAO,mBAAmB;AAG3E,KAAI;EACF,MAAM,WAAW,cAAc,QAAQ,OAAO,MAAM,SAAS,QAAQ,GAAG,CAAC;AAEzE,MAAI,SAAS,WAAW,EACtB,QAAO,MAAM,KACX,eAAe;GACb,MAAM;GACN,SAAS;GACV,CAAC,CACH;EAGH,MAAM,eAAe,MAAMA,iBACzB,EAAE,KAAK,EAAE,KAAK,SAAS,KAAK,OAAO,IAAI,MAAM,SAAS,GAAG,CAAC,EAAE,EAAE,EAC9D,GACA,SAAS,QACT,QACA,MACD;EAED,MAAM,oBAAmE,EAAE;EAE3E,MAAM,iBACJ,QAAQ,eAAe,sBAAsB,WAAW,EAAE;AAE5D,OAAK,MAAM,cAAc,cAAc;GACrC,MAAM,cAAc,MAAM,KAAK,WAAW,QAAQ,MAAM,CAAC;GACzD,MAAM,cAAc,YAAY,YAAY,SAAS,MAAM;GAC3D,MAAM,OAAO,WAAW,QAAQ,IAAI,YAAY;AAEhD,OAAI,CAAC,KAAM;GAIX,IAAI;AACJ,OAAI,SAAS,SACX,sBAAqB;QAChB;IACL,MAAM,iBAAiB,uCACrB;KAAE,KAAK,WAAW;KAAK,SAAS,KAAK;KAAS,EAC9C,eACD;AACD,yBAAqB,cAAc,QAAQ,WACzC,eAAe,SAAS,OAAO,CAChC;;AAGH,OAAI,mBAAmB,SAAS,EAC9B,mBAAkB,KAAK;IACrB,cAAc,OAAO,WAAW,IAAI;IACpC,SAAS;IACV,CAAC;;AAIN,MAAI,kBAAkB,WAAW,EAC/B,QAAO,MAAM,KACX,eAAe;GACb,MAAM;GACN,SAAS;GACV,CAAC,CACH;EAGH,MAAM,MAAM,MAAM,kBAAkB;GAClC;GACA,WAAW,OAAO,QAAQ,GAAG;GAC7B,QAAQ,OAAO,KAAK,GAAG;GACvB;GACD,CAAC;AAEF,SAAO,MAAM,KACX,eAAkC;GAChC,MAAM,EAAE,OAAO,IAAI,IAAK;GACxB,SAAS;GACV,CAAC,CACH;UACM,OAAO;AACd,SAAO,aAAa,uBAAuB,OAAO,MAAkB;;;AAIxE,MAAa,sBAAsB,OACjC,SACA,UACkB;CAClB,MAAM,EAAE,SAAS,QAAQ,WAAW,EAAE;AACtC,KAAI,CAAC,KACH,QAAO,aAAa,2BAA2B,OAAO,mBAAmB;CAE3E,MAAM,EAAE,UAAU,QAAQ;AAE1B,OADc,gBACH,CAAC,IAAI,oBAAoB,MAAM,EAAE,KAAK,MAAM,MAAM;AAC7D,QAAO,MAAM,KAAK,eAAe;EAAE,MAAM,EAAE,OAAO;EAAE,SAAS;EAAU,CAAC,CAAC;;AAG3E,MAAa,uBAAuB,OAClC,SACA,UACkB;CAClB,MAAM,EAAE,SAAS,QAAQ,WAAW,EAAE;AACtC,KAAI,CAAC,KACH,QAAO,aAAa,2BAA2B,OAAO,mBAAmB;CAE3E,MAAM,EAAE,UAAU,QAAQ;AAE1B,OADc,gBACH,CAAC,IAAI,oBAAoB,MAAM,CAAC;AAC3C,QAAO,MAAM,KAAK,eAAe;EAAE,MAAM,EAAE,OAAO;EAAE,SAAS;EAAW,CAAC,CAAC;;AAG5E,MAAa,qBAAqB,OAChC,SACA,UACkB;CAClB,MAAM,EAAE,SAAS,QAAQ,WAAW,EAAE;AACtC,KAAI,CAAC,KACH,QAAO,aAAa,2BAA2B,OAAO,mBAAmB;CAE3E,MAAM,EAAE,UAAU,QAAQ;CAC1B,MAAM,QAAQ,gBAAgB;AAC9B,OAAM,MAAM,IAAI,qBAAqB,MAAM,EAAE,KAAK,MAAM,MAAM;AAC9D,OAAM,MAAM,IAAI,oBAAoB,MAAM,CAAC;CAG3C,MAAM,MAAM,MADE,qBACS,CAAC,OAAO,MAAM;AACrC,KAAI,KAAK;EACP,MAAM,QAAQ,MAAM,IAAI,UAAU;AAClC,MAAI,UAAU,aAAa,UAAU,UACnC,OAAM,IAAI,QAAQ;;AAGtB,QAAO,MAAM,KAAK,eAAe;EAAE,MAAM,EAAE,OAAO;EAAE,SAAS;EAAW,CAAC,CAAC;;AAG5E,MAAa,sBAAsB,OACjC,SACA,UACkB;CAClB,MAAM,EAAE,SAAS,QAAQ,WAAW,EAAE;AACtC,KAAI,CAAC,KACH,QAAO,aAAa,2BAA2B,OAAO,mBAAmB;CAE3E,MAAM,EAAE,UAAU,QAAQ;CAE1B,MAAM,MAAM,MADE,qBACS,CAAC,OAAO,MAAM;AACrC,KAAI,CAAC,IAAK,QAAO,MAAM,OAAO,IAAI,CAAC,KAAK,EAAE,OAAO,iBAAiB,CAAC;CACnE,MAAM,QAAQ,gBAAgB;AAC9B,OAAM,MAAM,IAAI,qBAAqB,MAAM,CAAC;AAC5C,OAAM,MAAM,IAAI,oBAAoB,MAAM,CAAC;AAC3C,OAAM,IAAI,OAAO;AACjB,QAAO,MAAM,KAAK,eAAe;EAAE,MAAM,EAAE,OAAO;EAAE,SAAS;EAAY,CAAC,CAAC;;AAG7E,MAAa,wBAAwB,OACnC,SACA,UACkB;CAClB,MAAM,EAAE,SAAS,QAAQ,WAAW,EAAE;AACtC,KAAI,CAAC,KACH,QAAO,aAAa,2BAA2B,OAAO,mBAAmB;CAE3E,MAAM,EAAE,UAAU,QAAQ;CAE1B,MAAM,MAAM,MADE,qBACS,CAAC,OAAO,MAAM;AACrC,KAAI,CAAC,IAAK,QAAO,MAAM,OAAO,IAAI,CAAC,KAAK,EAAE,OAAO,iBAAiB,CAAC;CACnE,MAAM,SAAS,MAAM,kBAAkB,IAAI,KAAK;AAChD,QAAO,MAAM,KACX,eAAe;EAAE,MAAM,EAAE,OAAO,OAAO,IAAK;EAAE,SAAS;EAAa,CAAC,CACtE;;AAGH,MAAa,uBAAuB,OAClC,SACA,UACkB;CAClB,MAAM,EAAE,SAAS,SAAS,QAAQ,WAAW,EAAE;AAE/C,KAAI,CAAC,MAAM;AACT,QAAM,IAAI,aAAa;AACvB,QAAM,IAAI,KAAK;AACf;;AAGF,OAAM,QAAQ;CAEd,MAAM,UAAU,MAAM,YAAY;AAClC,QAAO,QAAQ,QAAQ,CAAC,SAAS,CAAC,KAAK,WAAW;AAChD,MAAI,UAAU,OACZ,OAAM,IAAI,UAAU,KAAK,MAAM;GAEjC;AASF,QAAO,QAAQ;EANb,gBAAgB;EAChB,iBAAiB;EACjB,YAAY;EACZ,qBAAqB;EAGE,CAAC,CAAC,SAAS,CAAC,KAAK,WAAW;AACnD,QAAM,IAAI,UAAU,KAAK,MAAM;GAC/B;AACF,OAAM,IAAI,gBAAgB;AAE1B,OAAM,IAAI,MAAM,kBAAkB;CAElC,MAAM,QAAQ,SAAc;AAC1B,MAAI,CAAC,MAAM,IAAI,iBAAiB,CAAC,MAAM,IAAI,UACzC,OAAM,IAAI,MAAM,SAAS,KAAK,UAAU,KAAK,CAAC,MAAM;;CAIxD,MAAM,YAAY,UAAU,OAAO,QAAQ,GAAG,GAAG;CACjD,MAAM,SAAS,OAAO,KAAK,GAAG;CAE9B,MAAM,kBAAkB,QACtB,YACI,OAAO,IAAI,KAAK,UAAU,KAAK,YAC/B,OAAO,IAAI,KAAK,OAAO,KAAK;CAElC,MAAM,UAAU,OAAO,QAAa;EAClC,MAAM,QAAQ,MAAM,IAAI,UAAU;EAClC,MAAM,WAAW,MAAM,uBAAuB,IAAI,GAAG;AACrD,OAAK;GACH,OAAO,IAAI;GACX;GACA;GACA,UAAU,IAAI;GACd,MAAM,IAAI;GACX,CAAC;;AAGJ,KAAI;EACF,MAAM,mBAAmB,qBAAqB;EAE9C,MAAM,kBAAkB,YAAY;AAQlC,WAAO,MAPY,iBAAiB,QAAQ;IAC1C;IACA;IACA;IACA;IACA;IACD,CAAC,EACU,OAAO,eAAe;;EAGpC,MAAM,OAAO,MAAM,iBAAiB;AACpC,OAAK,MAAM,OAAO,KAChB,OAAM,QAAQ,IAAI;EAGpB,MAAM,WAAW,YAAY,YAAY;AACvC,OAAI;IAQF,MAAM,gBAAe,MAPK,iBAAiB,QAAQ;KACjD;KACA;KACA;KACA;KACA;KACD,CAAC,EAC+B,OAAO,eAAe;AACvD,SAAK,MAAM,OAAO,aAChB,OAAM,QAAQ,IAAI;YAEb,OAAO;AACd,WAAO,MAAM,oCAAoC,MAAM;;KAExD,IAAK;AAER,UAAQ,IAAI,GAAG,eAAe;AAC5B,iBAAc,SAAS;IACvB;UACK,OAAO;AACd,SAAO,MAAM,sCAAsC,MAAM;AACzD,MAAI,CAAC,MAAM,IAAI,iBAAiB,CAAC,MAAM,IAAI,WAAW;AACpD,SAAM,IAAI,MACR,uBAAuB,KAAK,UAAU,EAAE,SAAS,yBAAyB,CAAC,CAAC,MAC7E;AACD,SAAM,IAAI,KAAK"}
|
|
1
|
+
{"version":3,"file":"translation.controller.mjs","names":["dictionaryService.findDictionaries"],"sources":["../../../src/controllers/translation.controller.ts"],"sourcesContent":["import { getMissingLocalesContentFromDictionary } from '@intlayer/core/plugins';\nimport type { Locale } from '@intlayer/types/allLocales';\nimport { logger } from '@logger';\nimport * as dictionaryService from '@services/dictionary.service';\nimport {\n addTranslationJob,\n getTranslationQueue,\n isTranslationJobPaused,\n translationCancelKey,\n translationPauseKey,\n} from '@services/translationQueue.service';\nimport { type AppError, ErrorHandler } from '@utils/errors';\nimport { getRedisClient } from '@utils/redis/connectRedis';\nimport { formatResponse, type ResponseData } from '@utils/responseData';\nimport type { FastifyReply, FastifyRequest } from 'fastify';\nimport { Types } from 'mongoose';\n\nexport type TranslateDictionariesBody = {\n dictionaryIds: string[];\n targetLocales: Locale[];\n mode?: 'complete' | 'review';\n};\nexport type TranslateDictionariesResult = ResponseData<{ jobId: string }>;\n\nexport const translateDictionaries = async (\n request: FastifyRequest<{ Body: TranslateDictionariesBody }>,\n reply: FastifyReply\n): Promise<void> => {\n const { project, user } = request.session || {};\n const { dictionaryIds, targetLocales, mode = 'complete' } = request.body;\n\n if (!project) {\n return ErrorHandler.handleGenericErrorResponse(\n reply,\n 'PROJECT_NOT_DEFINED'\n );\n }\n if (!user) {\n return ErrorHandler.handleGenericErrorResponse(reply, 'USER_NOT_DEFINED');\n }\n\n try {\n const validIds = dictionaryIds.filter((id) => Types.ObjectId.isValid(id));\n\n if (validIds.length === 0) {\n return reply.send(\n formatResponse({\n data: null,\n message: 'No valid dictionary IDs provided',\n })\n );\n }\n\n const dictionaries = await dictionaryService.findDictionaries(\n { _id: { $in: validIds.map((id) => new Types.ObjectId(id)) } },\n 0,\n validIds.length,\n undefined,\n false\n );\n\n const dictionaryTargets: { dictionaryId: string; locales: Locale[] }[] = [];\n\n const projectLocales =\n project.configuration?.internationalization?.locales ?? [];\n\n for (const dictionary of dictionaries) {\n if (!dictionary.content) continue;\n\n const versionList = Array.from(dictionary.content.keys());\n const lastVersion = versionList[versionList.length - 1] || 'v1';\n const node = dictionary.content.get(lastVersion);\n\n if (!node) continue;\n\n // In 'complete' mode skip locales that already have full translations.\n // In 'review' mode translate everything regardless.\n let localesToTranslate: Locale[];\n if (mode === 'review') {\n localesToTranslate = targetLocales;\n } else {\n const missingLocales = getMissingLocalesContentFromDictionary(\n { key: dictionary.key, content: node.content },\n projectLocales\n );\n localesToTranslate = targetLocales.filter((locale) =>\n missingLocales.includes(locale)\n );\n }\n\n if (localesToTranslate.length > 0) {\n dictionaryTargets.push({\n dictionaryId: String(dictionary._id),\n locales: localesToTranslate,\n });\n }\n }\n\n if (dictionaryTargets.length === 0) {\n return reply.send(\n formatResponse({\n data: null,\n message: 'All dictionaries are already translated',\n })\n );\n }\n\n const job = await addTranslationJob({\n dictionaryTargets,\n projectId: String(project.id),\n userId: String(user.id),\n mode,\n });\n\n return reply.send(\n formatResponse<{ jobId: string }>({\n data: { jobId: job.id! },\n message: 'Translation started',\n })\n );\n } catch (error) {\n logger.error('Error in translateDictionaries', error);\n return ErrorHandler.handleAppErrorResponse(reply, error as AppError);\n }\n};\n\nexport const pauseTranslationJob = async (\n request: FastifyRequest<{ Params: { jobId: string } }>,\n reply: FastifyReply\n): Promise<void> => {\n const { user } = request.session || {};\n if (!user) {\n return ErrorHandler.handleGenericErrorResponse(reply, 'USER_NOT_DEFINED');\n }\n const { jobId } = request.params;\n const redis = getRedisClient();\n await redis.set(translationPauseKey(jobId), '1', 'EX', 86400);\n return reply.send(formatResponse({ data: { jobId }, message: 'Paused' }));\n};\n\nexport const resumeTranslationJob = async (\n request: FastifyRequest<{ Params: { jobId: string } }>,\n reply: FastifyReply\n): Promise<void> => {\n const { user } = request.session || {};\n if (!user) {\n return ErrorHandler.handleGenericErrorResponse(reply, 'USER_NOT_DEFINED');\n }\n const { jobId } = request.params;\n const redis = getRedisClient();\n await redis.del(translationPauseKey(jobId));\n return reply.send(formatResponse({ data: { jobId }, message: 'Resumed' }));\n};\n\nexport const stopTranslationJob = async (\n request: FastifyRequest<{ Params: { jobId: string } }>,\n reply: FastifyReply\n): Promise<void> => {\n const { user } = request.session || {};\n if (!user) {\n return ErrorHandler.handleGenericErrorResponse(reply, 'USER_NOT_DEFINED');\n }\n const { jobId } = request.params;\n const redis = getRedisClient();\n await redis.set(translationCancelKey(jobId), '1', 'EX', 86400);\n await redis.del(translationPauseKey(jobId));\n\n const queue = getTranslationQueue();\n const job = await queue.getJob(jobId);\n if (job) {\n const state = await job.getState();\n if (state === 'waiting' || state === 'delayed') {\n await job.remove();\n }\n }\n return reply.send(formatResponse({ data: { jobId }, message: 'Stopped' }));\n};\n\nexport const retryTranslationJob = async (\n request: FastifyRequest<{ Params: { jobId: string } }>,\n reply: FastifyReply\n): Promise<void> => {\n const { user } = request.session || {};\n if (!user) {\n return ErrorHandler.handleGenericErrorResponse(reply, 'USER_NOT_DEFINED');\n }\n const { jobId } = request.params;\n const queue = getTranslationQueue();\n const job = await queue.getJob(jobId);\n if (!job) return reply.status(404).send({ error: 'Job not found' });\n const redis = getRedisClient();\n await redis.del(translationCancelKey(jobId));\n await redis.del(translationPauseKey(jobId));\n await job.retry();\n return reply.send(formatResponse({ data: { jobId }, message: 'Retrying' }));\n};\n\nexport const restartTranslationJob = async (\n request: FastifyRequest<{ Params: { jobId: string } }>,\n reply: FastifyReply\n): Promise<void> => {\n const { user } = request.session || {};\n if (!user) {\n return ErrorHandler.handleGenericErrorResponse(reply, 'USER_NOT_DEFINED');\n }\n const { jobId } = request.params;\n const queue = getTranslationQueue();\n const job = await queue.getJob(jobId);\n if (!job) return reply.status(404).send({ error: 'Job not found' });\n const newJob = await addTranslationJob(job.data);\n return reply.send(\n formatResponse({ data: { jobId: newJob.id! }, message: 'Restarted' })\n );\n};\n\nexport const getTranslationStatus = async (\n request: FastifyRequest,\n reply: FastifyReply\n): Promise<void> => {\n const { project, user } = request.session || {};\n\n if (!user) {\n reply.raw.statusCode = 401;\n reply.raw.end();\n return;\n }\n\n reply.hijack();\n\n const headers = reply.getHeaders();\n Object.entries(headers).forEach(([key, value]) => {\n if (value !== undefined) {\n reply.raw.setHeader(key, value);\n }\n });\n\n const sseHeaders = {\n 'Content-Type': 'text/event-stream; charset=utf-8',\n 'Cache-Control': 'no-cache, no-transform',\n Connection: 'keep-alive',\n 'X-Accel-Buffering': 'no',\n };\n\n Object.entries(sseHeaders).forEach(([key, value]) => {\n reply.raw.setHeader(key, value);\n });\n reply.raw.flushHeaders?.();\n\n reply.raw.write(': connected\\n\\n');\n\n const send = (data: any) => {\n if (!reply.raw.writableEnded && !reply.raw.destroyed) {\n reply.raw.write(`data: ${JSON.stringify(data)}\\n\\n`);\n }\n };\n\n const projectId = project ? String(project.id) : null;\n const userId = String(user.id);\n\n const matchesSession = (job: any) =>\n projectId\n ? String(job.data.projectId) === projectId\n : String(job.data.userId) === userId;\n\n const sendJob = async (job: any) => {\n const state = await job.getState();\n const isPaused = await isTranslationJobPaused(job.id);\n send({\n jobId: job.id,\n state,\n isPaused,\n progress: job.progress,\n data: job.data,\n });\n };\n\n try {\n const translationQueue = getTranslationQueue();\n\n const getRelevantJobs = async () => {\n const jobs = await translationQueue.getJobs([\n 'active',\n 'waiting',\n 'delayed',\n 'completed',\n 'failed',\n ]);\n return jobs.filter(matchesSession);\n };\n\n const jobs = await getRelevantJobs();\n for (const job of jobs) {\n await sendJob(job);\n }\n\n const interval = setInterval(async () => {\n try {\n const currentJobs = await translationQueue.getJobs([\n 'active',\n 'waiting',\n 'delayed',\n 'completed',\n 'failed',\n ]);\n const relevantJobs = currentJobs.filter(matchesSession);\n for (const job of relevantJobs) {\n await sendJob(job);\n }\n } catch (error) {\n logger.error('Error polling translation status', error);\n }\n }, 2000);\n\n request.raw.on('close', () => {\n clearInterval(interval);\n });\n } catch (error) {\n logger.error('Error in translation status stream', error);\n if (!reply.raw.writableEnded && !reply.raw.destroyed) {\n reply.raw.write(\n `event: error\\ndata: ${JSON.stringify({ message: 'Internal Server Error' })}\\n\\n`\n );\n reply.raw.end();\n }\n }\n};\n"],"mappings":"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"}
|