@data-client/rest 0.15.6 → 0.15.7

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.
@@ -818,6 +818,542 @@ describe('resource()', () => {
818
818
  );
819
819
  });
820
820
 
821
+ it('UserResource.getList.move should work', async () => {
822
+ mynock.patch(`/groups/five/users/2`).reply(200, (uri, body: any) => ({
823
+ id: 2,
824
+ username: 'user2',
825
+ email: 'user2@example.com',
826
+ group: 'ten',
827
+ ...body,
828
+ }));
829
+
830
+ const { result, controller } = renderDataClient(
831
+ () => {
832
+ return {
833
+ user2: useQuery(User, { id: 2 }),
834
+ groupFive: useCache(UserResource.getList, { group: 'five' }),
835
+ groupTen: useCache(UserResource.getList, { group: 'ten' }),
836
+ };
837
+ },
838
+ {
839
+ initialFixtures: [
840
+ {
841
+ endpoint: UserResource.getList,
842
+ args: [{ group: 'five' }],
843
+ response: [
844
+ {
845
+ id: 1,
846
+ username: 'user1',
847
+ email: 'user1@example.com',
848
+ group: 'five',
849
+ },
850
+ {
851
+ id: 2,
852
+ username: 'user2',
853
+ email: 'user2@example.com',
854
+ group: 'five',
855
+ },
856
+ ],
857
+ },
858
+ {
859
+ endpoint: UserResource.getList,
860
+ args: [{ group: 'ten' }],
861
+ response: [
862
+ {
863
+ id: 3,
864
+ username: 'user3',
865
+ email: 'user3@example.com',
866
+ group: 'ten',
867
+ },
868
+ ],
869
+ },
870
+ ],
871
+ },
872
+ );
873
+
874
+ expect(result.current.groupFive).toHaveLength(2);
875
+ expect(result.current.groupTen).toHaveLength(1);
876
+
877
+ // PATCH /groups/five/users/2 - moves user 2 from 'five' to 'ten'
878
+ await act(async () => {
879
+ const response = await controller.fetch(
880
+ UserResource.getList.move,
881
+ { group: 'five', id: '2' },
882
+ {
883
+ id: 2,
884
+ group: 'ten',
885
+ },
886
+ );
887
+ expect(response.id).toEqual(2);
888
+ });
889
+
890
+ // user should be removed from group 'five'
891
+ expect(result.current.groupFive).toHaveLength(1);
892
+ expect(result.current.groupFive?.[0]?.username).toBe('user1');
893
+
894
+ // user should be added to group 'ten'
895
+ expect(result.current.groupTen).toHaveLength(2);
896
+ expect(result.current.groupTen?.map((u: any) => u.username)).toEqual(
897
+ expect.arrayContaining(['user3', 'user2']),
898
+ );
899
+
900
+ // entity should be updated
901
+ expect(result.current.user2?.group).toEqual('ten');
902
+
903
+ // move uses full path params (group + id), no searchParams
904
+ () =>
905
+ controller.fetch(
906
+ UserResource.getList.move,
907
+ { group: 'five', id: 2 },
908
+ { id: 1, group: 'ten' },
909
+ );
910
+ () =>
911
+ controller.fetch(
912
+ UserResource.getList.move,
913
+ // @ts-expect-error - missing required group
914
+ { id: 2 },
915
+ { id: 1 },
916
+ );
917
+ // @ts-expect-error
918
+ () => controller.fetch(UserResource.getList.move, { username: 'never' });
919
+ // @ts-expect-error
920
+ () => controller.fetch(UserResource.getList.move, 1, 'hi');
921
+ () =>
922
+ controller.fetch(
923
+ UserResource.getList.move,
924
+ { group: 'five', id: 2 },
925
+ // @ts-expect-error
926
+ { sdf: 'never' },
927
+ );
928
+ () =>
929
+ controller.fetch(
930
+ UserResource.getList.move,
931
+ // @ts-expect-error
932
+ { sdf: 'five', id: 2 },
933
+ { id: 1 },
934
+ );
935
+ });
936
+
937
+ it('getList.move should work when entity lacks path param field', async () => {
938
+ // Entity has 'status' but NOT 'team' - team is only a URL path param
939
+ // Collection keys include both team (from path) and status (from searchParams)
940
+ class Task extends Entity {
941
+ readonly id: number | undefined = undefined;
942
+ readonly title: string = '';
943
+ readonly status: string = 'backlog';
944
+
945
+ pk() {
946
+ return this.id?.toString();
947
+ }
948
+ }
949
+
950
+ const TeamTaskResource = resource({
951
+ path: 'http\\://test.com/teams/:team/tasks/:id',
952
+ searchParams: {} as { status: string },
953
+ schema: Task,
954
+ });
955
+
956
+ mynock.patch(`/teams/alpha/tasks/3`).reply(200, (uri, body: any) => ({
957
+ id: 3,
958
+ title: 'My Task',
959
+ status: 'in-progress',
960
+ ...body,
961
+ }));
962
+
963
+ const { result, controller } = renderDataClient(
964
+ () => {
965
+ return {
966
+ task3: useQuery(Task, { id: 3 }),
967
+ backlog: useCache(TeamTaskResource.getList, {
968
+ team: 'alpha',
969
+ status: 'backlog',
970
+ }),
971
+ inProgress: useCache(TeamTaskResource.getList, {
972
+ team: 'alpha',
973
+ status: 'in-progress',
974
+ }),
975
+ };
976
+ },
977
+ {
978
+ initialFixtures: [
979
+ {
980
+ endpoint: TeamTaskResource.getList,
981
+ args: [{ team: 'alpha', status: 'backlog' }],
982
+ response: [
983
+ { id: 1, title: 'Task 1', status: 'backlog' },
984
+ { id: 3, title: 'My Task', status: 'backlog' },
985
+ ],
986
+ },
987
+ {
988
+ endpoint: TeamTaskResource.getList,
989
+ args: [{ team: 'alpha', status: 'in-progress' }],
990
+ response: [{ id: 2, title: 'Task 2', status: 'in-progress' }],
991
+ },
992
+ ],
993
+ },
994
+ );
995
+
996
+ expect(result.current.backlog).toHaveLength(2);
997
+ expect(result.current.inProgress).toHaveLength(1);
998
+
999
+ // PATCH /teams/alpha/tasks/3 - move task 3 from backlog to in-progress
1000
+ // 'team' is only in URL params (not on entity); 'status' is in body and on entity
1001
+ await act(async () => {
1002
+ const response = await controller.fetch(
1003
+ TeamTaskResource.getList.move,
1004
+ { team: 'alpha', id: '3' },
1005
+ { id: 3, status: 'in-progress' },
1006
+ );
1007
+ expect(response.id).toEqual(3);
1008
+ });
1009
+
1010
+ // task should be removed from backlog
1011
+ expect(result.current.backlog).toHaveLength(1);
1012
+ expect(result.current.backlog?.[0]?.title).toBe('Task 1');
1013
+
1014
+ // task should be added to in-progress
1015
+ expect(result.current.inProgress).toHaveLength(2);
1016
+ expect(result.current.inProgress?.map((t: any) => t.title)).toEqual(
1017
+ expect.arrayContaining(['Task 2', 'My Task']),
1018
+ );
1019
+
1020
+ // entity should be updated
1021
+ expect(result.current.task3?.status).toEqual('in-progress');
1022
+ });
1023
+
1024
+ it('getList.move should work with searchParams-based collections', async () => {
1025
+ class Task extends Entity {
1026
+ readonly id: number | undefined = undefined;
1027
+ readonly title: string = '';
1028
+ readonly status: string = 'backlog';
1029
+
1030
+ pk() {
1031
+ return this.id?.toString();
1032
+ }
1033
+ }
1034
+
1035
+ const TaskResource = resource({
1036
+ path: 'http\\://test.com/tasks/:id',
1037
+ searchParams: {} as { status: string },
1038
+ schema: Task,
1039
+ });
1040
+
1041
+ mynock.patch(`/tasks/3`).reply(200, (uri, body: any) => ({
1042
+ id: 3,
1043
+ title: 'My Task',
1044
+ status: 'in-progress',
1045
+ ...body,
1046
+ }));
1047
+
1048
+ const { result, controller } = renderDataClient(
1049
+ () => {
1050
+ return {
1051
+ task3: useQuery(Task, { id: 3 }),
1052
+ backlog: useCache(TaskResource.getList, { status: 'backlog' }),
1053
+ inProgress: useCache(TaskResource.getList, {
1054
+ status: 'in-progress',
1055
+ }),
1056
+ };
1057
+ },
1058
+ {
1059
+ initialFixtures: [
1060
+ {
1061
+ endpoint: TaskResource.getList,
1062
+ args: [{ status: 'backlog' }],
1063
+ response: [
1064
+ { id: 1, title: 'Task 1', status: 'backlog' },
1065
+ { id: 3, title: 'My Task', status: 'backlog' },
1066
+ ],
1067
+ },
1068
+ {
1069
+ endpoint: TaskResource.getList,
1070
+ args: [{ status: 'in-progress' }],
1071
+ response: [{ id: 2, title: 'Task 2', status: 'in-progress' }],
1072
+ },
1073
+ ],
1074
+ },
1075
+ );
1076
+
1077
+ expect(result.current.backlog).toHaveLength(2);
1078
+ expect(result.current.inProgress).toHaveLength(1);
1079
+
1080
+ // PATCH /tasks/3 - moves task 3 from 'backlog' to 'in-progress'
1081
+ await act(async () => {
1082
+ const response = await controller.fetch(
1083
+ TaskResource.getList.move,
1084
+ { id: '3' },
1085
+ { id: 3, status: 'in-progress' },
1086
+ );
1087
+ expect(response.id).toEqual(3);
1088
+ });
1089
+
1090
+ // task should be removed from backlog
1091
+ expect(result.current.backlog).toHaveLength(1);
1092
+ expect(result.current.backlog?.[0]?.title).toBe('Task 1');
1093
+
1094
+ // task should be added to in-progress
1095
+ expect(result.current.inProgress).toHaveLength(2);
1096
+ expect(result.current.inProgress?.map((t: any) => t.title)).toEqual(
1097
+ expect.arrayContaining(['Task 2', 'My Task']),
1098
+ );
1099
+
1100
+ // entity should be updated
1101
+ expect(result.current.task3?.status).toEqual('in-progress');
1102
+ });
1103
+
1104
+ it('getList.move should work with FormData body', async () => {
1105
+ class Task extends Entity {
1106
+ readonly id: number | undefined = undefined;
1107
+ readonly title: string = '';
1108
+ readonly status: string = 'backlog';
1109
+
1110
+ pk() {
1111
+ return this.id?.toString();
1112
+ }
1113
+ }
1114
+
1115
+ const TaskResource = resource({
1116
+ path: 'http\\://test.com/tasks/:id',
1117
+ searchParams: {} as { status: string },
1118
+ schema: Task,
1119
+ });
1120
+
1121
+ mynock.patch(`/tasks/3`).reply(200, (uri, body: any) => ({
1122
+ id: 3,
1123
+ title: 'My Task',
1124
+ status: 'in-progress',
1125
+ ...(body instanceof FormData ? Object.fromEntries(body.entries()) : body),
1126
+ }));
1127
+
1128
+ const { result, controller } = renderDataClient(
1129
+ () => {
1130
+ return {
1131
+ task3: useQuery(Task, { id: 3 }),
1132
+ backlog: useCache(TaskResource.getList, { status: 'backlog' }),
1133
+ inProgress: useCache(TaskResource.getList, {
1134
+ status: 'in-progress',
1135
+ }),
1136
+ };
1137
+ },
1138
+ {
1139
+ initialFixtures: [
1140
+ {
1141
+ endpoint: TaskResource.getList,
1142
+ args: [{ status: 'backlog' }],
1143
+ response: [
1144
+ { id: 1, title: 'Task 1', status: 'backlog' },
1145
+ { id: 3, title: 'My Task', status: 'backlog' },
1146
+ ],
1147
+ },
1148
+ {
1149
+ endpoint: TaskResource.getList,
1150
+ args: [{ status: 'in-progress' }],
1151
+ response: [{ id: 2, title: 'Task 2', status: 'in-progress' }],
1152
+ },
1153
+ ],
1154
+ },
1155
+ );
1156
+
1157
+ expect(result.current.backlog).toHaveLength(2);
1158
+ expect(result.current.inProgress).toHaveLength(1);
1159
+
1160
+ // Use FormData as the body (simulates multipart form submission)
1161
+ const formData = new FormData();
1162
+ formData.append('id', '3');
1163
+ formData.append('status', 'in-progress');
1164
+
1165
+ await act(async () => {
1166
+ const response = await controller.fetch(
1167
+ TaskResource.getList.move,
1168
+ { id: '3' },
1169
+ formData,
1170
+ );
1171
+ expect(response.id).toEqual(3);
1172
+ });
1173
+
1174
+ // task should be removed from backlog
1175
+ expect(result.current.backlog).toHaveLength(1);
1176
+ expect(result.current.backlog?.[0]?.title).toBe('Task 1');
1177
+
1178
+ // task should be added to in-progress
1179
+ expect(result.current.inProgress).toHaveLength(2);
1180
+ expect(result.current.inProgress?.map((t: any) => t.title)).toEqual(
1181
+ expect.arrayContaining(['Task 2', 'My Task']),
1182
+ );
1183
+
1184
+ // entity should be updated
1185
+ expect(result.current.task3?.status).toEqual('in-progress');
1186
+ });
1187
+
1188
+ it('getList.move should work optimistically with path-based collections', async () => {
1189
+ const OptUserResource = resource({
1190
+ path: 'http\\://test.com/groups/:group/users/:id',
1191
+ schema: User,
1192
+ optimistic: true,
1193
+ });
1194
+
1195
+ mynock.patch(`/groups/five/users/2`).reply(200, () => ({
1196
+ id: 2,
1197
+ username: 'user2',
1198
+ email: 'user2@example.com',
1199
+ group: 'ten',
1200
+ }));
1201
+
1202
+ const { result, controller } = renderDataClient(
1203
+ () => {
1204
+ return {
1205
+ user2: useQuery(User, { id: 2 }),
1206
+ groupFive: useCache(OptUserResource.getList, { group: 'five' }),
1207
+ groupTen: useCache(OptUserResource.getList, { group: 'ten' }),
1208
+ };
1209
+ },
1210
+ {
1211
+ initialFixtures: [
1212
+ {
1213
+ endpoint: OptUserResource.getList,
1214
+ args: [{ group: 'five' }],
1215
+ response: [
1216
+ {
1217
+ id: 1,
1218
+ username: 'user1',
1219
+ email: 'user1@example.com',
1220
+ group: 'five',
1221
+ },
1222
+ {
1223
+ id: 2,
1224
+ username: 'user2',
1225
+ email: 'user2@example.com',
1226
+ group: 'five',
1227
+ },
1228
+ ],
1229
+ },
1230
+ {
1231
+ endpoint: OptUserResource.getList,
1232
+ args: [{ group: 'ten' }],
1233
+ response: [
1234
+ {
1235
+ id: 3,
1236
+ username: 'user3',
1237
+ email: 'user3@example.com',
1238
+ group: 'ten',
1239
+ },
1240
+ ],
1241
+ },
1242
+ ],
1243
+ },
1244
+ );
1245
+
1246
+ expect(OptUserResource.getList.move.getOptimisticResponse).toBeDefined();
1247
+ expect(result.current.groupFive).toHaveLength(2);
1248
+ expect(result.current.groupTen).toHaveLength(1);
1249
+
1250
+ let promise: any;
1251
+ act(() => {
1252
+ promise = controller.fetch(
1253
+ OptUserResource.getList.move,
1254
+ { group: 'five', id: '2' },
1255
+ { id: 2, group: 'ten' },
1256
+ );
1257
+ });
1258
+
1259
+ // optimistic: should update immediately before network responds
1260
+ expect(result.current.groupFive).toHaveLength(1);
1261
+ expect(result.current.groupFive?.[0]?.username).toBe('user1');
1262
+ expect(result.current.groupTen).toHaveLength(2);
1263
+ expect(result.current.groupTen?.map((u: any) => u.username)).toEqual(
1264
+ expect.arrayContaining(['user3', 'user2']),
1265
+ );
1266
+ expect(result.current.user2?.group).toEqual('ten');
1267
+
1268
+ // after server response, should still be correct
1269
+ await act(() => promise);
1270
+ expect(result.current.groupFive).toHaveLength(1);
1271
+ expect(result.current.groupTen).toHaveLength(2);
1272
+ expect(result.current.user2?.group).toEqual('ten');
1273
+ });
1274
+
1275
+ it('getList.move should work optimistically with searchParams-based collections', async () => {
1276
+ class Task extends Entity {
1277
+ readonly id: number | undefined = undefined;
1278
+ readonly title: string = '';
1279
+ readonly status: string = 'backlog';
1280
+
1281
+ pk() {
1282
+ return this.id?.toString();
1283
+ }
1284
+ }
1285
+
1286
+ const TaskResource = resource({
1287
+ path: 'http\\://test.com/tasks/:id',
1288
+ searchParams: {} as { status: string },
1289
+ schema: Task,
1290
+ optimistic: true,
1291
+ });
1292
+
1293
+ mynock.patch(`/tasks/3`).reply(200, () => ({
1294
+ id: 3,
1295
+ title: 'My Task',
1296
+ status: 'in-progress',
1297
+ }));
1298
+
1299
+ const { result, controller } = renderDataClient(
1300
+ () => {
1301
+ return {
1302
+ task3: useQuery(Task, { id: 3 }),
1303
+ backlog: useCache(TaskResource.getList, { status: 'backlog' }),
1304
+ inProgress: useCache(TaskResource.getList, {
1305
+ status: 'in-progress',
1306
+ }),
1307
+ };
1308
+ },
1309
+ {
1310
+ initialFixtures: [
1311
+ {
1312
+ endpoint: TaskResource.getList,
1313
+ args: [{ status: 'backlog' }],
1314
+ response: [
1315
+ { id: 1, title: 'Task 1', status: 'backlog' },
1316
+ { id: 3, title: 'My Task', status: 'backlog' },
1317
+ ],
1318
+ },
1319
+ {
1320
+ endpoint: TaskResource.getList,
1321
+ args: [{ status: 'in-progress' }],
1322
+ response: [{ id: 2, title: 'Task 2', status: 'in-progress' }],
1323
+ },
1324
+ ],
1325
+ },
1326
+ );
1327
+
1328
+ expect(TaskResource.getList.move.getOptimisticResponse).toBeDefined();
1329
+ expect(result.current.backlog).toHaveLength(2);
1330
+ expect(result.current.inProgress).toHaveLength(1);
1331
+
1332
+ let promise: any;
1333
+ act(() => {
1334
+ promise = controller.fetch(
1335
+ TaskResource.getList.move,
1336
+ { id: '3' },
1337
+ { id: 3, status: 'in-progress' },
1338
+ );
1339
+ });
1340
+
1341
+ // optimistic: should update immediately before network responds
1342
+ expect(result.current.backlog).toHaveLength(1);
1343
+ expect(result.current.backlog?.[0]?.title).toBe('Task 1');
1344
+ expect(result.current.inProgress).toHaveLength(2);
1345
+ expect(result.current.inProgress?.map((t: any) => t.title)).toEqual(
1346
+ expect.arrayContaining(['Task 2', 'My Task']),
1347
+ );
1348
+ expect(result.current.task3?.status).toEqual('in-progress');
1349
+
1350
+ // after server response, should still be correct
1351
+ await act(() => promise);
1352
+ expect(result.current.backlog).toHaveLength(1);
1353
+ expect(result.current.inProgress).toHaveLength(2);
1354
+ expect(result.current.task3?.status).toEqual('in-progress');
1355
+ });
1356
+
821
1357
  it.each([
822
1358
  {
823
1359
  response: {
package/src/resource.ts CHANGED
@@ -96,6 +96,7 @@ This warning will not show in production.`,
96
96
  const getList = new Endpoint({
97
97
  ...extraMutateOptions,
98
98
  paginationField: paginationField as string,
99
+ movePath: path,
99
100
  path: shortenedPath,
100
101
  schema: new Collection([schema as any]),
101
102
  name: getName('getList'),
@@ -112,6 +113,7 @@ This warning will not show in production.`,
112
113
  method: 'PUT',
113
114
  name: getName('update'),
114
115
  }),
116
+ // TODO(breaking): Move to getList.move
115
117
  partialUpdate: new Endpoint({
116
118
  ...extraPartialOptions,
117
119
  path,
@@ -92,6 +92,7 @@ export interface Resource<
92
92
  body: 'body' extends keyof O ? O['body']
93
93
  : Partial<Denormalize<O['schema']>>;
94
94
  searchParams: O['searchParams'];
95
+ movePath: O['path'];
95
96
  } & Pick<O, 'paginationField'>
96
97
  >
97
98
  : GetEndpoint<
@@ -107,6 +108,7 @@ export interface Resource<
107
108
  body: 'body' extends keyof O ? O['body']
108
109
  : Partial<Denormalize<O['schema']>>;
109
110
  searchParams: Record<string, number | string | boolean> | undefined;
111
+ movePath: O['path'];
110
112
  } & Pick<O, 'paginationField'>
111
113
  >;
112
114
  /** Create a new item (POST)
@@ -7,5 +7,17 @@ export default class NetworkError extends Error {
7
7
  response: Response;
8
8
  name: string;
9
9
  constructor(response: Response);
10
+ /** Serialize the error for logging and debugging.
11
+ *
12
+ * Error properties are non-enumerable by default, so `JSON.stringify()`
13
+ * on a plain Error produces `{}`. This ensures status, message, and the
14
+ * request URL are always included in serialized output.
15
+ */
16
+ toJSON(): {
17
+ name: string;
18
+ status: number;
19
+ message: string;
20
+ url: string;
21
+ };
10
22
  }
11
23
  //# sourceMappingURL=NetworkError.d.ts.map
@@ -117,6 +117,13 @@ export interface RestInstance<F extends FetchFunction = FetchFunction, S extends
117
117
  remove: RemoveEndpoint<F, ExtractCollection<S>['remove'], Exclude<O, 'body' | 'method'> & {
118
118
  body: OptionsToAdderBodyArgument<O> | OptionsToAdderBodyArgument<O>[] | FormData;
119
119
  }>;
120
+ /** Move item between collections (PATCH) - removes from old, adds to new
121
+ * @see https://dataclient.io/rest/api/RestEndpoint#move
122
+ */
123
+ move: MoveEndpoint<F, ExtractCollection<S>['move'], {
124
+ path: 'movePath' extends keyof O ? O['movePath'] & string : O['path'];
125
+ body: OptionsToAdderBodyArgument<O> | FormData;
126
+ }>;
120
127
  }
