@unchainedshop/api 4.8.26 → 4.8.27

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.
Files changed (57) hide show
  1. package/lib/adminUiPlugins.d.ts +97 -0
  2. package/lib/adminUiPlugins.js +164 -0
  3. package/lib/auth.d.ts +36 -0
  4. package/lib/auth.js +167 -0
  5. package/lib/express/mountPluginRoutes.d.ts +3 -0
  6. package/lib/express/mountPluginRoutes.js +59 -0
  7. package/lib/express/mountRoutes.d.ts +3 -0
  8. package/lib/express/mountRoutes.js +57 -0
  9. package/lib/fastify/mountPluginRoutes.d.ts +3 -0
  10. package/lib/fastify/mountPluginRoutes.js +62 -0
  11. package/lib/fastify/mountRoutes.d.ts +3 -0
  12. package/lib/fastify/mountRoutes.js +60 -0
  13. package/lib/handlers/createBackchannelLogoutHandler.d.ts +4 -0
  14. package/lib/handlers/createBackchannelLogoutHandler.js +170 -0
  15. package/lib/middleware/createAuthMiddleware.d.ts +29 -0
  16. package/lib/middleware/createAuthMiddleware.js +91 -0
  17. package/lib/resolvers/mutations/accounts/logoutAllSessions.d.ts +6 -0
  18. package/lib/resolvers/mutations/accounts/logoutAllSessions.js +17 -0
  19. package/lib/resolvers/mutations/bulk/bulkAssignProductsToAssortment.d.ts +10 -0
  20. package/lib/resolvers/mutations/bulk/bulkAssignProductsToAssortment.js +18 -0
  21. package/lib/resolvers/mutations/bulk/bulkOperation.d.ts +12 -0
  22. package/lib/resolvers/mutations/bulk/bulkOperation.js +14 -0
  23. package/lib/resolvers/mutations/bulk/bulkRemoveAssortments.d.ts +9 -0
  24. package/lib/resolvers/mutations/bulk/bulkRemoveAssortments.js +7 -0
  25. package/lib/resolvers/mutations/bulk/bulkRemoveFilters.d.ts +9 -0
  26. package/lib/resolvers/mutations/bulk/bulkRemoveFilters.js +9 -0
  27. package/lib/resolvers/mutations/bulk/bulkRemoveProducts.d.ts +9 -0
  28. package/lib/resolvers/mutations/bulk/bulkRemoveProducts.js +9 -0
  29. package/lib/resolvers/mutations/bulk/bulkRemoveUsers.d.ts +9 -0
  30. package/lib/resolvers/mutations/bulk/bulkRemoveUsers.js +7 -0
  31. package/lib/resolvers/mutations/bulk/bulkSetAssortmentActive.d.ts +10 -0
  32. package/lib/resolvers/mutations/bulk/bulkSetAssortmentActive.js +9 -0
  33. package/lib/resolvers/mutations/bulk/bulkSetFilterActive.d.ts +10 -0
  34. package/lib/resolvers/mutations/bulk/bulkSetFilterActive.js +10 -0
  35. package/lib/resolvers/mutations/bulk/bulkSetProductStatus.d.ts +10 -0
  36. package/lib/resolvers/mutations/bulk/bulkSetProductStatus.js +17 -0
  37. package/lib/resolvers/mutations/bulk/bulkSetUserRoles.d.ts +10 -0
  38. package/lib/resolvers/mutations/bulk/bulkSetUserRoles.js +14 -0
  39. package/lib/resolvers/mutations/bulk/bulkUpdateAssortmentTags.d.ts +11 -0
  40. package/lib/resolvers/mutations/bulk/bulkUpdateAssortmentTags.js +10 -0
  41. package/lib/resolvers/mutations/bulk/bulkUpdateProductTags.d.ts +11 -0
  42. package/lib/resolvers/mutations/bulk/bulkUpdateProductTags.js +7 -0
  43. package/lib/resolvers/mutations/bulk/bulkUpdateUserTags.d.ts +11 -0
  44. package/lib/resolvers/mutations/bulk/bulkUpdateUserTags.js +7 -0
  45. package/lib/resolvers/queries/search/globalSearch.d.ts +36 -0
  46. package/lib/resolvers/queries/search/globalSearch.js +200 -0
  47. package/lib/resolvers/type/global-search-result-types.d.ts +3 -0
  48. package/lib/resolvers/type/global-search-result-types.js +15 -0
  49. package/lib/resolvers/type/order/order-payment-base.d.ts +7 -0
  50. package/lib/resolvers/type/order/order-payment-base.js +14 -0
  51. package/lib/schema/types/common.d.ts +2 -0
  52. package/lib/schema/types/common.js +19 -0
  53. package/lib/utils/mapServiceError.d.ts +1 -0
  54. package/lib/utils/mapServiceError.js +59 -0
  55. package/lib/utils/maskError.d.ts +1 -0
  56. package/lib/utils/maskError.js +22 -0
  57. package/package.json +24 -24
