@unchainedshop/api 5.0.0-alpha.4 → 5.0.0-alpha.5

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 (68) hide show
  1. package/lib/acl.js +4 -0
  2. package/lib/adminUiPlugins.d.ts +12 -1
  3. package/lib/adminUiPlugins.js +188 -26
  4. package/lib/errors.d.ts +2 -0
  5. package/lib/errors.js +2 -0
  6. package/lib/events.d.ts +1 -0
  7. package/lib/events.js +1 -0
  8. package/lib/express/index.js +44 -17
  9. package/lib/fastify/index.d.ts +2 -0
  10. package/lib/fastify/index.js +65 -24
  11. package/lib/mcp/tools/system/handlers/allocateWork.js +2 -2
  12. package/lib/mcp/tools/users/index.js +1 -1
  13. package/lib/middleware/createAuthMiddleware.js +2 -0
  14. package/lib/resolvers/mutations/accounts/loginWithPassword.js +16 -2
  15. package/lib/resolvers/mutations/accounts/logoutAllSessions.d.ts +3 -1
  16. package/lib/resolvers/mutations/accounts/logoutAllSessions.js +10 -4
  17. package/lib/resolvers/mutations/bulk/bulkAssignProductsToAssortment.d.ts +10 -0
  18. package/lib/resolvers/mutations/bulk/bulkAssignProductsToAssortment.js +18 -0
  19. package/lib/resolvers/mutations/bulk/bulkOperation.d.ts +12 -0
  20. package/lib/resolvers/mutations/bulk/bulkOperation.js +14 -0
  21. package/lib/resolvers/mutations/bulk/bulkRemoveAssortments.d.ts +9 -0
  22. package/lib/resolvers/mutations/bulk/bulkRemoveAssortments.js +7 -0
  23. package/lib/resolvers/mutations/bulk/bulkRemoveFilters.d.ts +9 -0
  24. package/lib/resolvers/mutations/bulk/bulkRemoveFilters.js +9 -0
  25. package/lib/resolvers/mutations/bulk/bulkRemoveProducts.d.ts +9 -0
  26. package/lib/resolvers/mutations/bulk/bulkRemoveProducts.js +9 -0
  27. package/lib/resolvers/mutations/bulk/bulkRemoveUsers.d.ts +9 -0
  28. package/lib/resolvers/mutations/bulk/bulkRemoveUsers.js +7 -0
  29. package/lib/resolvers/mutations/bulk/bulkSetAssortmentActive.d.ts +10 -0
  30. package/lib/resolvers/mutations/bulk/bulkSetAssortmentActive.js +9 -0
  31. package/lib/resolvers/mutations/bulk/bulkSetFilterActive.d.ts +10 -0
  32. package/lib/resolvers/mutations/bulk/bulkSetFilterActive.js +10 -0
  33. package/lib/resolvers/mutations/bulk/bulkSetProductStatus.d.ts +10 -0
  34. package/lib/resolvers/mutations/bulk/bulkSetProductStatus.js +17 -0
  35. package/lib/resolvers/mutations/bulk/bulkSetUserRoles.d.ts +10 -0
  36. package/lib/resolvers/mutations/bulk/bulkSetUserRoles.js +14 -0
  37. package/lib/resolvers/mutations/bulk/bulkUpdateAssortmentTags.d.ts +11 -0
  38. package/lib/resolvers/mutations/bulk/bulkUpdateAssortmentTags.js +10 -0
  39. package/lib/resolvers/mutations/bulk/bulkUpdateProductTags.d.ts +11 -0
  40. package/lib/resolvers/mutations/bulk/bulkUpdateProductTags.js +7 -0
  41. package/lib/resolvers/mutations/bulk/bulkUpdateUserTags.d.ts +11 -0
  42. package/lib/resolvers/mutations/bulk/bulkUpdateUserTags.js +7 -0
  43. package/lib/resolvers/mutations/index.d.ts +12 -0
  44. package/lib/resolvers/mutations/index.js +25 -1
  45. package/lib/resolvers/mutations/users/removeUser.js +9 -2
  46. package/lib/resolvers/mutations/users/setRoles.js +9 -2
  47. package/lib/resolvers/mutations/worker/allocateWork.d.ts +1 -1
  48. package/lib/resolvers/mutations/worker/allocateWork.js +4 -3
  49. package/lib/resolvers/queries/index.d.ts +1 -0
  50. package/lib/resolvers/queries/index.js +2 -0
  51. package/lib/resolvers/queries/search/globalSearch.d.ts +36 -0
  52. package/lib/resolvers/queries/search/globalSearch.js +200 -0
  53. package/lib/resolvers/type/global-search-result-types.d.ts +3 -0
  54. package/lib/resolvers/type/global-search-result-types.js +15 -0
  55. package/lib/resolvers/type/index.d.ts +3 -0
  56. package/lib/resolvers/type/index.js +2 -0
  57. package/lib/resolvers/type/user-types.d.ts +1 -0
  58. package/lib/resolvers/type/user-types.js +35 -4
  59. package/lib/roles/all.js +12 -0
  60. package/lib/roles/index.js +12 -0
  61. package/lib/roles/loggedIn.js +1 -1
  62. package/lib/schema/inputTypes.js +5 -0
  63. package/lib/schema/mutation.js +74 -1
  64. package/lib/schema/query.js +15 -0
  65. package/lib/schema/types/common.js +7 -0
  66. package/lib/schema/types/search.js +36 -0
  67. package/lib/schema/types/user.js +4 -0
  68. package/package.json +25 -25