121
128
  export type RestEndpointExtendOptions<O extends PartialRestGenerics, E extends {
122
129
  body?: any;
@@ -244,6 +251,15 @@ export type RemoveEndpoint<F extends FetchFunction = FetchFunction, S extends Sc
244
251
  }> = RestInstanceBase<RestFetch<'searchParams' extends keyof O ? O['searchParams'] extends undefined ? PathArgs<Exclude<O['path'], undefined>> : O['searchParams'] & PathArgs<Exclude<O['path'], undefined>> : PathArgs<Exclude<O['path'], undefined>>, O['body'], ResolveType<F>>, S, true, Omit<O, 'method'> & {
245
252
  method: 'PATCH';
246
253
  }>;
254
+ export type MoveEndpoint<F extends FetchFunction = FetchFunction, S extends Schema | undefined = any, O extends {
255
+ path: string;
256
+ body: any;
257
+ } = {
258
+ path: string;
259
+ body: any;
260
+ }> = RestInstanceBase<RestFetch<PathArgs<Exclude<O['path'], undefined>>, O['body'], ResolveType<F>>, S, true, Omit<O, 'method' | 'searchParams'> & {
261
+ method: 'PATCH';
262
+ }>;
247
263
  type OptionsToAdderBodyArgument<O extends {
248
264
  body?: any;
249
265
  }> = 'body' extends keyof O ? O['body'] : any;