@@ -0,0 +1,60 @@
1
+ import { createServerAdapter } from '@whatwg-node/server';
2
+ export function mountRoutes(fastify, unchainedAPI, routes) {
3
+ if (routes.length === 0)
4
+ return;
5
+ const endpoints = routes.map((r) => `${r.method} ${r.path}`).join(', ');
6
+ fastify.log.info(`Mounting ${routes.length} route(s): ${endpoints}`);
7
+ fastify.register((scope, opts, registered) => {
8
+ scope.removeAllContentTypeParsers();
9
+ scope.addContentTypeParser('*', function (request, payload, done) {
10
+ done(null);
11
+ });
12
+ for (const route of routes) {
13
+ const adapter = createServerAdapter(async (request, serverContext) => {
14
+ const context = {
15
+ ...unchainedAPI,
16
+ ...serverContext.unchainedContext,
17
+ params: serverContext.params || {},
18
+ rawRequest: serverContext.rawRequest,
19
+ };
20
+ try {
21
+ return await route.handler(request, context);
22
+ }
23
+ catch (error) {
24
+ fastify.log.error(`Error in route handler ${route.method} ${route.path}: ${error instanceof Error ? error.message : String(error)}`);
25
+ return new Response(JSON.stringify({
26
+ error: error instanceof Error ? error.message : 'Internal Server Error',
27
+ }), {
28
+ status: 500,
29
+ headers: { 'Content-Type': 'application/json' },
30
+ });
31
+ }
32
+ });
33
+ let methods;
34
+ if (route.method === 'ALL') {
35
+ methods = ['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'OPTIONS'];
36
+ }
37
+ else {
38
+ methods = [route.method];
39
+ }
40
+ scope.route({
41
+ url: route.path,
42
+ method: methods,
43
+ handler: async (req, reply) => {
44
+ const response = await adapter.handleNodeRequestAndResponse(req.raw, reply.raw, {
45
+ unchainedContext: req.unchainedContext,
46
+ params: req.params,
47
+ rawRequest: req.raw,
48
+ });
49
+ response.headers.forEach((value, key) => {
50
+ reply.header(key, value);
51
+ });
52
+ reply.status(response.status);
53
+ reply.send(response.body || undefined);
54
+ return reply;
55
+ },
56
+ });
57
+ }
58
+ registered();
59
+ });
60
+ }
@@ -0,0 +1,4 @@
1
+ import type { PluginHttpRoute } from '@unchainedshop/core';
2
+ import type { OIDCProviderConfig } from '../auth.ts';
3
+ export declare function createBackchannelLogoutRoute(providers: OIDCProviderConfig[]): PluginHttpRoute;
4
+ export default createBackchannelLogoutRoute;
@@ -0,0 +1,170 @@
1
+ import * as jose from 'jose';
2
+ import { createLogger } from '@unchainedshop/logger';
3
+ const logger = createLogger('unchained:api:backchannel-logout');
4
+ function normalizeIssuer(url) {
5
+ try {
6
+ const parsed = new URL(url);
7
+ return parsed.origin + parsed.pathname.replace(/\/$/, '');
8
+ }
9
+ catch {
10
+ return url;
11
+ }
12
+ }
13
+ const jwksCache = new Map();
14
+ function getJWKS(jwksUri) {
15
+ let jwks = jwksCache.get(jwksUri);
16
+ if (!jwks) {
17
+ jwks = jose.createRemoteJWKSet(new URL(jwksUri), {
18
+ cooldownDuration: 30000,
19
+ cacheMaxAge: 600000,
20
+ });
21
+ jwksCache.set(jwksUri, jwks);
22
+ }
23
+ return jwks;
24
+ }
25
+ export function createBackchannelLogoutRoute(providers) {
26
+ return {
27
+ path: '/backchannel-logout',
28
+ method: 'ALL',
29
+ handler: async (request, context) => {
30
+ if (request.method !== 'POST') {
31
+ return new Response(JSON.stringify({ error: 'method_not_allowed' }), {
32
+ status: 405,
33
+ headers: { 'Content-Type': 'application/json' },
34
+ });
35
+ }
36
+ try {
37
+ let logoutToken = null;
38
+ const contentType = request.headers.get('content-type') || '';
39
+ if (contentType.includes('application/x-www-form-urlencoded')) {
40
+ const formData = await request.formData();
41
+ logoutToken = formData.get('logout_token');
42
+ }
43
+ else if (contentType.includes('application/json')) {
44
+ const body = (await request.json());
45
+ logoutToken = body.logout_token || null;
46
+ }
47
+ if (!logoutToken) {
48
+ logger.warn('Back-channel logout request missing logout_token');
49
+ return new Response(JSON.stringify({ error: 'missing_logout_token' }), {
50
+ status: 400,
51
+ headers: { 'Content-Type': 'application/json' },
52
+ });
53
+ }
54
+ let decodedPayload;
55
+ try {
56
+ const parts = logoutToken.split('.');
57
+ if (parts.length !== 3) {
58
+ logger.warn('Invalid logout token format: not a valid JWT');
59
+ return new Response(JSON.stringify({ error: 'invalid_token' }), {
60
+ status: 400,
61
+ headers: { 'Content-Type': 'application/json' },
62
+ });
63
+ }
64
+ decodedPayload = JSON.parse(Buffer.from(parts[1], 'base64url').toString());
65
+ }
66
+ catch {
67
+ logger.warn('Failed to decode logout token');
68
+ return new Response(JSON.stringify({ error: 'invalid_token' }), {
69
+ status: 400,
70
+ headers: { 'Content-Type': 'application/json' },
71
+ });
72
+ }
73
+ const { iss } = decodedPayload;
74
+ if (!iss || typeof iss !== 'string') {
75
+ logger.warn('Logout token missing issuer (iss)');
76
+ return new Response(JSON.stringify({ error: 'invalid_token' }), {
77
+ status: 400,
78
+ headers: { 'Content-Type': 'application/json' },
79
+ });
80
+ }
81
+ const normalizedIss = normalizeIssuer(iss);
82
+ const provider = providers.find((p) => normalizeIssuer(p.issuer) === normalizedIss);
83
+ if (!provider) {
84
+ logger.warn('Unknown issuer in logout token:', { iss });
85
+ return new Response(JSON.stringify({ error: 'unknown_issuer' }), {
86
+ status: 400,
87
+ headers: { 'Content-Type': 'application/json' },
88
+ });
89
+ }
90
+ const jwksUri = provider.jwksUri || `${provider.issuer}/.well-known/jwks.json`;
91
+ let verifiedPayload;
92
+ try {
93
+ const JWKS = getJWKS(jwksUri);
94
+ const verifyOptions = {
95
+ issuer: provider.issuer,
96
+ };
97
+ if (provider.audience) {
98
+ verifyOptions.audience = provider.audience;
99
+ }
100
+ const { payload } = await jose.jwtVerify(logoutToken, JWKS, verifyOptions);
101
+ verifiedPayload = payload;
102
+ }
103
+ catch (error) {
104
+ if (error instanceof jose.errors.JWTExpired) {
105
+ logger.warn('Logout token expired');
106
+ return new Response(JSON.stringify({ error: 'token_expired' }), {
107
+ status: 400,
108
+ headers: { 'Content-Type': 'application/json' },
109
+ });
110
+ }
111
+ else if (error instanceof jose.errors.JWTClaimValidationFailed) {
112
+ logger.warn('Logout token claim validation failed:', { message: error.message });
113
+ return new Response(JSON.stringify({ error: 'invalid_claims' }), {
114
+ status: 400,
115
+ headers: { 'Content-Type': 'application/json' },
116
+ });
117
+ }
118
+ else if (error instanceof jose.errors.JWSSignatureVerificationFailed) {
119
+ logger.warn('Logout token signature verification failed - possible forgery attempt');
120
+ return new Response(JSON.stringify({ error: 'invalid_signature' }), {
121
+ status: 400,
122
+ headers: { 'Content-Type': 'application/json' },
123
+ });
124
+ }
125
+ else {
126
+ logger.error('Logout token verification failed:', { error });
127
+ return new Response(JSON.stringify({ error: 'verification_failed' }), {
128
+ status: 400,
129
+ headers: { 'Content-Type': 'application/json' },
130
+ });
131
+ }
132
+ }
133
+ const { sub, events } = verifiedPayload;
134
+ if (!events?.['http://schemas.openid.net/event/backchannel-logout']) {
135
+ logger.warn('Token is not a back-channel logout token (missing events claim)');
136
+ return new Response(JSON.stringify({ error: 'invalid_token_type' }), {
137
+ status: 400,
138
+ headers: { 'Content-Type': 'application/json' },
139
+ });
140
+ }
141
+ if (!sub) {
142
+ logger.warn('Logout token missing subject (sub)');
143
+ return new Response(JSON.stringify({ error: 'missing_subject' }), {
144
+ status: 400,
145
+ headers: { 'Content-Type': 'application/json' },
146
+ });
147
+ }
148
+ const user = await context.modules.users.findUserById(sub);
149
+ if (!user) {
150
+ logger.info('User not found for back-channel logout:', { sub });
151
+ return new Response('', { status: 200 });
152
+ }
153
+ await context.modules.users.updateOidcLogoutAt(user._id, new Date());
154
+ logger.info('Back-channel logout processed successfully:', {
155
+ userId: user._id,
156
+ issuer: iss,
157
+ });
158
+ return new Response('', { status: 200 });
159
+ }
160
+ catch (error) {
161
+ logger.error('Back-channel logout error:', { error });
162
+ return new Response(JSON.stringify({ error: 'internal_error' }), {
163
+ status: 500,
164
+ headers: { 'Content-Type': 'application/json' },
165
+ });
166
+ }
167
+ },
168
+ };
169
+ }
170
+ export default createBackchannelLogoutRoute;
@@ -0,0 +1,29 @@
1
+ import { type AuthConfig } from '../auth.ts';
2
+ import type { LoginFn, LogoutFn } from '../context.ts';
3
+ export interface AuthContextParams {
4
+ getHeader: (key: string) => string | undefined;
5
+ setHeader: (key: string, value: string) => void;
6
+ getCookie: (name: string) => string | undefined;
7
+ setCookie: (name: string, value: string, options: CookieOptions) => void;
8
+ clearCookie: (name: string, options: CookieOptions) => void;
9
+ remoteAddress?: string;
10
+ remotePort?: number;
11
+ }
12
+ export interface CookieOptions {
13
+ domain?: string;
14
+ path?: string;
15
+ secure?: boolean;
16
+ httpOnly?: boolean;
17
+ sameSite?: 'strict' | 'lax' | 'none' | boolean;
18
+ maxAge?: number;
19
+ expires?: Date;
20
+ }
21
+ export interface AuthContext {
22
+ userId?: string;
23
+ tokenVersion?: number;
24
+ impersonatorId?: string;
25
+ accessToken?: string;
26
+ login: LoginFn;
27
+ logout: LogoutFn;
28
+ }
29
+ export declare function createAuthContext(params: AuthContextParams, authConfig?: AuthConfig): Promise<AuthContext>;
@@ -0,0 +1,91 @@
1
+ import { emit } from '@unchainedshop/events';
2
+ import { signAccessToken, createAuthHandler } from "../auth.js";
3
+ import { API_EVENTS } from "../events.js";
4
+ import { createLogger } from '@unchainedshop/logger';
5
+ const logger = createLogger('unchained:api:auth-middleware');
6
+ const { UNCHAINED_COOKIE_NAME = 'unchained_token', UNCHAINED_COOKIE_PATH = '/', UNCHAINED_COOKIE_DOMAIN, UNCHAINED_COOKIE_SAMESITE = 'lax', UNCHAINED_COOKIE_INSECURE, UNCHAINED_TOKEN_EXPIRY_SECONDS = '3600', } = process.env;
7
+ function getTokenCookieOptions(expires) {
8
+ const secure = !UNCHAINED_COOKIE_INSECURE;
9
+ const sameSite = {
10
+ none: 'none',
11
+ lax: 'lax',
12
+ strict: 'strict',
13
+ '1': true,
14
+ '0': false,
15
+ }[UNCHAINED_COOKIE_SAMESITE?.trim()?.toLowerCase()] || 'lax';
16
+ if (!secure && process.env.NODE_ENV === 'production') {
17
+ logger.warn('SECURITY WARNING: Running with UNCHAINED_COOKIE_INSECURE in production is not recommended');
18
+ }
19
+ if (sameSite === 'none' && !secure) {
20
+ logger.warn('SECURITY WARNING: SameSite=None requires Secure flag to be effective');
21
+ }
22
+ const expirySeconds = parseInt(UNCHAINED_TOKEN_EXPIRY_SECONDS, 10);
23
+ return {
24
+ domain: UNCHAINED_COOKIE_DOMAIN,
25
+ path: UNCHAINED_COOKIE_PATH,
26
+ secure,
27
+ httpOnly: true,
28
+ sameSite: sameSite,
29
+ maxAge: expires ? undefined : expirySeconds * 1000,
30
+ expires,
31
+ };
32
+ }
33
+ function extractBearerToken(authHeader) {
34
+ if (!authHeader)
35
+ return undefined;
36
+ const parts = authHeader.split(' ');
37
+ if (parts.length !== 2)
38
+ return undefined;
39
+ const [scheme, token] = parts;
40
+ if (scheme.toLowerCase() !== 'bearer') {
41
+ logger.debug('Authorization header present but not Bearer scheme');
42
+ return undefined;
43
+ }
44
+ return token;
45
+ }
46
+ export async function createAuthContext(params, authConfig) {
47
+ const { getHeader, getCookie, setCookie, clearCookie } = params;
48
+ const authHeader = getHeader('authorization');
49
+ const headerToken = extractBearerToken(authHeader);
50
+ const cookieToken = getCookie(UNCHAINED_COOKIE_NAME);
51
+ const token = headerToken || cookieToken;
52
+ const verifyToken = createAuthHandler(authConfig);
53
+ const authResult = token ? await verifyToken(token) : {};
54
+ const login = async (user, options = {}) => {
55
+ const { impersonator } = options;
56
+ const tokenVersion = user.tokenVersion ?? 1;
57
+ const { token: newToken, expires } = await signAccessToken(user._id, tokenVersion, {
58
+ impersonatorId: impersonator?._id,
59
+ });
60
+ setCookie(UNCHAINED_COOKIE_NAME, newToken, getTokenCookieOptions(expires));
61
+ const tokenObject = {
62
+ _id: crypto.randomUUID(),
63
+ userId: user._id,
64
+ userName: user.username || user.emails?.[0]?.address,
65
+ tokenExpires: expires,
66
+ remoteAddress: params.remoteAddress,
67
+ };
68
+ await emit(API_EVENTS.API_LOGIN_TOKEN_CREATED, tokenObject);
69
+ user._inLoginMethodResponse = true;
70
+ return { user, ...tokenObject };
71
+ };
72
+ const logout = async () => {
73
+ clearCookie(UNCHAINED_COOKIE_NAME, getTokenCookieOptions());
74
+ if (authResult.userId) {
75
+ const tokenObject = {
76
+ _id: crypto.randomUUID(),
77
+ userId: authResult.userId,
78
+ };
79
+ await emit(API_EVENTS.API_LOGOUT, tokenObject);
80
+ }
81
+ return true;
82
+ };
83
+ return {
84
+ userId: authResult.userId,
85
+ tokenVersion: authResult.tokenVersion,
86
+ impersonatorId: authResult.impersonatorId,
87
+ accessToken: authResult.isApiKey ? authResult.accessToken : undefined,
88
+ login,
89
+ logout,
90
+ };
91
+ }
@@ -0,0 +1,6 @@
1
+ import type { Context } from '../../../context.ts';
2
+ export default function logoutAllSessions(root: never, params: {
3
+ userId?: string;
4
+ }, context: Context): Promise<{
5
+ success: boolean;
6
+ }>;
@@ -0,0 +1,17 @@
1
+ import { log } from '@unchainedshop/logger';
2
+ import { UserNotFoundError } from "../../../errors.js";
3
+ export default async function logoutAllSessions(root, params, context) {
4
+ const { userId, modules } = context;
5
+ const normalizedUserId = params.userId || userId;
6
+ log(`mutation logoutAllSessions ${normalizedUserId}`, { userId });
7
+ if (!(await modules.users.userExists({ userId: normalizedUserId })))
8
+ throw new UserNotFoundError({ userId: normalizedUserId });
9
+ const result = await modules.users.incrementTokenVersion(normalizedUserId);
10
+ if (!result) {
11
+ throw new Error('Failed to logout all sessions', { cause: 'LOGOUT_FAILED' });
12
+ }
13
+ if (normalizedUserId === userId) {
14
+ await context.logout();
15
+ }
16
+ return { success: true };
17
+ }
@@ -0,0 +1,10 @@
1
+ import type { Context } from '../../../context.ts';
2
+ export default function bulkAssignProductsToAssortment(root: never, { productIds, assortmentId }: {
3
+ productIds: string[];
4
+ assortmentId: string;
5
+ }, { modules, services, userId }: Context): Promise<{
6
+ successIds: string[];
7
+ successCount: number;
8
+ failedIds: string[];
9
+ failedCount: number;
10
+ }>;
@@ -0,0 +1,18 @@
1
+ import { log } from '@unchainedshop/logger';
2
+ import { InvalidIdError, AssortmentNotFoundError } from "../../../errors.js";
3
+ import { createBulkOperationResult, normalizeBulkIds } from "./bulkOperation.js";
4
+ export default async function bulkAssignProductsToAssortment(root, { productIds, assortmentId }, { modules, services, userId }) {
5
+ log(`mutation bulkAssignProductsToAssortment ${assortmentId} for ${productIds.length} products`, {
6
+ userId,
7
+ });
8
+ const normalizedProductIds = normalizeBulkIds(productIds);
9
+ if (!assortmentId)
10
+ throw new InvalidIdError({ assortmentId });
11
+ if (!(await modules.assortments.assortmentExists({ assortmentId })))
12
+ throw new AssortmentNotFoundError({ assortmentId });
13
+ const result = await services.assortments.bulkAssignProductsToAssortment({
14
+ assortmentId,
15
+ productIds: normalizedProductIds,
16
+ });
17
+ return createBulkOperationResult(result);
18
+ }
@@ -0,0 +1,12 @@
1
+ export declare const MAX_BULK_OPERATION_SIZE = 1000;
2
+ export interface BulkOperationIds {
3
+ successIds: string[];
4
+ failedIds: string[];
5
+ }
6
+ export declare const normalizeBulkIds: (ids: readonly string[]) => string[];
7
+ export declare const createBulkOperationResult: ({ successIds, failedIds }: BulkOperationIds) => {
8
+ successIds: string[];
9
+ successCount: number;
10
+ failedIds: string[];
11
+ failedCount: number;
12
+ };
@@ -0,0 +1,14 @@
1
+ import { BulkOperationTooLargeError } from "../../../errors.js";
2
+ export const MAX_BULK_OPERATION_SIZE = 1000;
3
+ export const normalizeBulkIds = (ids) => {
4
+ if (ids.length > MAX_BULK_OPERATION_SIZE) {
5
+ throw new BulkOperationTooLargeError({ limit: MAX_BULK_OPERATION_SIZE });
6
+ }
7
+ return [...new Set(ids)];
8
+ };
9
+ export const createBulkOperationResult = ({ successIds, failedIds }) => ({
10
+ successIds,
11
+ successCount: successIds.length,
12
+ failedIds,
13
+ failedCount: failedIds.length,
14
+ });
@@ -0,0 +1,9 @@
1
+ import type { Context } from '../../../context.ts';
2
+ export default function bulkRemoveAssortments(root: never, { assortmentIds }: {
3
+ assortmentIds: string[];
4
+ }, { modules, userId }: Context): Promise<{
5
+ successIds: string[];
6
+ successCount: number;
7
+ failedIds: string[];
8
+ failedCount: number;
9
+ }>;
@@ -0,0 +1,7 @@
1
+ import { log } from '@unchainedshop/logger';
2
+ import { createBulkOperationResult, normalizeBulkIds } from "./bulkOperation.js";
3
+ export default async function bulkRemoveAssortments(root, { assortmentIds }, { modules, userId }) {
4
+ log(`mutation bulkRemoveAssortments for ${assortmentIds.length} assortments`, { userId });
5
+ const result = await modules.assortments.bulkDelete(normalizeBulkIds(assortmentIds));
6
+ return createBulkOperationResult(result);
7
+ }
@@ -0,0 +1,9 @@
1
+ import type { Context } from '../../../context.ts';
2
+ export default function bulkRemoveFilters(root: never, { filterIds }: {
3
+ filterIds: string[];
4
+ }, { services, userId }: Context): Promise<{
5
+ successIds: string[];
6
+ successCount: number;
7
+ failedIds: string[];
8
+ failedCount: number;
9
+ }>;
@@ -0,0 +1,9 @@
1
+ import { log } from '@unchainedshop/logger';
2
+ import { createBulkOperationResult, normalizeBulkIds } from "./bulkOperation.js";
3
+ export default async function bulkRemoveFilters(root, { filterIds }, { services, userId }) {
4
+ log(`mutation bulkRemoveFilters for ${filterIds.length} filters`, { userId });
5
+ const result = await services.filters.bulkRemoveFilters({
6
+ filterIds: normalizeBulkIds(filterIds),
7
+ });
8
+ return createBulkOperationResult(result);
9
+ }
@@ -0,0 +1,9 @@
1
+ import type { Context } from '../../../context.ts';
2
+ export default function bulkRemoveProducts(root: never, { productIds }: {
3
+ productIds: string[];
4
+ }, { services, userId }: Context): Promise<{
5
+ successIds: string[];
6
+ successCount: number;
7
+ failedIds: string[];
8
+ failedCount: number;
9
+ }>;
@@ -0,0 +1,9 @@
1
+ import { log } from '@unchainedshop/logger';
2
+ import { createBulkOperationResult, normalizeBulkIds } from "./bulkOperation.js";
3
+ export default async function bulkRemoveProducts(root, { productIds }, { services, userId }) {
4
+ log(`mutation bulkRemoveProducts for ${productIds.length} products`, { userId });
5
+ const result = await services.products.bulkRemoveProducts({
6
+ productIds: normalizeBulkIds(productIds),
7
+ });
8
+ return createBulkOperationResult(result);
9
+ }
@@ -0,0 +1,9 @@
1
+ import type { Context } from '../../../context.ts';
2
+ export default function bulkRemoveUsers(root: never, { userIds }: {
3
+ userIds: string[];
4
+ }, { services, userId }: Context): Promise<{
5
+ successIds: string[];
6
+ successCount: number;
7
+ failedIds: string[];
8
+ failedCount: number;
9
+ }>;
@@ -0,0 +1,7 @@
1
+ import { log } from '@unchainedshop/logger';
2
+ import { createBulkOperationResult, normalizeBulkIds } from "./bulkOperation.js";
3
+ export default async function bulkRemoveUsers(root, { userIds }, { services, userId }) {
4
+ log(`mutation bulkRemoveUsers for ${userIds.length} users`, { userId });
5
+ const result = await services.users.bulkDeleteUsers({ userIds: normalizeBulkIds(userIds) });
6
+ return createBulkOperationResult(result);
7
+ }
@@ -0,0 +1,10 @@
1
+ import type { Context } from '../../../context.ts';
2
+ export default function bulkSetAssortmentActive(root: never, { assortmentIds, isActive }: {
3
+ assortmentIds: string[];
4
+ isActive: boolean;
5
+ }, { modules, userId }: Context): Promise<{
6
+ successIds: string[];
7
+ successCount: number;
8
+ failedIds: string[];
9
+ failedCount: number;
10
+ }>;
@@ -0,0 +1,9 @@
1
+ import { log } from '@unchainedshop/logger';
2
+ import { createBulkOperationResult, normalizeBulkIds } from "./bulkOperation.js";
3
+ export default async function bulkSetAssortmentActive(root, { assortmentIds, isActive }, { modules, userId }) {
4
+ log(`mutation bulkSetAssortmentActive ${isActive} for ${assortmentIds.length} assortments`, {
5
+ userId,
6
+ });
7
+ const result = await modules.assortments.bulkSetActive(normalizeBulkIds(assortmentIds), isActive);
8
+ return createBulkOperationResult(result);
9
+ }
@@ -0,0 +1,10 @@
1
+ import type { Context } from '../../../context.ts';
2
+ export default function bulkSetFilterActive(root: never, { filterIds, isActive }: {
3
+ filterIds: string[];
4
+ isActive: boolean;
5
+ }, { services, userId }: Context): Promise<{
6
+ successIds: string[];
7
+ successCount: number;
8
+ failedIds: string[];
9
+ failedCount: number;
10
+ }>;
@@ -0,0 +1,10 @@
1
+ import { log } from '@unchainedshop/logger';
2
+ import { createBulkOperationResult, normalizeBulkIds } from "./bulkOperation.js";
3
+ export default async function bulkSetFilterActive(root, { filterIds, isActive }, { services, userId }) {
4
+ log(`mutation bulkSetFilterActive ${isActive} for ${filterIds.length} filters`, { userId });
5
+ const result = await services.filters.bulkSetFilterActive({
6
+ filterIds: normalizeBulkIds(filterIds),
7
+ isActive,
8
+ });
9
+ return createBulkOperationResult(result);
10
+ }
@@ -0,0 +1,10 @@
1
+ import type { Context } from '../../../context.ts';
2
+ export default function bulkSetProductStatus(root: never, { productIds, status }: {
3
+ productIds: string[];
4
+ status: string;
5
+ }, { modules, userId }: Context): Promise<{
6
+ successIds: string[];
7
+ successCount: number;
8
+ failedIds: string[];
9
+ failedCount: number;
10
+ }>;
@@ -0,0 +1,17 @@
1
+ import { log } from '@unchainedshop/logger';
2
+ import { createBulkOperationResult, normalizeBulkIds } from "./bulkOperation.js";
3
+ export default async function bulkSetProductStatus(root, { productIds, status }, { modules, userId }) {
4
+ log(`mutation bulkSetProductStatus ${status} for ${productIds.length} products`, { userId });
5
+ const normalizedProductIds = normalizeBulkIds(productIds);
6
+ let result;
7
+ if (status === 'ACTIVE') {
8
+ result = await modules.products.bulkPublish(normalizedProductIds);
9
+ }
10
+ else if (status === 'DRAFT') {
11
+ result = await modules.products.bulkUnpublish(normalizedProductIds);
12
+ }
13
+ else {
14
+ result = { successIds: [], failedIds: normalizedProductIds };
15
+ }
16
+ return createBulkOperationResult(result);
17
+ }
@@ -0,0 +1,10 @@
1
+ import type { Context } from '../../../context.ts';
2
+ export default function bulkSetUserRoles(root: never, { userIds, roles }: {
3
+ userIds: string[];
4
+ roles: string[];
5
+ }, context: Context): Promise<{
6
+ successIds: string[];
7
+ successCount: number;
8
+ failedIds: string[];
9
+ failedCount: number;
10
+ }>;
@@ -0,0 +1,14 @@
1
+ import { log } from '@unchainedshop/logger';
2
+ import { getPublicRoles } from "../../../roles/index.js";
3
+ import { createBulkOperationResult, normalizeBulkIds } from "./bulkOperation.js";
4
+ export default async function bulkSetUserRoles(root, { userIds, roles }, context) {
5
+ const { modules, userId } = context;
6
+ log(`mutation bulkSetUserRoles for ${userIds.length} users`, { userId });
7
+ const publicRoles = getPublicRoles(context.roles);
8
+ const invalidRoles = roles.filter((r) => !publicRoles.includes(r));
9
+ if (invalidRoles.length) {
10
+ throw new Error(`Invalid role names: ${invalidRoles.join(', ')}`);
11
+ }
12
+ const result = await modules.users.bulkUpdateRoles(normalizeBulkIds(userIds), roles);
13
+ return createBulkOperationResult(result);
14
+ }
@@ -0,0 +1,11 @@
1
+ import type { Context } from '../../../context.ts';
2
+ export default function bulkUpdateAssortmentTags(root: never, { assortmentIds, add, remove }: {
3
+ assortmentIds: string[];
4
+ add?: string[];
5
+ remove?: string[];
6
+ }, { modules, userId }: Context): Promise<{
7
+ successIds: string[];
8
+ successCount: number;
9
+ failedIds: string[];
10
+ failedCount: number;
11
+ }>;
@@ -0,0 +1,10 @@
1
+ import { log } from '@unchainedshop/logger';
2
+ import { createBulkOperationResult, normalizeBulkIds } from "./bulkOperation.js";
3
+ export default async function bulkUpdateAssortmentTags(root, { assortmentIds, add, remove }, { modules, userId }) {
4
+ log(`mutation bulkUpdateAssortmentTags for ${assortmentIds.length} assortments`, { userId });
5
+ const result = await modules.assortments.bulkUpdateTags(normalizeBulkIds(assortmentIds), {
6
+ add,
7
+ remove,
8
+ });
9
+ return createBulkOperationResult(result);
10
+ }