@@ -0,0 +1,200 @@
1
+ import { log } from '@unchainedshop/logger';
2
+ import { checkAction } from "../../../acl.js";
3
+ import { actions } from "../../../roles/index.js";
4
+ const ALL_TYPES = [
5
+ 'PRODUCT',
6
+ 'USER',
7
+ 'ORDER',
8
+ 'ASSORTMENT',
9
+ 'FILTER',
10
+ 'ENROLLMENT',
11
+ 'QUOTATION',
12
+ 'WORK',
13
+ ];
14
+ const DEFAULT_LIMIT = 5;
15
+ const MAX_LIMIT = 50;
16
+ const typeActionMap = {
17
+ PRODUCT: actions.viewProducts,
18
+ USER: actions.viewUsers,
19
+ ORDER: actions.viewOrders,
20
+ ASSORTMENT: actions.viewAssortments,
21
+ FILTER: actions.viewFilters,
22
+ ENROLLMENT: actions.viewEnrollments,
23
+ QUOTATION: actions.viewQuotations,
24
+ WORK: actions.viewWorkQueue,
25
+ };
26
+ function getLimitForType(type, defaultLimit, typeLimits) {
27
+ const requestedLimit = typeLimits?.find((typeLimit) => typeLimit.type === type)?.limit ?? defaultLimit;
28
+ return Math.min(MAX_LIMIT, Math.max(1, requestedLimit));
29
+ }
30
+ function getSearchParams(type, queryString, limit, options) {
31
+ switch (type) {
32
+ case 'PRODUCT':
33
+ return {
34
+ queryString,
35
+ limit,
36
+ offset: 0,
37
+ includeDrafts: options.includeDraftProducts,
38
+ };
39
+ case 'USER':
40
+ return {
41
+ queryString,
42
+ limit,
43
+ offset: 0,
44
+ includeGuests: options.includeGuestUsers,
45
+ };
46
+ case 'ORDER':
47
+ return {
48
+ queryString,
49
+ limit,
50
+ offset: 0,
51
+ includeCarts: options.includeCarts,
52
+ };
53
+ case 'ASSORTMENT':
54
+ return {
55
+ queryString,
56
+ limit,
57
+ offset: 0,
58
+ includeInactive: options.includeInactiveAssortments,
59
+ };
60
+ case 'FILTER':
61
+ return {
62
+ queryString,
63
+ limit,
64
+ offset: 0,
65
+ includeInactive: options.includeInactiveFilters,
66
+ };
67
+ case 'ENROLLMENT':
68
+ case 'QUOTATION':
69
+ return { queryString, limit, offset: 0 };
70
+ case 'WORK':
71
+ return { queryString, limit, skip: 0 };
72
+ }
73
+ }
74
+ const typeNameMap = {
75
+ PRODUCT: 'Product',
76
+ USER: 'User',
77
+ ORDER: 'Order',
78
+ ASSORTMENT: 'Assortment',
79
+ FILTER: 'Filter',
80
+ ENROLLMENT: 'Enrollment',
81
+ QUOTATION: 'Quotation',
82
+ WORK: 'Work',
83
+ };
84
+ async function searchType(type, queryString, limit, modules, options) {
85
+ let results;
86
+ switch (type) {
87
+ case 'PRODUCT':
88
+ results = await modules.products.findProducts({
89
+ queryString,
90
+ limit,
91
+ offset: 0,
92
+ includeDrafts: options.includeDraftProducts,
93
+ });
94
+ break;
95
+ case 'USER':
96
+ results = await modules.users.findUsers({
97
+ queryString,
98
+ limit,
99
+ offset: 0,
100
+ includeGuests: options.includeGuestUsers,
101
+ });
102
+ break;
103
+ case 'ORDER':
104
+ results = await modules.orders.findOrders({
105
+ queryString,
106
+ limit,
107
+ offset: 0,
108
+ includeCarts: options.includeCarts,
109
+ });
110
+ break;
111
+ case 'ASSORTMENT':
112
+ results = await modules.assortments.findAssortments({
113
+ queryString,
114
+ limit,
115
+ offset: 0,
116
+ includeInactive: options.includeInactiveAssortments,
117
+ });
118
+ break;
119
+ case 'FILTER':
120
+ results = await modules.filters.findFilters({
121
+ queryString,
122
+ limit,
123
+ offset: 0,
124
+ includeInactive: options.includeInactiveFilters,
125
+ });
126
+ break;
127
+ case 'ENROLLMENT':
128
+ results = await modules.enrollments.findEnrollments({ queryString, limit, offset: 0 });
129
+ break;
130
+ case 'QUOTATION':
131
+ results = await modules.quotations.findQuotations({ queryString, limit, offset: 0 });
132
+ break;
133
+ case 'WORK':
134
+ results = await modules.worker.findWorkQueue({ queryString, limit, skip: 0 });
135
+ break;
136
+ default:
137
+ return [];
138
+ }
139
+ const __typename = typeNameMap[type];
140
+ return results.map((r) => ({ ...r, __typename }));
141
+ }
142
+ async function countType(type, queryString, modules, options) {
143
+ switch (type) {
144
+ case 'PRODUCT':
145
+ return modules.products.count({ queryString, includeDrafts: options.includeDraftProducts });
146
+ case 'USER':
147
+ return modules.users.count({ queryString, includeGuests: options.includeGuestUsers });
148
+ case 'ORDER':
149
+ return modules.orders.count({ queryString, includeCarts: options.includeCarts });
150
+ case 'ASSORTMENT':
151
+ return modules.assortments.count({
152
+ queryString,
153
+ includeInactive: options.includeInactiveAssortments,
154
+ });
155
+ case 'FILTER':
156
+ return modules.filters.count({ queryString, includeInactive: options.includeInactiveFilters });
157
+ case 'ENROLLMENT':
158
+ return modules.enrollments.count({ queryString });
159
+ case 'QUOTATION':
160
+ return modules.quotations.count({ queryString });
161
+ case 'WORK':
162
+ return modules.worker.count({ queryString });
163
+ default:
164
+ return 0;
165
+ }
166
+ }
167
+ async function checkTypeAuthorization(context, root, types, queryString, defaultLimit, typeLimits, options) {
168
+ await Promise.all(types.map((type) => checkAction(context, typeActionMap[type], [
169
+ root,
170
+ getSearchParams(type, queryString, getLimitForType(type, defaultLimit, typeLimits), options),
171
+ ])));
172
+ }
173
+ export default async function globalSearch(root, params, context) {
174
+ const { query: queryString, types, limit = DEFAULT_LIMIT, typeLimits, includeDraftProducts = true, includeInactiveAssortments = true, includeInactiveFilters = true, includeGuestUsers = false, includeCarts = false, } = params;
175
+ const { modules, userId } = context;
176
+ const sanitizedQuery = queryString?.trim().slice(0, 200) || '';
177
+ const requestedTypes = [...new Set(types?.length ? types : ALL_TYPES)];
178
+ const searchOptions = {
179
+ includeDraftProducts,
180
+ includeInactiveAssortments,
181
+ includeInactiveFilters,
182
+ includeGuestUsers,
183
+ includeCarts,
184
+ };
185
+ await checkTypeAuthorization(context, root, requestedTypes, sanitizedQuery, limit, typeLimits, searchOptions);
186
+ if (!sanitizedQuery)
187
+ return { results: [], counts: [] };
188
+ log(`query globalSearch: "${sanitizedQuery}" types: ${requestedTypes.join(',')}`, { userId });
189
+ const [resultsByType, countsByType] = await Promise.all([
190
+ Promise.all(requestedTypes.map((type) => searchType(type, sanitizedQuery, getLimitForType(type, limit, typeLimits), modules, searchOptions))),
191
+ Promise.all(requestedTypes.map((type) => countType(type, sanitizedQuery, modules, searchOptions))),
192
+ ]);
193
+ const results = resultsByType.flat();
194
+ const counts = requestedTypes.map((type, i) => ({
195
+ type,
196
+ totalCount: countsByType[i],
197
+ authorized: true,
198
+ }));
199
+ return { results, counts };
200
+ }
@@ -0,0 +1,3 @@
1
+ export declare const GlobalSearchResult: {
2
+ __resolveType(obj: Record<string, any>): string;
3
+ };
@@ -0,0 +1,15 @@
1
+ import { ProductType } from '@unchainedshop/core-products';
2
+ const productTypeMap = {
3
+ [ProductType.CONFIGURABLE_PRODUCT]: 'ConfigurableProduct',
4
+ [ProductType.BUNDLE_PRODUCT]: 'BundleProduct',
5
+ [ProductType.PLAN_PRODUCT]: 'PlanProduct',
6
+ [ProductType.TOKENIZED_PRODUCT]: 'TokenizedProduct',
7
+ };
8
+ export const GlobalSearchResult = {
9
+ __resolveType(obj) {
10
+ if (obj.__typename === 'Product') {
11
+ return productTypeMap[obj.type] || 'SimpleProduct';
12
+ }
13
+ return obj.__typename;
14
+ },
15
+ };
@@ -269,6 +269,9 @@ declare const types: {
269
269
  forceLocale?: string;
270
270
  }, { locale, loaders }: import("../../context.ts").Context): Promise<import("@unchainedshop/core-filters").FilterText>;
