@hamak/ui-remote-resource-impl 0.4.19
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/dist/es2015/index.js +33 -0
- package/dist/es2015/middleware/entity-middleware.js +209 -0
- package/dist/es2015/middleware/entity-sync-middleware.js +36 -0
- package/dist/es2015/middleware/resource-middleware.js +111 -0
- package/dist/es2015/middleware/sync-middleware.js +85 -0
- package/dist/es2015/plugin/resource-plugin-factory.js +151 -0
- package/dist/es2015/providers/mock-resource-provider.js +215 -0
- package/dist/es2015/providers/rest-resource-provider.js +140 -0
- package/dist/es2015/registry/entity-registry.js +50 -0
- package/dist/es2015/registry/resource-registry.js +68 -0
- package/dist/index.d.ts +18 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +16 -0
- package/dist/middleware/entity-middleware.d.ts +13 -0
- package/dist/middleware/entity-middleware.d.ts.map +1 -0
- package/dist/middleware/entity-middleware.js +221 -0
- package/dist/middleware/entity-sync-middleware.d.ts +7 -0
- package/dist/middleware/entity-sync-middleware.d.ts.map +1 -0
- package/dist/middleware/entity-sync-middleware.js +31 -0
- package/dist/middleware/resource-middleware.d.ts +13 -0
- package/dist/middleware/resource-middleware.d.ts.map +1 -0
- package/dist/middleware/resource-middleware.js +97 -0
- package/dist/middleware/sync-middleware.d.ts +12 -0
- package/dist/middleware/sync-middleware.d.ts.map +1 -0
- package/dist/middleware/sync-middleware.js +80 -0
- package/dist/plugin/resource-plugin-factory.d.ts +31 -0
- package/dist/plugin/resource-plugin-factory.d.ts.map +1 -0
- package/dist/plugin/resource-plugin-factory.js +131 -0
- package/dist/providers/mock-resource-provider.d.ts +147 -0
- package/dist/providers/mock-resource-provider.d.ts.map +1 -0
- package/dist/providers/mock-resource-provider.js +196 -0
- package/dist/providers/rest-resource-provider.d.ts +51 -0
- package/dist/providers/rest-resource-provider.d.ts.map +1 -0
- package/dist/providers/rest-resource-provider.js +117 -0
- package/dist/registry/entity-registry.d.ts +54 -0
- package/dist/registry/entity-registry.d.ts.map +1 -0
- package/dist/registry/entity-registry.js +46 -0
- package/dist/registry/resource-registry.d.ts +80 -0
- package/dist/registry/resource-registry.d.ts.map +1 -0
- package/dist/registry/resource-registry.js +64 -0
- package/package.json +57 -0
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
import { EntityActionTypes, ResourceActionFactory, resourceAttributesUtil } from '@hamak/ui-remote-resource-api';
|
|
2
|
+
/**
|
|
3
|
+
* Entity middleware
|
|
4
|
+
* Translates entity actions to resource actions
|
|
5
|
+
*/
|
|
6
|
+
export function createEntityMiddleware(config) {
|
|
7
|
+
const { entityRegistry, fsSliceName, onError } = config;
|
|
8
|
+
const resourceActionFactory = new ResourceActionFactory();
|
|
9
|
+
return (store) => (next) => (action) => {
|
|
10
|
+
// Only handle entity actions
|
|
11
|
+
if (!isEntityAction(action)) {
|
|
12
|
+
return next(action);
|
|
13
|
+
}
|
|
14
|
+
// Pass through for tracking
|
|
15
|
+
next(action);
|
|
16
|
+
const entityAction = action;
|
|
17
|
+
try {
|
|
18
|
+
// Translate entity action to resource action
|
|
19
|
+
const resourceAction = translateEntityAction(entityAction, entityRegistry, resourceActionFactory, store.getState(), fsSliceName);
|
|
20
|
+
// Dispatch resource action
|
|
21
|
+
store.dispatch(resourceAction);
|
|
22
|
+
}
|
|
23
|
+
catch (error) {
|
|
24
|
+
onError?.(error, entityAction);
|
|
25
|
+
console.error('Entity middleware error:', error);
|
|
26
|
+
}
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
function isEntityAction(action) {
|
|
30
|
+
return [
|
|
31
|
+
EntityActionTypes.FETCH_ENTITY,
|
|
32
|
+
EntityActionTypes.UPDATE_ENTITY,
|
|
33
|
+
EntityActionTypes.DELETE_ENTITY,
|
|
34
|
+
EntityActionTypes.CREATE_ENTITY
|
|
35
|
+
].includes(action.type);
|
|
36
|
+
}
|
|
37
|
+
function translateEntityAction(entityAction, entityRegistry, resourceActionFactory, state, fsSliceName) {
|
|
38
|
+
const { entityId } = entityAction.payload;
|
|
39
|
+
// Get entity definition
|
|
40
|
+
const entityDef = entityRegistry.getEntity(entityId);
|
|
41
|
+
if (!entityDef) {
|
|
42
|
+
throw new Error(`Entity not found: ${entityId}`);
|
|
43
|
+
}
|
|
44
|
+
switch (entityAction.type) {
|
|
45
|
+
case EntityActionTypes.FETCH_ENTITY:
|
|
46
|
+
return handleFetchEntity(entityAction, entityDef, resourceActionFactory);
|
|
47
|
+
case EntityActionTypes.UPDATE_ENTITY:
|
|
48
|
+
return handleUpdateEntity(entityAction, entityDef, resourceActionFactory, state, fsSliceName);
|
|
49
|
+
case EntityActionTypes.DELETE_ENTITY:
|
|
50
|
+
return handleDeleteEntity(entityAction, entityDef, resourceActionFactory, state, fsSliceName);
|
|
51
|
+
case EntityActionTypes.CREATE_ENTITY:
|
|
52
|
+
return handleCreateEntity(entityAction, entityDef, resourceActionFactory);
|
|
53
|
+
default:
|
|
54
|
+
throw new Error(`Unknown entity action type: ${entityAction.type}`);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Handle FETCH_ENTITY: keys provided, generate path and params
|
|
59
|
+
*/
|
|
60
|
+
function handleFetchEntity(action, entityDef, resourceActionFactory) {
|
|
61
|
+
const { keys, params, path } = action.payload;
|
|
62
|
+
// Validate keys
|
|
63
|
+
validateKeys(keys, entityDef.keySchema);
|
|
64
|
+
// Generate path from keys (if not explicitly provided)
|
|
65
|
+
const targetPath = path || generatePath(keys, entityDef);
|
|
66
|
+
// Map keys to resource params
|
|
67
|
+
const resourceParams = entityDef.keyMapper(keys, 'fetch');
|
|
68
|
+
// Merge with additional params
|
|
69
|
+
const mergedParams = {
|
|
70
|
+
...resourceParams.params,
|
|
71
|
+
...params,
|
|
72
|
+
// Store entity context for entity sync middleware
|
|
73
|
+
_entityContext: {
|
|
74
|
+
entityId: entityDef.id,
|
|
75
|
+
keys,
|
|
76
|
+
keyFields: entityDef.keySchema.fields
|
|
77
|
+
}
|
|
78
|
+
};
|
|
79
|
+
return resourceActionFactory.callRequest(entityDef.resourceEndpointId, 'fetch', mergedParams, targetPath, action.id);
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Handle UPDATE_ENTITY: path provided, read keys from file attributes
|
|
83
|
+
*/
|
|
84
|
+
function handleUpdateEntity(action, entityDef, resourceActionFactory, state, fsSliceName) {
|
|
85
|
+
const { path, data, params } = action.payload;
|
|
86
|
+
// Read file node
|
|
87
|
+
const fileNode = selectFileNode(state, path, fsSliceName);
|
|
88
|
+
if (!fileNode) {
|
|
89
|
+
throw new Error(`Entity not found at path: ${JSON.stringify(path)}`);
|
|
90
|
+
}
|
|
91
|
+
// Get entity keys from extension state
|
|
92
|
+
const keys = resourceAttributesUtil.getEntityKeys(fileNode);
|
|
93
|
+
if (!keys) {
|
|
94
|
+
throw new Error(`Entity keys not found in file attributes at: ${JSON.stringify(path)}`);
|
|
95
|
+
}
|
|
96
|
+
// Validate keys
|
|
97
|
+
validateKeys(keys, entityDef.keySchema);
|
|
98
|
+
// Map keys to resource params
|
|
99
|
+
const resourceParams = entityDef.keyMapper(keys, 'update');
|
|
100
|
+
// Merge with body data and additional params
|
|
101
|
+
const mergedParams = {
|
|
102
|
+
...resourceParams.params,
|
|
103
|
+
...params,
|
|
104
|
+
body: data,
|
|
105
|
+
_entityContext: {
|
|
106
|
+
entityId: entityDef.id,
|
|
107
|
+
keys,
|
|
108
|
+
keyFields: entityDef.keySchema.fields
|
|
109
|
+
}
|
|
110
|
+
};
|
|
111
|
+
return resourceActionFactory.callRequest(entityDef.resourceEndpointId, 'update', mergedParams, path, action.id);
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Handle DELETE_ENTITY: path provided, read keys from file attributes
|
|
115
|
+
*/
|
|
116
|
+
function handleDeleteEntity(action, entityDef, resourceActionFactory, state, fsSliceName) {
|
|
117
|
+
const { path, params } = action.payload;
|
|
118
|
+
// Read file node
|
|
119
|
+
const fileNode = selectFileNode(state, path, fsSliceName);
|
|
120
|
+
if (!fileNode) {
|
|
121
|
+
throw new Error(`Entity not found at path: ${JSON.stringify(path)}`);
|
|
122
|
+
}
|
|
123
|
+
// Get entity keys from extension state
|
|
124
|
+
const keys = resourceAttributesUtil.getEntityKeys(fileNode);
|
|
125
|
+
if (!keys) {
|
|
126
|
+
throw new Error(`Entity keys not found in file attributes at: ${JSON.stringify(path)}`);
|
|
127
|
+
}
|
|
128
|
+
// Validate keys
|
|
129
|
+
validateKeys(keys, entityDef.keySchema);
|
|
130
|
+
// Map keys to resource params
|
|
131
|
+
const resourceParams = entityDef.keyMapper(keys, 'delete');
|
|
132
|
+
// Merge with additional params
|
|
133
|
+
const mergedParams = {
|
|
134
|
+
...resourceParams.params,
|
|
135
|
+
...params,
|
|
136
|
+
_entityContext: {
|
|
137
|
+
entityId: entityDef.id,
|
|
138
|
+
keys,
|
|
139
|
+
keyFields: entityDef.keySchema.fields
|
|
140
|
+
}
|
|
141
|
+
};
|
|
142
|
+
return resourceActionFactory.callRequest(entityDef.resourceEndpointId, 'delete', mergedParams, path, action.id);
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* Handle CREATE_ENTITY: keys optional (may be generated by server)
|
|
146
|
+
*/
|
|
147
|
+
function handleCreateEntity(action, entityDef, resourceActionFactory) {
|
|
148
|
+
const { keys, data, path, params } = action.payload;
|
|
149
|
+
// Keys are optional for create (server may generate ID)
|
|
150
|
+
let targetPath = path;
|
|
151
|
+
let resourceParams;
|
|
152
|
+
if (keys) {
|
|
153
|
+
validateKeys(keys, entityDef.keySchema, true); // Partial validation
|
|
154
|
+
targetPath = path || generatePath(keys, entityDef);
|
|
155
|
+
resourceParams = entityDef.keyMapper(keys, 'create');
|
|
156
|
+
}
|
|
157
|
+
else {
|
|
158
|
+
resourceParams = { params: {} };
|
|
159
|
+
}
|
|
160
|
+
// Merge with body data and additional params
|
|
161
|
+
const mergedParams = {
|
|
162
|
+
...resourceParams.params,
|
|
163
|
+
...params,
|
|
164
|
+
body: data,
|
|
165
|
+
_entityContext: keys
|
|
166
|
+
? {
|
|
167
|
+
entityId: entityDef.id,
|
|
168
|
+
keys,
|
|
169
|
+
keyFields: entityDef.keySchema.fields
|
|
170
|
+
}
|
|
171
|
+
: undefined
|
|
172
|
+
};
|
|
173
|
+
return resourceActionFactory.callRequest(entityDef.resourceEndpointId, 'create', mergedParams, targetPath, action.id);
|
|
174
|
+
}
|
|
175
|
+
/**
|
|
176
|
+
* Validate entity keys against schema
|
|
177
|
+
*/
|
|
178
|
+
function validateKeys(keys, schema, partial = false) {
|
|
179
|
+
if (!partial) {
|
|
180
|
+
for (const field of schema.fields) {
|
|
181
|
+
if (!(field in keys) || keys[field] === undefined || keys[field] === null) {
|
|
182
|
+
throw new Error(`Missing required key field: ${field}`);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
if (schema.validate) {
|
|
187
|
+
const result = schema.validate(keys);
|
|
188
|
+
if (result !== true) {
|
|
189
|
+
throw new Error(typeof result === 'string' ? result : 'Invalid entity keys');
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
/**
|
|
194
|
+
* Generate file path from entity keys
|
|
195
|
+
*/
|
|
196
|
+
function generatePath(keys, entityDef) {
|
|
197
|
+
if (entityDef.pathGenerator) {
|
|
198
|
+
const result = entityDef.pathGenerator(keys, entityDef.id);
|
|
199
|
+
return Array.isArray(result) ? result : [result];
|
|
200
|
+
}
|
|
201
|
+
// Default path generator: ['entities', entityId, ...keyValues]
|
|
202
|
+
const keyValues = entityDef.keySchema.fields.map((field) => String(keys[field]));
|
|
203
|
+
return ['entities', entityDef.id, ...keyValues];
|
|
204
|
+
}
|
|
205
|
+
/**
|
|
206
|
+
* Select file node from state
|
|
207
|
+
*/
|
|
208
|
+
function selectFileNode(state, path, fsSliceName) {
|
|
209
|
+
const fsState = state[fsSliceName];
|
|
210
|
+
if (!fsState?.root)
|
|
211
|
+
return undefined;
|
|
212
|
+
const pathArray = Array.isArray(path) ? path : [path];
|
|
213
|
+
let currentNode = fsState.root;
|
|
214
|
+
for (const segment of pathArray) {
|
|
215
|
+
if (!currentNode || currentNode.type !== 'directory') {
|
|
216
|
+
return undefined;
|
|
217
|
+
}
|
|
218
|
+
currentNode = currentNode.children?.[segment];
|
|
219
|
+
}
|
|
220
|
+
return currentNode;
|
|
221
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { Middleware } from 'redux';
|
|
2
|
+
/**
|
|
3
|
+
* Entity sync middleware
|
|
4
|
+
* Stores entity keys in extension state when resource calls succeed
|
|
5
|
+
*/
|
|
6
|
+
export declare function createEntitySyncMiddleware<S = any>(): Middleware<{}, S>;
|
|
7
|
+
//# sourceMappingURL=entity-sync-middleware.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"entity-sync-middleware.d.ts","sourceRoot":"","sources":["../../src/middleware/entity-sync-middleware.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,OAAO,CAAC;AAOxC;;;GAGG;AACH,wBAAgB,0BAA0B,CAAC,CAAC,GAAG,GAAG,KAAK,UAAU,CAAC,EAAE,EAAE,CAAC,CAAC,CAWvE"}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { ResourceActionTypes, resourceAttributesUtil } from '@hamak/ui-remote-resource-api';
|
|
2
|
+
/**
|
|
3
|
+
* Entity sync middleware
|
|
4
|
+
* Stores entity keys in extension state when resource calls succeed
|
|
5
|
+
*/
|
|
6
|
+
export function createEntitySyncMiddleware() {
|
|
7
|
+
return (store) => (next) => (action) => {
|
|
8
|
+
const result = next(action);
|
|
9
|
+
// Listen for resource success actions that originated from entity actions
|
|
10
|
+
if (action.type === ResourceActionTypes.RESOURCE_CALL_SUCCESS) {
|
|
11
|
+
handleEntityResourceSuccess(store, action);
|
|
12
|
+
}
|
|
13
|
+
return result;
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
function handleEntityResourceSuccess(store, action) {
|
|
17
|
+
const { request } = action;
|
|
18
|
+
// Check if this resource action originated from an entity action
|
|
19
|
+
const entityContext = request.payload.params?._entityContext;
|
|
20
|
+
if (!entityContext) {
|
|
21
|
+
return; // Not an entity-originated action
|
|
22
|
+
}
|
|
23
|
+
const { entityId, keys, keyFields } = entityContext;
|
|
24
|
+
const { path } = action.payload;
|
|
25
|
+
// Create entity attributes
|
|
26
|
+
const entityAttrs = resourceAttributesUtil.createEntityAttributes(entityId, keys, keyFields);
|
|
27
|
+
// TODO: Entity attributes are no longer stored in file extension state
|
|
28
|
+
// The FileSystemAdapter doesn't support updating extension states directly
|
|
29
|
+
// Entity information is now tracked through the file content itself
|
|
30
|
+
// Future: Consider adding entity metadata support to FileSystemAdapter
|
|
31
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { Middleware } from 'redux';
|
|
2
|
+
import { type ResourceAction } from '@hamak/ui-remote-resource-api';
|
|
3
|
+
export interface ResourceMiddlewareConfig {
|
|
4
|
+
registry: any;
|
|
5
|
+
onError?: (error: any, action: ResourceAction) => void;
|
|
6
|
+
onSuccess?: (result: any, action: ResourceAction) => void;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Resource middleware
|
|
10
|
+
* Intercepts RESOURCE_CALL_REQUEST actions and executes API calls via providers
|
|
11
|
+
*/
|
|
12
|
+
export declare function createResourceMiddleware<S = any>(config: ResourceMiddlewareConfig): Middleware<{}, S>;
|
|
13
|
+
//# sourceMappingURL=resource-middleware.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"resource-middleware.d.ts","sourceRoot":"","sources":["../../src/middleware/resource-middleware.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,OAAO,CAAC;AACxC,OAAO,EAIL,KAAK,cAAc,EACpB,MAAM,+BAA+B,CAAC;AAGvC,MAAM,WAAW,wBAAwB;IACvC,QAAQ,EAAE,GAAG,CAAC;IACd,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,GAAG,EAAE,MAAM,EAAE,cAAc,KAAK,IAAI,CAAC;IACvD,SAAS,CAAC,EAAE,CAAC,MAAM,EAAE,GAAG,EAAE,MAAM,EAAE,cAAc,KAAK,IAAI,CAAC;CAC3D;AAED;;;GAGG;AACH,wBAAgB,wBAAwB,CAAC,CAAC,GAAG,GAAG,EAC9C,MAAM,EAAE,wBAAwB,GAC/B,UAAU,CAAC,EAAE,EAAE,CAAC,CAAC,CA6EnB"}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { ResourceActionTypes, ResourceActionFactory } from '@hamak/ui-remote-resource-api';
|
|
2
|
+
/**
|
|
3
|
+
* Resource middleware
|
|
4
|
+
* Intercepts RESOURCE_CALL_REQUEST actions and executes API calls via providers
|
|
5
|
+
*/
|
|
6
|
+
export function createResourceMiddleware(config) {
|
|
7
|
+
const { registry, onError, onSuccess } = config;
|
|
8
|
+
const actionFactory = new ResourceActionFactory();
|
|
9
|
+
return (store) => (next) => async (action) => {
|
|
10
|
+
// Only handle RESOURCE_CALL_REQUEST actions
|
|
11
|
+
if (action.type !== ResourceActionTypes.RESOURCE_CALL_REQUEST) {
|
|
12
|
+
return next(action);
|
|
13
|
+
}
|
|
14
|
+
// Continue action flow (for sync middleware to set loading state)
|
|
15
|
+
next(action);
|
|
16
|
+
const requestAction = action;
|
|
17
|
+
const { endpointId, operation, params, path } = requestAction.payload;
|
|
18
|
+
try {
|
|
19
|
+
// Lookup endpoint
|
|
20
|
+
const endpointDef = registry.getEndpoint(endpointId);
|
|
21
|
+
if (!endpointDef) {
|
|
22
|
+
throw new Error(`Endpoint not found: ${endpointId}`);
|
|
23
|
+
}
|
|
24
|
+
// Get provider
|
|
25
|
+
const provider = registry.getProvider(endpointDef.providerType);
|
|
26
|
+
if (!provider) {
|
|
27
|
+
throw new Error(`Provider not found: ${endpointDef.providerType}`);
|
|
28
|
+
}
|
|
29
|
+
// Map action to call params (mapper receives entire action)
|
|
30
|
+
const callParams = endpointDef.payloadMapper
|
|
31
|
+
? endpointDef.payloadMapper(requestAction)
|
|
32
|
+
: { params }; // Default: just use params
|
|
33
|
+
// Execute provider call
|
|
34
|
+
const result = await provider.call(endpointDef.endpoint, callParams, operation);
|
|
35
|
+
// Transform response (transformer receives entire action)
|
|
36
|
+
const transformedData = endpointDef.responseTransformer
|
|
37
|
+
? endpointDef.responseTransformer(result.data, result.metadata, requestAction)
|
|
38
|
+
: result.data;
|
|
39
|
+
// Determine storage path
|
|
40
|
+
const storagePath = determinePath(path, endpointDef, params, operation);
|
|
41
|
+
// Dispatch success action
|
|
42
|
+
const successAction = actionFactory.callSuccess(endpointId, storagePath, transformedData, result.metadata, requestAction);
|
|
43
|
+
store.dispatch(successAction);
|
|
44
|
+
onSuccess?.(result, requestAction);
|
|
45
|
+
}
|
|
46
|
+
catch (error) {
|
|
47
|
+
// Determine storage path for error state
|
|
48
|
+
const endpointDef = registry.getEndpoint(endpointId);
|
|
49
|
+
const storagePath = determinePath(path, endpointDef, params, operation);
|
|
50
|
+
// Dispatch failure action
|
|
51
|
+
const failureAction = actionFactory.callFailure(endpointId, storagePath, normalizeError(error), requestAction);
|
|
52
|
+
store.dispatch(failureAction);
|
|
53
|
+
onError?.(error, requestAction);
|
|
54
|
+
}
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Determine the file path where resource should be stored
|
|
59
|
+
*/
|
|
60
|
+
function determinePath(explicitPath, endpointDef, params, operation) {
|
|
61
|
+
// 1. Use explicit path from action if provided
|
|
62
|
+
if (explicitPath) {
|
|
63
|
+
return explicitPath;
|
|
64
|
+
}
|
|
65
|
+
// 2. Use endpoint's default folder + generate filename
|
|
66
|
+
if (endpointDef?.defaultFolder) {
|
|
67
|
+
const folder = Array.isArray(endpointDef.defaultFolder)
|
|
68
|
+
? endpointDef.defaultFolder
|
|
69
|
+
: [endpointDef.defaultFolder];
|
|
70
|
+
// Generate filename from params or operation
|
|
71
|
+
const filename = generateFilename(params, operation);
|
|
72
|
+
return [...folder, filename];
|
|
73
|
+
}
|
|
74
|
+
// 3. Fallback to root with generated filename
|
|
75
|
+
return [generateFilename(params, operation)];
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Generate filename from params or operation
|
|
79
|
+
*/
|
|
80
|
+
function generateFilename(params, operation) {
|
|
81
|
+
// If params has an id field, use it
|
|
82
|
+
if (params?.id) {
|
|
83
|
+
return String(params.id);
|
|
84
|
+
}
|
|
85
|
+
// Otherwise generate based on operation + timestamp
|
|
86
|
+
return `${operation}-${Date.now()}`;
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Normalize error to consistent format
|
|
90
|
+
*/
|
|
91
|
+
function normalizeError(error) {
|
|
92
|
+
return {
|
|
93
|
+
message: error.message || 'Unknown error',
|
|
94
|
+
code: error.code || error.response?.status?.toString(),
|
|
95
|
+
details: error.response?.data || error
|
|
96
|
+
};
|
|
97
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { Middleware } from 'redux';
|
|
2
|
+
import type { FileSystemNodeActions } from '@hamak/ui-store-impl';
|
|
3
|
+
export interface SyncMiddlewareConfig {
|
|
4
|
+
/** FileSystem actions from FileSystemAdapter */
|
|
5
|
+
fsActions: FileSystemNodeActions;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Sync middleware
|
|
9
|
+
* Updates file system state when resource actions complete
|
|
10
|
+
*/
|
|
11
|
+
export declare function createSyncMiddleware<S = any>(config: SyncMiddlewareConfig): Middleware<{}, S>;
|
|
12
|
+
//# sourceMappingURL=sync-middleware.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sync-middleware.d.ts","sourceRoot":"","sources":["../../src/middleware/sync-middleware.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,OAAO,CAAC;AAOxC,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,sBAAsB,CAAC;AAGlE,MAAM,WAAW,oBAAoB;IACnC,gDAAgD;IAChD,SAAS,EAAE,qBAAqB,CAAC;CAClC;AAED;;;GAGG;AACH,wBAAgB,oBAAoB,CAAC,CAAC,GAAG,GAAG,EAC1C,MAAM,EAAE,oBAAoB,GAC3B,UAAU,CAAC,EAAE,EAAE,CAAC,CAAC,CAwBnB"}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { ResourceActionTypes } from '@hamak/ui-remote-resource-api';
|
|
2
|
+
import { Pathway } from '@hamak/navigation-utils';
|
|
3
|
+
/**
|
|
4
|
+
* Sync middleware
|
|
5
|
+
* Updates file system state when resource actions complete
|
|
6
|
+
*/
|
|
7
|
+
export function createSyncMiddleware(config) {
|
|
8
|
+
const { fsActions } = config;
|
|
9
|
+
return (store) => (next) => (action) => {
|
|
10
|
+
// Pass through all actions first
|
|
11
|
+
const result = next(action);
|
|
12
|
+
// Handle request action (set loading state)
|
|
13
|
+
if (action.type === ResourceActionTypes.RESOURCE_CALL_REQUEST) {
|
|
14
|
+
handleRequestAction(store, action, fsActions);
|
|
15
|
+
}
|
|
16
|
+
// Handle success action (update content)
|
|
17
|
+
if (action.type === ResourceActionTypes.RESOURCE_CALL_SUCCESS) {
|
|
18
|
+
handleSuccessAction(store, action, fsActions);
|
|
19
|
+
}
|
|
20
|
+
// Handle failure action (set error state)
|
|
21
|
+
if (action.type === ResourceActionTypes.RESOURCE_CALL_FAILURE) {
|
|
22
|
+
handleFailureAction(store, action, fsActions);
|
|
23
|
+
}
|
|
24
|
+
return result;
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
function handleRequestAction(store, action, fsActions) {
|
|
28
|
+
const { path } = action.payload;
|
|
29
|
+
if (!path) {
|
|
30
|
+
// Loading state will be set when success/failure with resolved path
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
// Normalize path using Pathway utility
|
|
34
|
+
const pathway = Pathway.of(path);
|
|
35
|
+
const parentPathway = pathway.getParent();
|
|
36
|
+
const parentSegments = parentPathway.getSegments();
|
|
37
|
+
// Ensure parent directory exists
|
|
38
|
+
if (parentSegments.length > 0) {
|
|
39
|
+
store.dispatch(fsActions.mkdir(parentSegments, true));
|
|
40
|
+
}
|
|
41
|
+
// Set loading state by creating file node with contentIsPresent=false
|
|
42
|
+
store.dispatch(fsActions.setFile(path, null, 'xs:any', { override: false, contentIsPresent: false }));
|
|
43
|
+
}
|
|
44
|
+
function handleSuccessAction(store, action, fsActions) {
|
|
45
|
+
const { path, data } = action.payload;
|
|
46
|
+
const { request } = action;
|
|
47
|
+
// Normalize path using Pathway utility
|
|
48
|
+
const pathway = Pathway.of(path);
|
|
49
|
+
const parentPathway = pathway.getParent();
|
|
50
|
+
const parentSegments = parentPathway.getSegments();
|
|
51
|
+
// Ensure parent directory exists
|
|
52
|
+
if (parentSegments.length > 0) {
|
|
53
|
+
store.dispatch(fsActions.mkdir(parentSegments, true));
|
|
54
|
+
}
|
|
55
|
+
// Set file content from remote (automatically sets contentLoaded=true and clears loading)
|
|
56
|
+
// Schema should come from endpoint definition if available
|
|
57
|
+
const schema = request.meta?.schema || 'xs:any';
|
|
58
|
+
// First ensure file exists with proper schema
|
|
59
|
+
store.dispatch(fsActions.setFile(path, data, schema, { override: true, contentIsPresent: true }));
|
|
60
|
+
// Then set content from remote to mark it as loaded from remote source
|
|
61
|
+
store.dispatch(fsActions.setFileContent(path, data, true));
|
|
62
|
+
}
|
|
63
|
+
function handleFailureAction(store, action, fsActions) {
|
|
64
|
+
const { path, error } = action.payload;
|
|
65
|
+
if (!path) {
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
// Normalize path using Pathway utility
|
|
69
|
+
const pathway = Pathway.of(path);
|
|
70
|
+
const parentPathway = pathway.getParent();
|
|
71
|
+
const parentSegments = parentPathway.getSegments();
|
|
72
|
+
// Ensure parent directory exists
|
|
73
|
+
if (parentSegments.length > 0) {
|
|
74
|
+
store.dispatch(fsActions.mkdir(parentSegments, true));
|
|
75
|
+
}
|
|
76
|
+
// For now, just ensure the file exists and set content to null/error placeholder
|
|
77
|
+
// TODO: FileSystemAdapter doesn't currently support setting error state directly
|
|
78
|
+
// We might need to extend it or handle errors differently
|
|
79
|
+
store.dispatch(fsActions.setFile(path, { error }, 'xs:any', { override: true, contentIsPresent: true }));
|
|
80
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { ResourcePluginConfig } from '@hamak/ui-remote-resource-spi';
|
|
2
|
+
/**
|
|
3
|
+
* Microkernel plugin module interface
|
|
4
|
+
*/
|
|
5
|
+
export interface PluginModule {
|
|
6
|
+
initialize(ctx: InitializationContext): Promise<void>;
|
|
7
|
+
activate(ctx: ActivateContext): Promise<void>;
|
|
8
|
+
deactivate?(): Promise<void>;
|
|
9
|
+
}
|
|
10
|
+
interface InitializationContext {
|
|
11
|
+
provide(provider: any): void;
|
|
12
|
+
[key: string]: any;
|
|
13
|
+
}
|
|
14
|
+
interface ActivateContext {
|
|
15
|
+
resolve(token: any): any;
|
|
16
|
+
hooks: {
|
|
17
|
+
emit(event: string, data?: any): void;
|
|
18
|
+
};
|
|
19
|
+
[key: string]: any;
|
|
20
|
+
}
|
|
21
|
+
export declare const RESOURCE_REGISTRY_TOKEN = "RESOURCE_REGISTRY";
|
|
22
|
+
export declare const ENTITY_REGISTRY_TOKEN = "ENTITY_REGISTRY";
|
|
23
|
+
/**
|
|
24
|
+
* Create remote resource plugin
|
|
25
|
+
*
|
|
26
|
+
* @param config - Plugin configuration
|
|
27
|
+
* @returns Plugin module for microkernel registration
|
|
28
|
+
*/
|
|
29
|
+
export declare function createResourcePlugin(config?: ResourcePluginConfig): PluginModule;
|
|
30
|
+
export {};
|
|
31
|
+
//# sourceMappingURL=resource-plugin-factory.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"resource-plugin-factory.d.ts","sourceRoot":"","sources":["../../src/plugin/resource-plugin-factory.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,+BAA+B,CAAC;AAgB1E;;GAEG;AACH,MAAM,WAAW,YAAY;IAC3B,UAAU,CAAC,GAAG,EAAE,qBAAqB,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACtD,QAAQ,CAAC,GAAG,EAAE,eAAe,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC9C,UAAU,CAAC,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CAC9B;AAED,UAAU,qBAAqB;IAC7B,OAAO,CAAC,QAAQ,EAAE,GAAG,GAAG,IAAI,CAAC;IAC7B,CAAC,GAAG,EAAE,MAAM,GAAG,GAAG,CAAC;CACpB;AAED,UAAU,eAAe;IACvB,OAAO,CAAC,KAAK,EAAE,GAAG,GAAG,GAAG,CAAC;IACzB,KAAK,EAAE;QACL,IAAI,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,GAAG,GAAG,IAAI,CAAC;KACvC,CAAC;IACF,CAAC,GAAG,EAAE,MAAM,GAAG,GAAG,CAAC;CACpB;AAGD,eAAO,MAAM,uBAAuB,sBAAsB,CAAC;AAC3D,eAAO,MAAM,qBAAqB,oBAAoB,CAAC;AAEvD;;;;;GAKG;AACH,wBAAgB,oBAAoB,CAClC,MAAM,GAAE,oBAAyB,GAChC,YAAY,CAgId"}
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import { MIDDLEWARE_REGISTRY_TOKEN } from '@hamak/ui-store-api';
|
|
2
|
+
import { FILESYSTEM_ADAPTER_TOKEN } from '@hamak/ui-store-impl';
|
|
3
|
+
import { ResourceRegistry } from '../registry/resource-registry';
|
|
4
|
+
import { EntityRegistry } from '../registry/entity-registry';
|
|
5
|
+
import { RestResourceProvider } from '../providers/rest-resource-provider';
|
|
6
|
+
import { MockResourceProvider } from '../providers/mock-resource-provider';
|
|
7
|
+
import { createResourceMiddleware } from '../middleware/resource-middleware';
|
|
8
|
+
import { createSyncMiddleware } from '../middleware/sync-middleware';
|
|
9
|
+
import { createEntityMiddleware } from '../middleware/entity-middleware';
|
|
10
|
+
import { createEntitySyncMiddleware } from '../middleware/entity-sync-middleware';
|
|
11
|
+
// Dependency injection tokens (for local use)
|
|
12
|
+
export const RESOURCE_REGISTRY_TOKEN = 'RESOURCE_REGISTRY';
|
|
13
|
+
export const ENTITY_REGISTRY_TOKEN = 'ENTITY_REGISTRY';
|
|
14
|
+
/**
|
|
15
|
+
* Create remote resource plugin
|
|
16
|
+
*
|
|
17
|
+
* @param config - Plugin configuration
|
|
18
|
+
* @returns Plugin module for microkernel registration
|
|
19
|
+
*/
|
|
20
|
+
export function createResourcePlugin(config = {}) {
|
|
21
|
+
const resourceRegistry = new ResourceRegistry();
|
|
22
|
+
const entityRegistry = new EntityRegistry();
|
|
23
|
+
return {
|
|
24
|
+
async initialize(ctx) {
|
|
25
|
+
// Register default providers
|
|
26
|
+
const restProvider = new RestResourceProvider();
|
|
27
|
+
const mockProvider = new MockResourceProvider();
|
|
28
|
+
resourceRegistry.registerProvider(restProvider);
|
|
29
|
+
resourceRegistry.registerProvider(mockProvider);
|
|
30
|
+
// Register custom providers
|
|
31
|
+
if (config.providers) {
|
|
32
|
+
config.providers.forEach((provider) => {
|
|
33
|
+
resourceRegistry.registerProvider(provider);
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
// Register endpoints
|
|
37
|
+
if (config.endpoints) {
|
|
38
|
+
config.endpoints.forEach((endpoint) => {
|
|
39
|
+
resourceRegistry.registerEndpoint(endpoint);
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
// Register entities
|
|
43
|
+
if (config.entities) {
|
|
44
|
+
config.entities.forEach((entity) => {
|
|
45
|
+
entityRegistry.registerEntity(entity);
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
// Provide registries via DI
|
|
49
|
+
ctx.provide({
|
|
50
|
+
provide: RESOURCE_REGISTRY_TOKEN,
|
|
51
|
+
useValue: resourceRegistry
|
|
52
|
+
});
|
|
53
|
+
ctx.provide({
|
|
54
|
+
provide: ENTITY_REGISTRY_TOKEN,
|
|
55
|
+
useValue: entityRegistry
|
|
56
|
+
});
|
|
57
|
+
// Register middleware during initialization
|
|
58
|
+
try {
|
|
59
|
+
// Access resolve method if available on context
|
|
60
|
+
const resolve = ctx.resolve;
|
|
61
|
+
if (resolve) {
|
|
62
|
+
// Resolve FileSystemAdapter from DI
|
|
63
|
+
const fsAdapter = resolve(FILESYSTEM_ADAPTER_TOKEN);
|
|
64
|
+
if (!fsAdapter) {
|
|
65
|
+
console.warn('[ui-remote-resource] FileSystemAdapter not available in DI');
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
const fsActions = fsAdapter.getActions();
|
|
69
|
+
const fsSliceName = fsAdapter.sliceName;
|
|
70
|
+
// Create middleware with FileSystemAdapter actions
|
|
71
|
+
const entityMiddleware = createEntityMiddleware({
|
|
72
|
+
entityRegistry,
|
|
73
|
+
fsSliceName,
|
|
74
|
+
onError: config.onError
|
|
75
|
+
});
|
|
76
|
+
const resourceMiddleware = createResourceMiddleware({
|
|
77
|
+
registry: resourceRegistry,
|
|
78
|
+
onError: config.onError,
|
|
79
|
+
onSuccess: config.onSuccess
|
|
80
|
+
});
|
|
81
|
+
const entitySyncMiddleware = createEntitySyncMiddleware();
|
|
82
|
+
const resourceSyncMiddleware = createSyncMiddleware({
|
|
83
|
+
fsActions
|
|
84
|
+
});
|
|
85
|
+
const middlewareRegistry = resolve(MIDDLEWARE_REGISTRY_TOKEN);
|
|
86
|
+
middlewareRegistry.register({
|
|
87
|
+
id: 'entity-middleware',
|
|
88
|
+
middleware: entityMiddleware,
|
|
89
|
+
priority: 110,
|
|
90
|
+
plugin: 'ui-remote-resource'
|
|
91
|
+
});
|
|
92
|
+
middlewareRegistry.register({
|
|
93
|
+
id: 'resource-middleware',
|
|
94
|
+
middleware: resourceMiddleware,
|
|
95
|
+
priority: 100,
|
|
96
|
+
plugin: 'ui-remote-resource'
|
|
97
|
+
});
|
|
98
|
+
middlewareRegistry.register({
|
|
99
|
+
id: 'entity-sync-middleware',
|
|
100
|
+
middleware: entitySyncMiddleware,
|
|
101
|
+
priority: 60,
|
|
102
|
+
plugin: 'ui-remote-resource'
|
|
103
|
+
});
|
|
104
|
+
middlewareRegistry.register({
|
|
105
|
+
id: 'resource-sync-middleware',
|
|
106
|
+
middleware: resourceSyncMiddleware,
|
|
107
|
+
priority: 50,
|
|
108
|
+
plugin: 'ui-remote-resource'
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
else {
|
|
112
|
+
console.warn('Resolve method not available in initialization context');
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
catch (error) {
|
|
116
|
+
console.warn('Failed to register middleware during initialization:', error);
|
|
117
|
+
}
|
|
118
|
+
},
|
|
119
|
+
async activate(ctx) {
|
|
120
|
+
// Emit ready event
|
|
121
|
+
ctx.hooks?.emit('ui-remote-resource:ready', {
|
|
122
|
+
resourceRegistry,
|
|
123
|
+
entityRegistry
|
|
124
|
+
});
|
|
125
|
+
},
|
|
126
|
+
async deactivate() {
|
|
127
|
+
// Cleanup if needed
|
|
128
|
+
// For now, no cleanup required
|
|
129
|
+
}
|
|
130
|
+
};
|
|
131
|
+
}
|