@@ -74,6 +74,7 @@ export interface Resource<O extends ResourceGenerics = {
74
74
  ], ParamToArgs<O['searchParams'] extends undefined ? KeysToArgs<ShortenPath<O['path']>> : O['searchParams'] & PathArgs<ShortenPath<O['path']>>>>;
75
75
  body: 'body' extends keyof O ? O['body'] : Partial<Denormalize<O['schema']>>;
76
76
  searchParams: O['searchParams'];
77
+ movePath: O['path'];
77
78
  } & Pick<O, 'paginationField'>> : GetEndpoint<{
78
79
  path: ShortenPath<O['path']>;
79
80
  schema: schema.Collection<[
@@ -81,6 +82,7 @@ export interface Resource<O extends ResourceGenerics = {
81
82
  ], ParamToArgs<(Record<string, number | string | boolean> | undefined) & PathArgs<ShortenPath<O['path']>>>>;
82
83
  body: 'body' extends keyof O ? O['body'] : Partial<Denormalize<O['schema']>>;
83
84
  searchParams: Record<string, number | string | boolean> | undefined;
85
+ movePath: O['path'];
84
86
  } & Pick<O, 'paginationField'>>;
85
87
  /** Create a new item (POST)
86
88
  *
@@ -7,5 +7,17 @@ export default class NetworkError extends Error {
7
7
  response: Response;
8
8
  name: string;
9
9
  constructor(response: Response);
10
+ /** Serialize the error for logging and debugging.
11
+ *
12
+ * Error properties are non-enumerable by default, so `JSON.stringify()`
13
+ * on a plain Error produces `{}`. This ensures status, message, and the
14
+ * request URL are always included in serialized output.
15
+ */
16
+ toJSON(): {
17
+ name: string;
18
+ status: number;
19
+ message: string;
20
+ url: string;
21
+ };
10
22
  }