271
271
  };
272
+ GlobalSearchResult: {
273
+ __resolveType(obj: Record<string, any>): string;
274
+ };
272
275
  FilterOption: {
273
276
  _id(obj: import("@unchainedshop/core-filters").FilterOption): string;
274
277
  value(obj: import("@unchainedshop/core-filters").FilterOption): string;
@@ -16,6 +16,7 @@ import { EnrollmentDelivery } from "./enrollment/enrollment-delivery-types.js";
16
16
  import { EnrollmentPayment } from "./enrollment/enrollment-payment-types.js";
17
17
  import { EnrollmentPeriod } from "./enrollment/enrollment-period-types.js";
18
18
  import { EnrollmentPlan } from "./enrollment/enrollment-plan-tyes.js";
19
+ import { GlobalSearchResult } from "./global-search-result-types.js";
19
20
  import { Filter } from "./filter/filter-types.js";
20
21
  import { FilterOption } from "./filter/filter-option-types.js";
21
22
  import { Language } from "./language-types.js";
@@ -91,6 +92,7 @@ const types = {
91
92
  EnrollmentPeriod,
92
93
  EnrollmentPlan,
93
94
  Filter,
95
+ GlobalSearchResult,
94
96
  FilterOption,
95
97
  Language,
96
98
  LoadedFilter,
@@ -64,6 +64,7 @@ export interface UserHelperTypes {
64
64
  lastContact: HelperType<any, Contact>;
65
65
  lastLogin: HelperType<any, UserType['lastLogin']>;
66
66
  allowedActions: HelperType<any, string[]>;
67
+ viewerAllowedActions: HelperType<any, string[]>;
67
68
  name: HelperType<any, string>;
68
69
  orders: HelperType<{
69
70
  includeCarts: boolean;
@@ -1,7 +1,25 @@
1
1
  import { permissions } from '@unchainedshop/roles';
2
2
  import { checkAction, checkTypeResolver } from "../../acl.js";
3
3
  import { actions } from "../../roles/index.js";
4
- const { viewUserPrivateInfos, viewUserPublicInfos, viewUserOrders, viewUserEnrollments, viewUserQuotations, viewUserProductReviews, } = actions;
4
+ const { impersonate, logoutAllSessions, manageUsers, removeUser, updateUser, updateUsername, uploadUserAvatar, viewUser, viewUserRoles, viewUserPrivateInfos, viewUserPublicInfos, viewUserOrders, viewUserEnrollments, viewUserQuotations, viewUserProductReviews, viewUserTokens, } = actions;
5
+ const USER_TARGET_ACTIONS = [
6
+ impersonate,
7
+ logoutAllSessions,
8
+ manageUsers,
9
+ removeUser,
10
+ updateUser,
11
+ updateUsername,
12
+ uploadUserAvatar,
13
+ viewUser,
14
+ viewUserEnrollments,
15
+ viewUserOrders,
16
+ viewUserPrivateInfos,
17
+ viewUserProductReviews,
18
+ viewUserPublicInfos,
19
+ viewUserQuotations,
20
+ viewUserRoles,
21
+ viewUserTokens,
22
+ ];
5
23
  export const User = {
6
24
  _id: checkTypeResolver(viewUserPublicInfos, '_id'),
7
25
  created: checkTypeResolver(viewUserPrivateInfos, 'created'),
@@ -12,7 +30,7 @@ export const User = {
12
30
  lastContact: checkTypeResolver(viewUserPrivateInfos, 'lastContact'),
13
31
  lastLogin: checkTypeResolver(viewUserPrivateInfos, 'lastLogin'),
14
32
  profile: checkTypeResolver(viewUserPrivateInfos, 'profile'),
15
- roles: checkTypeResolver(viewUserPrivateInfos, 'roles'),
33
+ roles: checkTypeResolver(viewUserRoles, 'roles'),
16
34
  tags: checkTypeResolver(viewUserPrivateInfos, 'tags'),
17
35
  username: checkTypeResolver(viewUserPrivateInfos, 'username'),
18
36
  name: async (user, params, context) => {
@@ -58,7 +76,7 @@ export const User = {
58
76
  });
59
77
  },
60
78
  async tokens(user, params, context) {
61
- await checkAction(context, viewUserPrivateInfos, [user, params]);
79
+ await checkAction(context, viewUserTokens, [user, params]);
62
80
  const walletAddresses = user.services?.web3?.flatMap((service) => {
63
81
  return service.verified ? [service.address] : [];
64
82
  }) || [];
@@ -89,10 +107,23 @@ export const User = {
89
107
  return context.loaders.languageLoader.load({ isoCode: userLocale.language });
90
108
  },
91
109
  allowedActions: async (user, params, context) => {
92
- await checkAction(context, viewUserPrivateInfos, [user, params]);
110
+ await checkAction(context, viewUserRoles, [user, params]);
93
111
  const userRoles = context.roles.getUserRoles(user?._id, user.roles, true);
94
112
  return permissions(userRoles, context.roles.roles);
95
113
  },
114
+ viewerAllowedActions: async (user, params, context) => {
115
+ const targetArgs = [user, { userId: user._id }];
116
+ await checkAction(context, viewUser, targetArgs);
117
+ const allowedActions = await Promise.all(USER_TARGET_ACTIONS.map(async (action) => {
118
+ try {
119
+ return (await context.roles.userHasPermission(context, action, targetArgs)) ? action : null;
120
+ }
121
+ catch {
122
+ return null;
123
+ }
124
+ }));
125
+ return allowedActions.filter((action) => action !== null).toSorted();
126
+ },
96
127
  orders: async (user, params, context) => {
97
128
  await checkAction(context, viewUserOrders, [user, params]);
98
129
  return context.modules.orders.findOrders({
package/lib/roles/all.js CHANGED
@@ -101,6 +101,18 @@ export const all = (role, actions) => {
101
101
  role.allow(actions.registerPaymentCredentials, () => false);
102
102
  role.allow(actions.managePaymentCredentials, () => false);
103
103
  role.allow(actions.bulkImport, () => false);
104
+ role.allow(actions.bulkSetProductStatus, () => false);
105
+ role.allow(actions.bulkUpdateProductTags, () => false);
106
+ role.allow(actions.bulkAssignProductsToAssortment, () => false);
107
+ role.allow(actions.bulkRemoveProducts, () => false);
108
+ role.allow(actions.bulkUpdateUserTags, () => false);
109
+ role.allow(actions.bulkRemoveUsers, () => false);
110
+ role.allow(actions.bulkSetUserRoles, () => false);
111
+ role.allow(actions.bulkRemoveAssortments, () => false);
112
+ role.allow(actions.bulkUpdateAssortmentTags, () => false);
113
+ role.allow(actions.bulkSetAssortmentActive, () => false);
114
+ role.allow(actions.bulkRemoveFilters, () => false);
115
+ role.allow(actions.bulkSetFilterActive, () => false);
104
116
  role.allow(actions.viewOrder, () => false);
105
117
  role.allow(actions.viewQuotation, () => false);
106
118
  role.allow(actions.viewEnrollment, () => false);
@@ -96,6 +96,18 @@ const actions = [
96
96
  'registerPaymentCredentials',
97
97
  'managePaymentCredentials',
98
98
  'bulkImport',
99
+ 'bulkSetProductStatus',
100
+ 'bulkUpdateProductTags',
101
+ 'bulkAssignProductsToAssortment',
102
+ 'bulkRemoveProducts',
103
+ 'bulkUpdateUserTags',
104
+ 'bulkRemoveUsers',
105
+ 'bulkSetUserRoles',
106
+ 'bulkRemoveAssortments',
107
+ 'bulkUpdateAssortmentTags',
108
+ 'bulkSetAssortmentActive',
109
+ 'bulkRemoveFilters',
110
+ 'bulkSetFilterActive',
99
111
  'logout',
100
112
  'logoutAllSessions',
101
113
  'loginAsGuest',
@@ -184,5 +184,5 @@ export const loggedIn = (role, actions) => {
184
184
  role.allow(actions.uploadUserAvatar, canUpdateAvatar);
185
185
  role.allow(actions.uploadTempFile, canUpdateAvatar);
186
186
  role.allow(actions.changePassword, () => true);
187
- role.allow(actions.logoutAllSessions, () => true);
187
+ role.allow(actions.logoutAllSessions, isMyself);
188
188
  };
@@ -262,5 +262,10 @@ export default [
262
262
  start: DateTime
263
263
  end: DateTime
264
264
  }
265
+
266
+ input GlobalSearchTypeLimitInput {
267
+ type: SearchableEntity!
268
+ limit: Int!
269
+ }
265
270
  `,
266
271
  ];
@@ -60,8 +60,9 @@ export default [
60
60
  """
61
61
  Log the user out of all sessions by invalidating all JWT tokens.
62
62
  This increments the token version, making all existing tokens invalid.
63
+ Pass a userId to force-logout another user (requires updateUser permission).
63
64
  """
64
- logoutAllSessions: SuccessResponse
65
+ logoutAllSessions(userId: ID): SuccessResponse
65
66
 
66
67
  """
67
68
  Impersonate a user
@@ -858,6 +859,78 @@ export default [
858
859
  invalidateToken(tokenId: ID!): Token!
859
860
  exportToken(tokenId: ID!, quantity: Int! = 1, recipientWalletAddress: String!): Token!
860
861
 
862
+ """
863
+ Publish or unpublish multiple products at once
864
+ """
865
+ bulkSetProductStatus(productIds: [ID!]!, status: ProductStatus!): BulkOperationResult!
866
+
867
+ """
868
+ Add or remove tags from multiple products at once
869
+ """
870
+ bulkUpdateProductTags(
871
+ productIds: [ID!]!
872
+ add: [LowerCaseString!]
873
+ remove: [LowerCaseString!]
874
+ ): BulkOperationResult!
875
+
876
+ """
877
+ Assign multiple products to an assortment
878
+ """
879
+ bulkAssignProductsToAssortment(productIds: [ID!]!, assortmentId: ID!): BulkOperationResult!
880
+
881
+ """
882
+ Remove multiple products
883
+ """
884
+ bulkRemoveProducts(productIds: [ID!]!): BulkOperationResult!
885
+
886
+ """
887
+ Add or remove tags from multiple users at once
888
+ """
889
+ bulkUpdateUserTags(
890
+ userIds: [ID!]!
891
+ add: [LowerCaseString!]
892
+ remove: [LowerCaseString!]
893
+ ): BulkOperationResult!
894
+
895
+ """
896
+ Remove multiple users
897
+ """
898
+ bulkRemoveUsers(userIds: [ID!]!): BulkOperationResult!
899
+
900
+ """
901
+ Set roles for multiple users at once
902
+ """
903
+ bulkSetUserRoles(userIds: [ID!]!, roles: [String!]!): BulkOperationResult!
904
+
905
+ """
906
+ Remove multiple assortments
907
+ """
908
+ bulkRemoveAssortments(assortmentIds: [ID!]!): BulkOperationResult!
909
+
910
+ """
911
+ Add or remove tags from multiple assortments at once
912
+ """
913
+ bulkUpdateAssortmentTags(
914
+ assortmentIds: [ID!]!
915
+ add: [LowerCaseString!]
916
+ remove: [LowerCaseString!]
917
+ ): BulkOperationResult!
918
+
919
+ """
920
+ Activate or deactivate multiple assortments at once
921
+ """
922
+ bulkSetAssortmentActive(assortmentIds: [ID!]!, isActive: Boolean!): BulkOperationResult!
923
+
924
+ """
925
+ Remove multiple filters
926
+ """
927
+ bulkRemoveFilters(filterIds: [ID!]!): BulkOperationResult!
928
+
929
+ """
930
+ Activate or deactivate multiple filters at once
931
+ """
932
+ bulkSetFilterActive(filterIds: [ID!]!, isActive: Boolean!): BulkOperationResult!
933
+
861
934
  """
862
935
  Store user W3C Push subscription object
863
936
  """
@@ -415,6 +415,21 @@ export default [
415
415
  """
416
416
  enrollment(enrollmentId: ID!): Enrollment
417
417
 
418
+ """
419
+ Search across multiple entity types in a single request
420
+ """
421
+ globalSearch(
422
+ query: String!
423
+ types: [SearchableEntity!]
424
+ limit: Int = 5
425
+ typeLimits: [GlobalSearchTypeLimitInput!]
426
+ includeDraftProducts: Boolean = true
427
+ includeInactiveAssortments: Boolean = true
428
+ includeInactiveFilters: Boolean = true
429
+ includeGuestUsers: Boolean = false
430
+ includeCarts: Boolean = false
431
+ ): GlobalSearchResponse! @cacheControl(scope: PRIVATE, maxAge: 0)
432
+
418
433
  """
419
434
  Search products
420
435
  """
@@ -4,6 +4,13 @@ export default [
4
4
  success: Boolean
5
5
  }
6
6
 
7
+ type BulkOperationResult @cacheControl(maxAge: 0, scope: PRIVATE) {
8
+ successIds: [ID!]!
9
+ successCount: Int!
10
+ failedCount: Int!
11
+ failedIds: [ID!]!
12
+ }
13
+
7
14
  enum SortDirection {
8
15
  ASC
9
16
  DESC
@@ -28,5 +28,41 @@ export default [
28
28
  assortmentsCount: Int!
29
29
  assortments(limit: Int = 10, offset: Int = 0): [Assortment!]!
30
30
  }
31
+
32
+ enum SearchableEntity {
33
+ PRODUCT
34
+ USER
35
+ ORDER
36
+ ASSORTMENT
37
+ FILTER
38
+ ENROLLMENT
39
+ QUOTATION
40
+ WORK
41
+ }
42
+
43
+ union GlobalSearchResult =
44
+ | SimpleProduct
45
+ | ConfigurableProduct
46
+ | BundleProduct
47
+ | PlanProduct
48
+ | TokenizedProduct
49
+ | User
50
+ | Order
51
+ | Assortment
52
+ | Filter
53
+ | Enrollment
54
+ | Quotation
55
+ | Work
56
+
57
+ type GlobalSearchTypeCount {
58
+ type: SearchableEntity!
59
+ totalCount: Int!
60
+ authorized: Boolean!
61
+ }
62
+
63
+ type GlobalSearchResponse {
64
+ results: [GlobalSearchResult!]!
65
+ counts: [GlobalSearchTypeCount!]!
66
+ }
31
67
  `,
32
68
  ];
@@ -134,6 +134,10 @@ export default [
134
134
  sort: [SortOptionInput!]
135
135
  ): [Enrollment!]!
136
136
  allowedActions: [RoleAction!]!
137
+ """
138
+ Built-in user-target actions the current viewer can perform on this user.
139
+ """
140
+ viewerAllowedActions: [RoleAction!]! @cacheControl(maxAge: 0, scope: PRIVATE)
137
141
  tokens: [Token!]!
138
142
  reviews(limit: Int = 10, offset: Int = 0, sort: [SortOptionInput!]): [ProductReview!]!
139
143
  reviewsCount: Int!
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "@unchainedshop/api",
3
3
  "description": "GraphQL API layer for the Unchained Engine with Express/Fastify adapters and MCP server",
4
4
  "mcpName": "io.github.unchainedshop/unchained",
5
- "version": "5.0.0-alpha.4",
5
+ "version": "5.0.0-alpha.5",
6
6
  "main": "lib/api-index.js",
7
7
  "types": "lib/api-index.d.ts",
8
8
  "type": "module",
@@ -66,7 +66,7 @@
66
66
  "@fastify/multipart": ">= 10 < 11",
67
67
  "@fastify/static": ">= 10.1.3 < 11",
68
68
  "@modelcontextprotocol/server": ">= 2 < 3",
69
- "@unchainedshop/admin-ui": "^5.0.0-alpha.1",
69
+ "@unchainedshop/admin-ui": "^5.0.0-alpha.5",
70
70
  "ai": ">= 7 < 8",
71
71
  "cookie-parser": ">= 1.4 < 2",
72
72
  "express": ">= 5 < 6",
@@ -118,29 +118,29 @@
118
118
  },
119
119
  "dependencies": {
120
120
  "@envelop/core": "^5.6.0",
121
- "@unchainedshop/core": "^5.0.0-alpha.1",
122
- "@unchainedshop/core-assortments": "^5.0.0-alpha.1",
123
- "@unchainedshop/core-bookmarks": "^5.0.0-alpha.1",
124
- "@unchainedshop/core-countries": "^5.0.0-alpha.1",
125
- "@unchainedshop/core-currencies": "^5.0.0-alpha.1",
126
- "@unchainedshop/core-delivery": "^5.0.0-alpha.1",
127
- "@unchainedshop/core-enrollments": "^5.0.0-alpha.1",
128
- "@unchainedshop/core-events": "^5.0.0-alpha.1",
129
- "@unchainedshop/core-files": "^5.0.0-alpha.1",
130
- "@unchainedshop/core-filters": "^5.0.0-alpha.1",
131
- "@unchainedshop/core-languages": "^5.0.0-alpha.1",
132
- "@unchainedshop/core-orders": "^5.0.0-alpha.1",
133
- "@unchainedshop/core-payment": "^5.0.0-alpha.1",
134
- "@unchainedshop/core-products": "^5.0.0-alpha.1",
135
- "@unchainedshop/core-quotations": "^5.0.0-alpha.1",
136
- "@unchainedshop/core-users": "^5.0.0-alpha.1",
137
- "@unchainedshop/core-warehousing": "^5.0.0-alpha.1",
138
- "@unchainedshop/core-worker": "^5.0.0-alpha.1",
139
- "@unchainedshop/events": "^5.0.0-alpha.1",
140
- "@unchainedshop/logger": "^5.0.0-alpha.1",
141
- "@unchainedshop/mongodb": "^5.0.0-alpha.1",
142
- "@unchainedshop/roles": "^5.0.0-alpha.1",
143
- "@unchainedshop/utils": "^5.0.0-alpha.1",
121
+ "@unchainedshop/core": "^5.0.0-alpha.5",
122
+ "@unchainedshop/core-assortments": "^5.0.0-alpha.5",
123
+ "@unchainedshop/core-bookmarks": "^5.0.0-alpha.5",
124
+ "@unchainedshop/core-countries": "^5.0.0-alpha.5",
125
+ "@unchainedshop/core-currencies": "^5.0.0-alpha.5",
126
+ "@unchainedshop/core-delivery": "^5.0.0-alpha.5",
127
+ "@unchainedshop/core-enrollments": "^5.0.0-alpha.5",
128
+ "@unchainedshop/core-events": "^5.0.0-alpha.5",
129
+ "@unchainedshop/core-files": "^5.0.0-alpha.5",
130
+ "@unchainedshop/core-filters": "^5.0.0-alpha.5",
131
+ "@unchainedshop/core-languages": "^5.0.0-alpha.5",
132
+ "@unchainedshop/core-orders": "^5.0.0-alpha.5",
133
+ "@unchainedshop/core-payment": "^5.0.0-alpha.5",
134
+ "@unchainedshop/core-products": "^5.0.0-alpha.5",
135
+ "@unchainedshop/core-quotations": "^5.0.0-alpha.5",
136
+ "@unchainedshop/core-users": "^5.0.0-alpha.5",
137
+ "@unchainedshop/core-warehousing": "^5.0.0-alpha.5",
138
+ "@unchainedshop/core-worker": "^5.0.0-alpha.5",
139
+ "@unchainedshop/events": "^5.0.0-alpha.5",
140
+ "@unchainedshop/logger": "^5.0.0-alpha.5",
141
+ "@unchainedshop/mongodb": "^5.0.0-alpha.5",
142
+ "@unchainedshop/roles": "^5.0.0-alpha.5",
143
+ "@unchainedshop/utils": "^5.0.0-alpha.5",
144
144
  "@whatwg-node/server": "^0.11.0",
145
145
  "dataloader": "^2.2.3",
146
146
  "graphql-scalars": "^2.0.0",