Passed
Push — trunk ( 903808...fb58d8 )
by Christian
15:46 queued 04:22
created

extension-api-data.service.ts ➔ parsePath   B

Complexity

Conditions 8

Size

Total Lines 21
Code Lines 15

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 8
eloc 15
dl 0
loc 21
rs 7.3333
c 0
b 0
f 0
1
/**
2
 * @package admin
3
 */
4
5
import type Vue from 'vue';
6
import { updateSubscriber, register, handleGet } from '@shopware-ag/admin-extension-sdk/es/data';
7
import { get, debounce } from 'lodash';
8
import { selectData } from '@shopware-ag/admin-extension-sdk/es/data/_internals/selectData';
9
import MissingPrivilegesError from '@shopware-ag/admin-extension-sdk/es/privileges/missing-privileges-error';
10
11
type publishOptions = {
12
    id: string,
13
    path: string,
14
    scope: Vue,
15
}
16
17
type dataset = {
18
    id: string,
19
    scope: number,
20
    data: unknown
21
}
22
23
type transferObject = {
24
    [key: string|symbol]: unknown
25
}
26
27
type ParsedPath = {
28
    pathToLastSegment: string,
29
    lastSegment: string,
30
};
31
32
type vueWithUid = Partial<Vue> & { _uid: number };
33
34
// This is used by the Vue devtool extension plugin
35
let publishedDataSets: dataset[] = [];
36
37
handleGet((data, additionalOptions) => {
38
    // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
39
    const origin = additionalOptions?._event_?.origin;
40
    const registeredDataSet = publishedDataSets.find(s => s.id === data.id);
41
42
    if (!registeredDataSet) {
43
        return null;
44
    }
45
46
    const selectors = data.selectors;
47
48
    if (!selectors) {
49
        return registeredDataSet.data;
50
    }
51
52
    const selectedData = selectData(registeredDataSet.data, selectors, 'datasetGet', origin);
53
54
    if (selectedData instanceof MissingPrivilegesError) {
55
        console.error(selectedData);
56
    }
57
58
    return selectedData;
59
});
60
61
/**
62
 * Splits an object path like "foo.bar.buz" to "{ pathToLastSegment: 'foo.bar', lastSegment: 'buz' }".
63
 */
64
function parsePath(path :string): ParsedPath | null {
65
    if (!path.includes('.')) {
66
        return null;
67
    }
68
69
    const properties = path.split('.');
70
    const lastSegment = properties.pop();
71
    const pathToLastSegment = properties.join('.');
72
73
    if (lastSegment && lastSegment.length && pathToLastSegment && pathToLastSegment.length) {
74
        return {
75
            pathToLastSegment,
76
            lastSegment,
77
        };
78
    }
79
80
    return null;
81
}
82
83
// eslint-disable-next-line sw-deprecation-rules/private-feature-declarations
84
export function publishData({ id, path, scope }: publishOptions): void {
85
    const registeredDataSet = publishedDataSets.find(s => s.id === id);
86
87
    // Dataset registered from different scope? Prevent update.
88
    if (registeredDataSet && registeredDataSet.scope !== (scope as vueWithUid)._uid) {
89
        console.error(`The dataset id "${id}" you tried to publish is already registered.`);
90
91
        return;
92
    }
93
94
    // Dataset registered from same scope? Update.
95
    if (registeredDataSet && registeredDataSet.scope === (scope as vueWithUid)._uid) {
96
        // eslint-disable-next-line @typescript-eslint/no-empty-function
97
        register({ id: id, data: get(scope, path) }).catch(() => {});
98
99
        return;
100
    }
101
102
    // Create updateSubscriber which maps back changes from the app to Vue
103
    updateSubscriber(id, (value) => {
104
        // Null updates are not allowed
105
        if (!value) {
106
            return;
107
        }
108
109
        function setObject(transferObject: transferObject, prePath: string|null = null): void {
110
            if (typeof transferObject?.getIsDirty === 'function' && !transferObject.getIsDirty()) {
111
                return;
112
            }
113
114
            Object.keys(transferObject).forEach((property) => {
115
                let realPath : string;
116
                if (prePath) {
117
                    realPath = `${prePath}.${property}`;
118
                } else {
119
                    realPath = `${path}.${property}`;
120
                }
121
122
                const parsedPath = parsePath(realPath);
123
                if (parsedPath === null) {
124
                    return;
125
                }
126
127
                // @ts-expect-error
128
                // eslint-disable-next-line max-len
129
                if (Shopware.Utils.hasOwnProperty(transferObject[property], 'getDraft', this) && typeof transferObject[property].getDraft === 'function') {
130
                    setObject({ [property]: Shopware.Utils.object.cloneDeep(transferObject[property]) }, realPath);
131
132
                    return;
133
                }
134
135
                if (Array.isArray(transferObject[property])) {
136
                    (transferObject[property] as Array<unknown>).forEach((c, index) => {
137
                        setObject({ [index]: c }, realPath);
138
                    });
139
140
                    return;
141
                }
142
143
                scope.$set(
144
                    Shopware.Utils.object.get(scope, parsedPath.pathToLastSegment) as Vue,
145
                    parsedPath.lastSegment,
146
                    transferObject[property],
147
                );
148
            });
149
        }
150
151
        // @ts-expect-error
152
        if (typeof value.data?.getDraft === 'function') {
153
            setObject(value.data as transferObject);
154
155
            return;
156
        }
157
158
        if (Array.isArray(value.data)) {
159
            value.data.forEach((entry, index) => {
160
                if (entry === null || typeof entry !== 'object') {
161
                    return;
162
                }
163
164
                setObject({ [index]: entry as unknown });
165
            });
166
        } else if (typeof value.data === 'object') {
167
            setObject(value.data as transferObject);
168
169
            return;
170
        }
171
172
        // Vue.set does not resolve path's therefore we need to resolve to the last child property
173
        if (path.includes('.')) {
174
            const properties = path.split('.');
175
            const lastPath = properties.pop();
176
            const newPath = properties.join('.');
177
            if (!lastPath) {
178
                return;
179
            }
180
181
            scope.$set(Shopware.Utils.object.get(scope, newPath) as Vue, lastPath, value.data);
182
183
            return;
184
        }
185
186
        scope.$set(scope, path, value.data);
187
    });
188
189
    // Watch for Changes on the Reactive Vue property and automatically publish them
190
    const unwatch = scope.$watch(path, debounce((value: Vue) => {
191
        // const preparedValue = prepareValue(value);
192
193
        // eslint-disable-next-line @typescript-eslint/no-empty-function
194
        register({ id: id, data: value }).catch(() => {});
195
196
        const dataSet = publishedDataSets.find(set => set.id === id);
197
        if (dataSet) {
198
            dataSet.data = value;
199
200
            return;
201
        }
202
203
        publishedDataSets.push({
204
            id,
205
            data: value,
206
            scope: (scope as vueWithUid)._uid,
207
        });
208
    }, 750), {
209
        deep: true,
210
        immediate: true,
211
    });
212
213
    // Before the registering component gets destroyed, destroy the watcher and deregister the dataset
214
    scope.$once('hook:beforeDestroy', () => {
215
        publishedDataSets = publishedDataSets.filter(value => value.id !== id);
216
217
        unwatch();
218
    });
219
220
    // eslint-disable-next-line @typescript-eslint/no-empty-function
221
    register({ id: id, data: get(scope, path) }).catch(() => {});
222
}
223
224
// eslint-disable-next-line sw-deprecation-rules/private-feature-declarations
225
export function getPublishedDataSets(): dataset[] {
226
    return publishedDataSets;
227
}
228