@data-client/rest 0.15.6 → 0.16.0
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/CHANGELOG.md +118 -0
- package/dist/index.js +61 -18
- package/dist/index.umd.min.js +1 -1
- package/legacy/NetworkError.js +16 -1
- package/legacy/RestEndpoint.js +17 -4
- package/legacy/RestEndpointTypes.js +1 -1
- package/legacy/RestHelpers.js +29 -16
- package/legacy/pathTypes.js +1 -1
- package/legacy/resource.js +3 -1
- package/legacy/resourceTypes.js +1 -1
- package/lib/NetworkError.d.ts +12 -0
- package/lib/NetworkError.d.ts.map +1 -1
- package/lib/NetworkError.js +16 -1
- package/lib/RestEndpoint.js +17 -4
- package/lib/RestEndpointTypes.d.ts +43 -9
- package/lib/RestEndpointTypes.d.ts.map +1 -1
- package/lib/RestEndpointTypes.js +1 -1
- package/lib/RestHelpers.d.ts +2 -2
- package/lib/RestHelpers.d.ts.map +1 -1
- package/lib/RestHelpers.js +29 -16
- package/lib/pathTypes.d.ts +14 -13
- package/lib/pathTypes.d.ts.map +1 -1
- package/lib/pathTypes.js +1 -1
- package/lib/resource.d.ts.map +1 -1
- package/lib/resource.js +3 -1
- package/lib/resourceTypes.d.ts +5 -1
- package/lib/resourceTypes.d.ts.map +1 -1
- package/lib/resourceTypes.js +1 -1
- package/package.json +5 -5
- package/src/NetworkError.ts +15 -0
- package/src/RestEndpoint.js +21 -3
- package/src/RestEndpointTypes.ts +33 -9
- package/src/RestHelpers.ts +34 -26
- package/src/__tests__/Resource.test.ts +131 -0
- package/src/__tests__/RestEndpoint.ts +212 -4
- package/src/__tests__/__snapshots__/RestEndpoint.ts.snap +2 -0
- package/src/__tests__/__snapshots__/resource-construction.test.ts.snap +1 -1
- package/src/__tests__/hookifyResource.ts +7 -0
- package/src/__tests__/resource-construction.test.ts +543 -0
- package/src/pathTypes.ts +35 -23
- package/src/resource.ts +2 -0
- package/src/resourceTypes.ts +3 -1
- package/ts4.0/NetworkError.d.ts +12 -0
- package/ts4.0/RestEndpointTypes.d.ts +61 -9
- package/ts4.0/RestHelpers.d.ts +2 -2
- package/ts4.0/pathTypes.d.ts +1 -1
- package/ts4.0/resourceExtendable.d.ts +1 -1
- package/ts4.0/resourceTypes.d.ts +7 -1
- package/ts4.1/NetworkError.d.ts +12 -0
- package/ts4.1/RestEndpointTypes.d.ts +61 -9
- package/ts4.1/RestHelpers.d.ts +2 -2
- package/ts4.1/pathTypes.d.ts +14 -13
- package/ts4.1/resourceExtendable.d.ts +1 -1
- package/ts4.1/resourceTypes.d.ts +7 -1
|
@@ -64,6 +64,13 @@ describe('resource()', () => {
|
|
|
64
64
|
isAdmin: true,
|
|
65
65
|
};
|
|
66
66
|
|
|
67
|
+
let errorSpy: jest.SpyInstance;
|
|
68
|
+
beforeAll(() => {
|
|
69
|
+
errorSpy = jest.spyOn(console, 'error').mockImplementation(() => {});
|
|
70
|
+
});
|
|
71
|
+
afterAll(() => {
|
|
72
|
+
errorSpy.mockRestore();
|
|
73
|
+
});
|
|
67
74
|
beforeEach(() => {
|
|
68
75
|
nock(/.*/)
|
|
69
76
|
.persist()
|
|
@@ -818,6 +825,542 @@ describe('resource()', () => {
|
|
|
818
825
|
);
|
|
819
826
|
});
|
|
820
827
|
|
|
828
|
+
it('UserResource.getList.move should work', async () => {
|
|
829
|
+
mynock.patch(`/groups/five/users/2`).reply(200, (uri, body: any) => ({
|
|
830
|
+
id: 2,
|
|
831
|
+
username: 'user2',
|
|
832
|
+
email: 'user2@example.com',
|
|
833
|
+
group: 'ten',
|
|
834
|
+
...body,
|
|
835
|
+
}));
|
|
836
|
+
|
|
837
|
+
const { result, controller } = renderDataClient(
|
|
838
|
+
() => {
|
|
839
|
+
return {
|
|
840
|
+
user2: useQuery(User, { id: 2 }),
|
|
841
|
+
groupFive: useCache(UserResource.getList, { group: 'five' }),
|
|
842
|
+
groupTen: useCache(UserResource.getList, { group: 'ten' }),
|
|
843
|
+
};
|
|
844
|
+
},
|
|
845
|
+
{
|
|
846
|
+
initialFixtures: [
|
|
847
|
+
{
|
|
848
|
+
endpoint: UserResource.getList,
|
|
849
|
+
args: [{ group: 'five' }],
|
|
850
|
+
response: [
|
|
851
|
+
{
|
|
852
|
+
id: 1,
|
|
853
|
+
username: 'user1',
|
|
854
|
+
email: 'user1@example.com',
|
|
855
|
+
group: 'five',
|
|
856
|
+
},
|
|
857
|
+
{
|
|
858
|
+
id: 2,
|
|
859
|
+
username: 'user2',
|
|
860
|
+
email: 'user2@example.com',
|
|
861
|
+
group: 'five',
|
|
862
|
+
},
|
|
863
|
+
],
|
|
864
|
+
},
|
|
865
|
+
{
|
|
866
|
+
endpoint: UserResource.getList,
|
|
867
|
+
args: [{ group: 'ten' }],
|
|
868
|
+
response: [
|
|
869
|
+
{
|
|
870
|
+
id: 3,
|
|
871
|
+
username: 'user3',
|
|
872
|
+
email: 'user3@example.com',
|
|
873
|
+
group: 'ten',
|
|
874
|
+
},
|
|
875
|
+
],
|
|
876
|
+
},
|
|
877
|
+
],
|
|
878
|
+
},
|
|
879
|
+
);
|
|
880
|
+
|
|
881
|
+
expect(result.current.groupFive).toHaveLength(2);
|
|
882
|
+
expect(result.current.groupTen).toHaveLength(1);
|
|
883
|
+
|
|
884
|
+
// PATCH /groups/five/users/2 - moves user 2 from 'five' to 'ten'
|
|
885
|
+
await act(async () => {
|
|
886
|
+
const response = await controller.fetch(
|
|
887
|
+
UserResource.getList.move,
|
|
888
|
+
{ group: 'five', id: '2' },
|
|
889
|
+
{
|
|
890
|
+
id: 2,
|
|
891
|
+
group: 'ten',
|
|
892
|
+
},
|
|
893
|
+
);
|
|
894
|
+
expect(response.id).toEqual(2);
|
|
895
|
+
});
|
|
896
|
+
|
|
897
|
+
// user should be removed from group 'five'
|
|
898
|
+
expect(result.current.groupFive).toHaveLength(1);
|
|
899
|
+
expect(result.current.groupFive?.[0]?.username).toBe('user1');
|
|
900
|
+
|
|
901
|
+
// user should be added to group 'ten'
|
|
902
|
+
expect(result.current.groupTen).toHaveLength(2);
|
|
903
|
+
expect(result.current.groupTen?.map((u: any) => u.username)).toEqual(
|
|
904
|
+
expect.arrayContaining(['user3', 'user2']),
|
|
905
|
+
);
|
|
906
|
+
|
|
907
|
+
// entity should be updated
|
|
908
|
+
expect(result.current.user2?.group).toEqual('ten');
|
|
909
|
+
|
|
910
|
+
// move uses full path params (group + id), no searchParams
|
|
911
|
+
() =>
|
|
912
|
+
controller.fetch(
|
|
913
|
+
UserResource.getList.move,
|
|
914
|
+
{ group: 'five', id: 2 },
|
|
915
|
+
{ id: 1, group: 'ten' },
|
|
916
|
+
);
|
|
917
|
+
() =>
|
|
918
|
+
controller.fetch(
|
|
919
|
+
UserResource.getList.move,
|
|
920
|
+
// @ts-expect-error - missing required group
|
|
921
|
+
{ id: 2 },
|
|
922
|
+
{ id: 1 },
|
|
923
|
+
);
|
|
924
|
+
// @ts-expect-error
|
|
925
|
+
() => controller.fetch(UserResource.getList.move, { username: 'never' });
|
|
926
|
+
// @ts-expect-error
|
|
927
|
+
() => controller.fetch(UserResource.getList.move, 1, 'hi');
|
|
928
|
+
() =>
|
|
929
|
+
controller.fetch(
|
|
930
|
+
UserResource.getList.move,
|
|
931
|
+
{ group: 'five', id: 2 },
|
|
932
|
+
// @ts-expect-error
|
|
933
|
+
{ sdf: 'never' },
|
|
934
|
+
);
|
|
935
|
+
() =>
|
|
936
|
+
controller.fetch(
|
|
937
|
+
UserResource.getList.move,
|
|
938
|
+
// @ts-expect-error
|
|
939
|
+
{ sdf: 'five', id: 2 },
|
|
940
|
+
{ id: 1 },
|
|
941
|
+
);
|
|
942
|
+
});
|
|
943
|
+
|
|
944
|
+
it('getList.move should work when entity lacks path param field', async () => {
|
|
945
|
+
// Entity has 'status' but NOT 'team' - team is only a URL path param
|
|
946
|
+
// Collection keys include both team (from path) and status (from searchParams)
|
|
947
|
+
class Task extends Entity {
|
|
948
|
+
readonly id: number | undefined = undefined;
|
|
949
|
+
readonly title: string = '';
|
|
950
|
+
readonly status: string = 'backlog';
|
|
951
|
+
|
|
952
|
+
pk() {
|
|
953
|
+
return this.id?.toString();
|
|
954
|
+
}
|
|
955
|
+
}
|
|
956
|
+
|
|
957
|
+
const TeamTaskResource = resource({
|
|
958
|
+
path: 'http\\://test.com/teams/:team/tasks/:id',
|
|
959
|
+
searchParams: {} as { status: string },
|
|
960
|
+
schema: Task,
|
|
961
|
+
});
|
|
962
|
+
|
|
963
|
+
mynock.patch(`/teams/alpha/tasks/3`).reply(200, (uri, body: any) => ({
|
|
964
|
+
id: 3,
|
|
965
|
+
title: 'My Task',
|
|
966
|
+
status: 'in-progress',
|
|
967
|
+
...body,
|
|
968
|
+
}));
|
|
969
|
+
|
|
970
|
+
const { result, controller } = renderDataClient(
|
|
971
|
+
() => {
|
|
972
|
+
return {
|
|
973
|
+
task3: useQuery(Task, { id: 3 }),
|
|
974
|
+
backlog: useCache(TeamTaskResource.getList, {
|
|
975
|
+
team: 'alpha',
|
|
976
|
+
status: 'backlog',
|
|
977
|
+
}),
|
|
978
|
+
inProgress: useCache(TeamTaskResource.getList, {
|
|
979
|
+
team: 'alpha',
|
|
980
|
+
status: 'in-progress',
|
|
981
|
+
}),
|
|
982
|
+
};
|
|
983
|
+
},
|
|
984
|
+
{
|
|
985
|
+
initialFixtures: [
|
|
986
|
+
{
|
|
987
|
+
endpoint: TeamTaskResource.getList,
|
|
988
|
+
args: [{ team: 'alpha', status: 'backlog' }],
|
|
989
|
+
response: [
|
|
990
|
+
{ id: 1, title: 'Task 1', status: 'backlog' },
|
|
991
|
+
{ id: 3, title: 'My Task', status: 'backlog' },
|
|
992
|
+
],
|
|
993
|
+
},
|
|
994
|
+
{
|
|
995
|
+
endpoint: TeamTaskResource.getList,
|
|
996
|
+
args: [{ team: 'alpha', status: 'in-progress' }],
|
|
997
|
+
response: [{ id: 2, title: 'Task 2', status: 'in-progress' }],
|
|
998
|
+
},
|
|
999
|
+
],
|
|
1000
|
+
},
|
|
1001
|
+
);
|
|
1002
|
+
|
|
1003
|
+
expect(result.current.backlog).toHaveLength(2);
|
|
1004
|
+
expect(result.current.inProgress).toHaveLength(1);
|
|
1005
|
+
|
|
1006
|
+
// PATCH /teams/alpha/tasks/3 - move task 3 from backlog to in-progress
|
|
1007
|
+
// 'team' is only in URL params (not on entity); 'status' is in body and on entity
|
|
1008
|
+
await act(async () => {
|
|
1009
|
+
const response = await controller.fetch(
|
|
1010
|
+
TeamTaskResource.getList.move,
|
|
1011
|
+
{ team: 'alpha', id: '3' },
|
|
1012
|
+
{ id: 3, status: 'in-progress' },
|
|
1013
|
+
);
|
|
1014
|
+
expect(response.id).toEqual(3);
|
|
1015
|
+
});
|
|
1016
|
+
|
|
1017
|
+
// task should be removed from backlog
|
|
1018
|
+
expect(result.current.backlog).toHaveLength(1);
|
|
1019
|
+
expect(result.current.backlog?.[0]?.title).toBe('Task 1');
|
|
1020
|
+
|
|
1021
|
+
// task should be added to in-progress
|
|
1022
|
+
expect(result.current.inProgress).toHaveLength(2);
|
|
1023
|
+
expect(result.current.inProgress?.map((t: any) => t.title)).toEqual(
|
|
1024
|
+
expect.arrayContaining(['Task 2', 'My Task']),
|
|
1025
|
+
);
|
|
1026
|
+
|
|
1027
|
+
// entity should be updated
|
|
1028
|
+
expect(result.current.task3?.status).toEqual('in-progress');
|
|
1029
|
+
});
|
|
1030
|
+
|
|
1031
|
+
it('getList.move should work with searchParams-based collections', async () => {
|
|
1032
|
+
class Task extends Entity {
|
|
1033
|
+
readonly id: number | undefined = undefined;
|
|
1034
|
+
readonly title: string = '';
|
|
1035
|
+
readonly status: string = 'backlog';
|
|
1036
|
+
|
|
1037
|
+
pk() {
|
|
1038
|
+
return this.id?.toString();
|
|
1039
|
+
}
|
|
1040
|
+
}
|
|
1041
|
+
|
|
1042
|
+
const TaskResource = resource({
|
|
1043
|
+
path: 'http\\://test.com/tasks/:id',
|
|
1044
|
+
searchParams: {} as { status: string },
|
|
1045
|
+
schema: Task,
|
|
1046
|
+
});
|
|
1047
|
+
|
|
1048
|
+
mynock.patch(`/tasks/3`).reply(200, (uri, body: any) => ({
|
|
1049
|
+
id: 3,
|
|
1050
|
+
title: 'My Task',
|
|
1051
|
+
status: 'in-progress',
|
|
1052
|
+
...body,
|
|
1053
|
+
}));
|
|
1054
|
+
|
|
1055
|
+
const { result, controller } = renderDataClient(
|
|
1056
|
+
() => {
|
|
1057
|
+
return {
|
|
1058
|
+
task3: useQuery(Task, { id: 3 }),
|
|
1059
|
+
backlog: useCache(TaskResource.getList, { status: 'backlog' }),
|
|
1060
|
+
inProgress: useCache(TaskResource.getList, {
|
|
1061
|
+
status: 'in-progress',
|
|
1062
|
+
}),
|
|
1063
|
+
};
|
|
1064
|
+
},
|
|
1065
|
+
{
|
|
1066
|
+
initialFixtures: [
|
|
1067
|
+
{
|
|
1068
|
+
endpoint: TaskResource.getList,
|
|
1069
|
+
args: [{ status: 'backlog' }],
|
|
1070
|
+
response: [
|
|
1071
|
+
{ id: 1, title: 'Task 1', status: 'backlog' },
|
|
1072
|
+
{ id: 3, title: 'My Task', status: 'backlog' },
|
|
1073
|
+
],
|
|
1074
|
+
},
|
|
1075
|
+
{
|
|
1076
|
+
endpoint: TaskResource.getList,
|
|
1077
|
+
args: [{ status: 'in-progress' }],
|
|
1078
|
+
response: [{ id: 2, title: 'Task 2', status: 'in-progress' }],
|
|
1079
|
+
},
|
|
1080
|
+
],
|
|
1081
|
+
},
|
|
1082
|
+
);
|
|
1083
|
+
|
|
1084
|
+
expect(result.current.backlog).toHaveLength(2);
|
|
1085
|
+
expect(result.current.inProgress).toHaveLength(1);
|
|
1086
|
+
|
|
1087
|
+
// PATCH /tasks/3 - moves task 3 from 'backlog' to 'in-progress'
|
|
1088
|
+
await act(async () => {
|
|
1089
|
+
const response = await controller.fetch(
|
|
1090
|
+
TaskResource.getList.move,
|
|
1091
|
+
{ id: '3' },
|
|
1092
|
+
{ id: 3, status: 'in-progress' },
|
|
1093
|
+
);
|
|
1094
|
+
expect(response.id).toEqual(3);
|
|
1095
|
+
});
|
|
1096
|
+
|
|
1097
|
+
// task should be removed from backlog
|
|
1098
|
+
expect(result.current.backlog).toHaveLength(1);
|
|
1099
|
+
expect(result.current.backlog?.[0]?.title).toBe('Task 1');
|
|
1100
|
+
|
|
1101
|
+
// task should be added to in-progress
|
|
1102
|
+
expect(result.current.inProgress).toHaveLength(2);
|
|
1103
|
+
expect(result.current.inProgress?.map((t: any) => t.title)).toEqual(
|
|
1104
|
+
expect.arrayContaining(['Task 2', 'My Task']),
|
|
1105
|
+
);
|
|
1106
|
+
|
|
1107
|
+
// entity should be updated
|
|
1108
|
+
expect(result.current.task3?.status).toEqual('in-progress');
|
|
1109
|
+
});
|
|
1110
|
+
|
|
1111
|
+
it('getList.move should work with FormData body', async () => {
|
|
1112
|
+
class Task extends Entity {
|
|
1113
|
+
readonly id: number | undefined = undefined;
|
|
1114
|
+
readonly title: string = '';
|
|
1115
|
+
readonly status: string = 'backlog';
|
|
1116
|
+
|
|
1117
|
+
pk() {
|
|
1118
|
+
return this.id?.toString();
|
|
1119
|
+
}
|
|
1120
|
+
}
|
|
1121
|
+
|
|
1122
|
+
const TaskResource = resource({
|
|
1123
|
+
path: 'http\\://test.com/tasks/:id',
|
|
1124
|
+
searchParams: {} as { status: string },
|
|
1125
|
+
schema: Task,
|
|
1126
|
+
});
|
|
1127
|
+
|
|
1128
|
+
mynock.patch(`/tasks/3`).reply(200, (uri, body: any) => ({
|
|
1129
|
+
id: 3,
|
|
1130
|
+
title: 'My Task',
|
|
1131
|
+
status: 'in-progress',
|
|
1132
|
+
...(body instanceof FormData ? Object.fromEntries(body.entries()) : body),
|
|
1133
|
+
}));
|
|
1134
|
+
|
|
1135
|
+
const { result, controller } = renderDataClient(
|
|
1136
|
+
() => {
|
|
1137
|
+
return {
|
|
1138
|
+
task3: useQuery(Task, { id: 3 }),
|
|
1139
|
+
backlog: useCache(TaskResource.getList, { status: 'backlog' }),
|
|
1140
|
+
inProgress: useCache(TaskResource.getList, {
|
|
1141
|
+
status: 'in-progress',
|
|
1142
|
+
}),
|
|
1143
|
+
};
|
|
1144
|
+
},
|
|
1145
|
+
{
|
|
1146
|
+
initialFixtures: [
|
|
1147
|
+
{
|
|
1148
|
+
endpoint: TaskResource.getList,
|
|
1149
|
+
args: [{ status: 'backlog' }],
|
|
1150
|
+
response: [
|
|
1151
|
+
{ id: 1, title: 'Task 1', status: 'backlog' },
|
|
1152
|
+
{ id: 3, title: 'My Task', status: 'backlog' },
|
|
1153
|
+
],
|
|
1154
|
+
},
|
|
1155
|
+
{
|
|
1156
|
+
endpoint: TaskResource.getList,
|
|
1157
|
+
args: [{ status: 'in-progress' }],
|
|
1158
|
+
response: [{ id: 2, title: 'Task 2', status: 'in-progress' }],
|
|
1159
|
+
},
|
|
1160
|
+
],
|
|
1161
|
+
},
|
|
1162
|
+
);
|
|
1163
|
+
|
|
1164
|
+
expect(result.current.backlog).toHaveLength(2);
|
|
1165
|
+
expect(result.current.inProgress).toHaveLength(1);
|
|
1166
|
+
|
|
1167
|
+
// Use FormData as the body (simulates multipart form submission)
|
|
1168
|
+
const formData = new FormData();
|
|
1169
|
+
formData.append('id', '3');
|
|
1170
|
+
formData.append('status', 'in-progress');
|
|
1171
|
+
|
|
1172
|
+
await act(async () => {
|
|
1173
|
+
const response = await controller.fetch(
|
|
1174
|
+
TaskResource.getList.move,
|
|
1175
|
+
{ id: '3' },
|
|
1176
|
+
formData,
|
|
1177
|
+
);
|
|
1178
|
+
expect(response.id).toEqual(3);
|
|
1179
|
+
});
|
|
1180
|
+
|
|
1181
|
+
// task should be removed from backlog
|
|
1182
|
+
expect(result.current.backlog).toHaveLength(1);
|
|
1183
|
+
expect(result.current.backlog?.[0]?.title).toBe('Task 1');
|
|
1184
|
+
|
|
1185
|
+
// task should be added to in-progress
|
|
1186
|
+
expect(result.current.inProgress).toHaveLength(2);
|
|
1187
|
+
expect(result.current.inProgress?.map((t: any) => t.title)).toEqual(
|
|
1188
|
+
expect.arrayContaining(['Task 2', 'My Task']),
|
|
1189
|
+
);
|
|
1190
|
+
|
|
1191
|
+
// entity should be updated
|
|
1192
|
+
expect(result.current.task3?.status).toEqual('in-progress');
|
|
1193
|
+
});
|
|
1194
|
+
|
|
1195
|
+
it('getList.move should work optimistically with path-based collections', async () => {
|
|
1196
|
+
const OptUserResource = resource({
|
|
1197
|
+
path: 'http\\://test.com/groups/:group/users/:id',
|
|
1198
|
+
schema: User,
|
|
1199
|
+
optimistic: true,
|
|
1200
|
+
});
|
|
1201
|
+
|
|
1202
|
+
mynock.patch(`/groups/five/users/2`).reply(200, () => ({
|
|
1203
|
+
id: 2,
|
|
1204
|
+
username: 'user2',
|
|
1205
|
+
email: 'user2@example.com',
|
|
1206
|
+
group: 'ten',
|
|
1207
|
+
}));
|
|
1208
|
+
|
|
1209
|
+
const { result, controller } = renderDataClient(
|
|
1210
|
+
() => {
|
|
1211
|
+
return {
|
|
1212
|
+
user2: useQuery(User, { id: 2 }),
|
|
1213
|
+
groupFive: useCache(OptUserResource.getList, { group: 'five' }),
|
|
1214
|
+
groupTen: useCache(OptUserResource.getList, { group: 'ten' }),
|
|
1215
|
+
};
|
|
1216
|
+
},
|
|
1217
|
+
{
|
|
1218
|
+
initialFixtures: [
|
|
1219
|
+
{
|
|
1220
|
+
endpoint: OptUserResource.getList,
|
|
1221
|
+
args: [{ group: 'five' }],
|
|
1222
|
+
response: [
|
|
1223
|
+
{
|
|
1224
|
+
id: 1,
|
|
1225
|
+
username: 'user1',
|
|
1226
|
+
email: 'user1@example.com',
|
|
1227
|
+
group: 'five',
|
|
1228
|
+
},
|
|
1229
|
+
{
|
|
1230
|
+
id: 2,
|
|
1231
|
+
username: 'user2',
|
|
1232
|
+
email: 'user2@example.com',
|
|
1233
|
+
group: 'five',
|
|
1234
|
+
},
|
|
1235
|
+
],
|
|
1236
|
+
},
|
|
1237
|
+
{
|
|
1238
|
+
endpoint: OptUserResource.getList,
|
|
1239
|
+
args: [{ group: 'ten' }],
|
|
1240
|
+
response: [
|
|
1241
|
+
{
|
|
1242
|
+
id: 3,
|
|
1243
|
+
username: 'user3',
|
|
1244
|
+
email: 'user3@example.com',
|
|
1245
|
+
group: 'ten',
|
|
1246
|
+
},
|
|
1247
|
+
],
|
|
1248
|
+
},
|
|
1249
|
+
],
|
|
1250
|
+
},
|
|
1251
|
+
);
|
|
1252
|
+
|
|
1253
|
+
expect(OptUserResource.getList.move.getOptimisticResponse).toBeDefined();
|
|
1254
|
+
expect(result.current.groupFive).toHaveLength(2);
|
|
1255
|
+
expect(result.current.groupTen).toHaveLength(1);
|
|
1256
|
+
|
|
1257
|
+
let promise: any;
|
|
1258
|
+
act(() => {
|
|
1259
|
+
promise = controller.fetch(
|
|
1260
|
+
OptUserResource.getList.move,
|
|
1261
|
+
{ group: 'five', id: '2' },
|
|
1262
|
+
{ id: 2, group: 'ten' },
|
|
1263
|
+
);
|
|
1264
|
+
});
|
|
1265
|
+
|
|
1266
|
+
// optimistic: should update immediately before network responds
|
|
1267
|
+
expect(result.current.groupFive).toHaveLength(1);
|
|
1268
|
+
expect(result.current.groupFive?.[0]?.username).toBe('user1');
|
|
1269
|
+
expect(result.current.groupTen).toHaveLength(2);
|
|
1270
|
+
expect(result.current.groupTen?.map((u: any) => u.username)).toEqual(
|
|
1271
|
+
expect.arrayContaining(['user3', 'user2']),
|
|
1272
|
+
);
|
|
1273
|
+
expect(result.current.user2?.group).toEqual('ten');
|
|
1274
|
+
|
|
1275
|
+
// after server response, should still be correct
|
|
1276
|
+
await act(() => promise);
|
|
1277
|
+
expect(result.current.groupFive).toHaveLength(1);
|
|
1278
|
+
expect(result.current.groupTen).toHaveLength(2);
|
|
1279
|
+
expect(result.current.user2?.group).toEqual('ten');
|
|
1280
|
+
});
|
|
1281
|
+
|
|
1282
|
+
it('getList.move should work optimistically with searchParams-based collections', async () => {
|
|
1283
|
+
class Task extends Entity {
|
|
1284
|
+
readonly id: number | undefined = undefined;
|
|
1285
|
+
readonly title: string = '';
|
|
1286
|
+
readonly status: string = 'backlog';
|
|
1287
|
+
|
|
1288
|
+
pk() {
|
|
1289
|
+
return this.id?.toString();
|
|
1290
|
+
}
|
|
1291
|
+
}
|
|
1292
|
+
|
|
1293
|
+
const TaskResource = resource({
|
|
1294
|
+
path: 'http\\://test.com/tasks/:id',
|
|
1295
|
+
searchParams: {} as { status: string },
|
|
1296
|
+
schema: Task,
|
|
1297
|
+
optimistic: true,
|
|
1298
|
+
});
|
|
1299
|
+
|
|
1300
|
+
mynock.patch(`/tasks/3`).reply(200, () => ({
|
|
1301
|
+
id: 3,
|
|
1302
|
+
title: 'My Task',
|
|
1303
|
+
status: 'in-progress',
|
|
1304
|
+
}));
|
|
1305
|
+
|
|
1306
|
+
const { result, controller } = renderDataClient(
|
|
1307
|
+
() => {
|
|
1308
|
+
return {
|
|
1309
|
+
task3: useQuery(Task, { id: 3 }),
|
|
1310
|
+
backlog: useCache(TaskResource.getList, { status: 'backlog' }),
|
|
1311
|
+
inProgress: useCache(TaskResource.getList, {
|
|
1312
|
+
status: 'in-progress',
|
|
1313
|
+
}),
|
|
1314
|
+
};
|
|
1315
|
+
},
|
|
1316
|
+
{
|
|
1317
|
+
initialFixtures: [
|
|
1318
|
+
{
|
|
1319
|
+
endpoint: TaskResource.getList,
|
|
1320
|
+
args: [{ status: 'backlog' }],
|
|
1321
|
+
response: [
|
|
1322
|
+
{ id: 1, title: 'Task 1', status: 'backlog' },
|
|
1323
|
+
{ id: 3, title: 'My Task', status: 'backlog' },
|
|
1324
|
+
],
|
|
1325
|
+
},
|
|
1326
|
+
{
|
|
1327
|
+
endpoint: TaskResource.getList,
|
|
1328
|
+
args: [{ status: 'in-progress' }],
|
|
1329
|
+
response: [{ id: 2, title: 'Task 2', status: 'in-progress' }],
|
|
1330
|
+
},
|
|
1331
|
+
],
|
|
1332
|
+
},
|
|
1333
|
+
);
|
|
1334
|
+
|
|
1335
|
+
expect(TaskResource.getList.move.getOptimisticResponse).toBeDefined();
|
|
1336
|
+
expect(result.current.backlog).toHaveLength(2);
|
|
1337
|
+
expect(result.current.inProgress).toHaveLength(1);
|
|
1338
|
+
|
|
1339
|
+
let promise: any;
|
|
1340
|
+
act(() => {
|
|
1341
|
+
promise = controller.fetch(
|
|
1342
|
+
TaskResource.getList.move,
|
|
1343
|
+
{ id: '3' },
|
|
1344
|
+
{ id: 3, status: 'in-progress' },
|
|
1345
|
+
);
|
|
1346
|
+
});
|
|
1347
|
+
|
|
1348
|
+
// optimistic: should update immediately before network responds
|
|
1349
|
+
expect(result.current.backlog).toHaveLength(1);
|
|
1350
|
+
expect(result.current.backlog?.[0]?.title).toBe('Task 1');
|
|
1351
|
+
expect(result.current.inProgress).toHaveLength(2);
|
|
1352
|
+
expect(result.current.inProgress?.map((t: any) => t.title)).toEqual(
|
|
1353
|
+
expect.arrayContaining(['Task 2', 'My Task']),
|
|
1354
|
+
);
|
|
1355
|
+
expect(result.current.task3?.status).toEqual('in-progress');
|
|
1356
|
+
|
|
1357
|
+
// after server response, should still be correct
|
|
1358
|
+
await act(() => promise);
|
|
1359
|
+
expect(result.current.backlog).toHaveLength(1);
|
|
1360
|
+
expect(result.current.inProgress).toHaveLength(2);
|
|
1361
|
+
expect(result.current.task3?.status).toEqual('in-progress');
|
|
1362
|
+
});
|
|
1363
|
+
|
|
821
1364
|
it.each([
|
|
822
1365
|
{
|
|
823
1366
|
response: {
|
package/src/pathTypes.ts
CHANGED
|
@@ -1,8 +1,14 @@
|
|
|
1
|
-
type
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
1
|
+
type CleanKey<S extends string> = S extends `"${infer K}"` ? K : S;
|
|
2
|
+
|
|
3
|
+
type KeyName<K extends string> = CleanKey<
|
|
4
|
+
K extends `*${infer N}}` ? N
|
|
5
|
+
: K extends `*${infer N}` ? N
|
|
6
|
+
: K extends `${infer N}}` ? N
|
|
7
|
+
: K
|
|
8
|
+
>;
|
|
9
|
+
|
|
10
|
+
type KeyVal<K extends string> =
|
|
11
|
+
K extends `*${string}` ? string[] : string | number;
|
|
6
12
|
|
|
7
13
|
/** Parameters for a given path */
|
|
8
14
|
export type PathArgs<S extends string> =
|
|
@@ -14,43 +20,49 @@ export type PathArgs<S extends string> =
|
|
|
14
20
|
/** Computes the union of keys for a path string */
|
|
15
21
|
export type PathKeys<S extends string> =
|
|
16
22
|
string extends S ? string
|
|
17
|
-
: S extends `${infer A}\\${':' | '
|
|
23
|
+
: S extends `${infer A}\\${':' | '*' | '}'}${infer B}` ?
|
|
18
24
|
PathKeys<A> | PathKeys<B>
|
|
19
|
-
:
|
|
25
|
+
: Splits<S, ':'> | Splits<S, '*'>;
|
|
20
26
|
|
|
21
|
-
type
|
|
22
|
-
S extends
|
|
23
|
-
|
|
27
|
+
type Splits<S extends string, M extends ':' | '*'> =
|
|
28
|
+
S extends `${string}${M}${infer K}${M}${infer R}` ?
|
|
29
|
+
Splits<`${M}${K}`, M> | Splits<`${M}${R}`, M>
|
|
30
|
+
: S extends (
|
|
31
|
+
`${string}${M}${infer K}${'/' | '\\' | '%' | '&' | '*' | ':' | '{' | ';' | ',' | '!' | '@'}${infer R}`
|
|
24
32
|
) ?
|
|
25
|
-
|
|
26
|
-
: S extends `${string}
|
|
27
|
-
|
|
28
|
-
|
|
33
|
+
Splits<`${M}${K}`, M> | Splits<R, M>
|
|
34
|
+
: S extends `${string}${M}${infer K}` ?
|
|
35
|
+
M extends '*' ?
|
|
36
|
+
`*${K}`
|
|
37
|
+
: K
|
|
29
38
|
: never;
|
|
30
39
|
|
|
31
40
|
export type KeysToArgs<Key extends string> = {
|
|
32
|
-
[K in Key as
|
|
33
|
-
} & (
|
|
41
|
+
[K in Key as K extends `${string}}` ? KeyName<K> : never]?: KeyVal<K>;
|
|
42
|
+
} & (Exclude<Key, `${string}}`> extends never ? unknown
|
|
34
43
|
: {
|
|
35
|
-
[K in Key as
|
|
44
|
+
[K in Key as K extends `${string}}` ? never : KeyName<K>]: KeyVal<K>;
|
|
36
45
|
});
|
|
37
46
|
|
|
38
47
|
export type PathArgsAndSearch<S extends string> =
|
|
39
|
-
|
|
48
|
+
Exclude<PathKeys<S>, `${string}}`> extends never ?
|
|
40
49
|
Record<string, number | string | boolean> | undefined
|
|
41
50
|
: {
|
|
42
|
-
[K in PathKeys<S> as
|
|
43
|
-
|
|
51
|
+
[K in PathKeys<S> as K extends `${string}}` ? never
|
|
52
|
+
: KeyName<K>]: KeyVal<K>;
|
|
53
|
+
} & Record<string, number | string | string[]>;
|
|
44
54
|
|
|
45
|
-
/** Removes the last :token */
|
|
55
|
+
/** Removes the last :param or *wildcard token */
|
|
46
56
|
export type ShortenPath<S extends string> =
|
|
47
57
|
string extends S ? string
|
|
48
|
-
: S extends `${infer B}:${infer R}` ?
|
|
58
|
+
: S extends `${infer B}:${infer R}` ? TrimToken<`${B}:${ShortenPath<R>}`>
|
|
59
|
+
: S extends `${infer B}*${infer R}` ? TrimToken<`${B}*${ShortenPath<R>}`>
|
|
49
60
|
: '';
|
|
50
61
|
|
|
51
|
-
type
|
|
62
|
+
type TrimToken<S extends string> =
|
|
52
63
|
string extends S ? string
|
|
53
64
|
: S extends `${infer R}:` ? R
|
|
65
|
+
: S extends `${infer R}*` ? R
|
|
54
66
|
: S;
|
|
55
67
|
|
|
56
68
|
export type ResourcePath = string; // `${string}:${string}`; TODO: Maybe do this in the future? Seems to hard to understand for now
|
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,
|
package/src/resourceTypes.ts
CHANGED
|
@@ -84,7 +84,7 @@ export interface Resource<
|
|
|
84
84
|
schema: schema.Collection<
|
|
85
85
|
[O['schema']],
|
|
86
86
|
ParamToArgs<
|
|
87
|
-
O['searchParams'] extends undefined ?
|
|
87
|
+
[O['searchParams']] extends [undefined] ?
|
|
88
88
|
KeysToArgs<ShortenPath<O['path']>>
|
|
89
89
|
: O['searchParams'] & PathArgs<ShortenPath<O['path']>>
|
|
90
90
|
>
|
|
@@ -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)
|