11
23
  //# sourceMappingURL=NetworkError.d.ts.map
@@ -117,6 +117,13 @@ export interface RestInstance<F extends FetchFunction = FetchFunction, S extends
117
117
  remove: RemoveEndpoint<F, ExtractCollection<S>['remove'], Exclude<O, 'body' | 'method'> & {
118
118
  body: OptionsToAdderBodyArgument<O> | OptionsToAdderBodyArgument<O>[] | FormData;
119
119
  }>;
120
+ /** Move item between collections (PATCH) - removes from old, adds to new
121
+ * @see https://dataclient.io/rest/api/RestEndpoint#move
122
+ */
123
+ move: MoveEndpoint<F, ExtractCollection<S>['move'], {
124
+ path: 'movePath' extends keyof O ? O['movePath'] & string : O['path'];
125
+ body: OptionsToAdderBodyArgument<O> | FormData;
126
+ }>;
120
127
  }
121
128
  export type RestEndpointExtendOptions<O extends PartialRestGenerics, E extends {
122
129
  body?: any;
@@ -244,6 +251,15 @@ export type RemoveEndpoint<F extends FetchFunction = FetchFunction, S extends Sc
244
251
  }> = RestInstanceBase<RestFetch<'searchParams' extends keyof O ? O['searchParams'] extends undefined ? PathArgs<Exclude<O['path'], undefined>> : O['searchParams'] & PathArgs<Exclude<O['path'], undefined>> : PathArgs<Exclude<O['path'], undefined>>, O['body'], ResolveType<F>>, S, true, Omit<O, 'method'> & {
245
252
  method: 'PATCH';
246
253
  }>;
254
+ export type MoveEndpoint<F extends FetchFunction = FetchFunction, S extends Schema | undefined = any, O extends {
255
+ path: string;
256
+ body: any;
257
+ } = {
258
+ path: string;
259
+ body: any;
260
+ }> = RestInstanceBase<RestFetch<PathArgs<Exclude<O['path'], undefined>>, O['body'], ResolveType<F>>, S, true, Omit<O, 'method' | 'searchParams'> & {
261
+ method: 'PATCH';
262
+ }>;
247
263
  type OptionsToAdderBodyArgument<O extends {
248
264
  body?: any;
249
265
  }> = 'body' extends keyof O ? O['body'] : any;