@byline/core 4.2.0 → 4.4.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/dist/@types/admin-types.d.ts +26 -9
- package/dist/@types/admin-types.js +4 -2
- package/dist/@types/collection-types.d.ts +9 -3
- package/dist/config/attach-hooks.js +19 -24
- package/dist/config/routes.test.node.js +1 -7
- package/dist/config/validate-admin-configs.d.ts +8 -5
- package/dist/config/validate-admin-configs.js +76 -45
- package/dist/config/validate-admin-configs.test.node.js +93 -3
- package/dist/config/validate-collections.js +63 -25
- package/dist/config/validate-collections.test.node.js +63 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/patches/apply-patches.js +20 -29
- package/dist/patches/patch.test.node.js +49 -1
- package/dist/paths/index.d.ts +16 -0
- package/dist/paths/index.js +15 -0
- package/dist/paths/parse-path.d.ts +44 -0
- package/dist/paths/parse-path.js +133 -0
- package/dist/paths/path-dialects.test.node.d.ts +8 -0
- package/dist/paths/path-dialects.test.node.js +267 -0
- package/dist/paths/path-types.d.ts +86 -0
- package/dist/paths/path-types.js +8 -0
- package/dist/paths/paths.test.node.d.ts +8 -0
- package/dist/paths/paths.test.node.js +318 -0
- package/dist/paths/resolve-path.d.ts +47 -0
- package/dist/paths/resolve-path.js +116 -0
- package/dist/schemas/zod/builder.js +26 -2
- package/dist/schemas/zod/builder.test.node.js +72 -0
- package/dist/services/assign-counter-values.js +9 -3
- package/dist/storage/collection-fingerprint.test.node.js +1 -4
- package/package.json +6 -6
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* This Source Code is subject to the terms of the Mozilla Public
|
|
3
|
+
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
4
|
+
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
|
5
|
+
*
|
|
6
|
+
* Copyright (c) Infonomic Company Limited
|
|
7
|
+
*/
|
|
8
|
+
import { describe, expect, it } from 'vitest';
|
|
9
|
+
import { prepareHookAttachment } from '../config/attach-hooks.js';
|
|
10
|
+
import { validateAdminConfigs, validateBlockAdminConfigs, } from '../config/validate-admin-configs.js';
|
|
11
|
+
import { validateCollections } from '../config/validate-collections.js';
|
|
12
|
+
import { parsePatchPath } from '../patches/apply-patches.js';
|
|
13
|
+
// ---------------------------------------------------------------------------
|
|
14
|
+
// Characterization tests for Byline's field-path notations.
|
|
15
|
+
//
|
|
16
|
+
// Several subsystems address fields by dotted path, and they do NOT all use
|
|
17
|
+
// the same grammar. This file pins the CURRENT behaviour of each so that the
|
|
18
|
+
// reconciliation work (see PATH-GRAMMAR-ANALYSIS-AND-RECONCILIATION.md) either
|
|
19
|
+
// preserves it or changes it visibly and on purpose.
|
|
20
|
+
//
|
|
21
|
+
// The organising model — the two categories these dialects fall into:
|
|
22
|
+
//
|
|
23
|
+
// * INSTANCE paths address a value in one item of one document. Indices are
|
|
24
|
+
// required. The block type is redundant: the item carries its own `_type`.
|
|
25
|
+
// * DECLARATION paths address a field declaration in the schema. There are
|
|
26
|
+
// no indices. The block type IS required — without it, two blocks in the
|
|
27
|
+
// same field that declare the same field name are indistinguishable.
|
|
28
|
+
//
|
|
29
|
+
// The dialects do not all agree yet. Where one is deliberately narrower than
|
|
30
|
+
// the grammar (admin `fields{}` barring block traversal) the comment says so;
|
|
31
|
+
// where one is still being brought into line, it cites the phase that moves
|
|
32
|
+
// it. A failure here means a dialect changed — check it was on purpose.
|
|
33
|
+
// ---------------------------------------------------------------------------
|
|
34
|
+
/**
|
|
35
|
+
* One fixture, exercising every structural combination the dialects differ
|
|
36
|
+
* over: array→group nesting, a blocks field, an array inside a block, an
|
|
37
|
+
* upload field inside a block, and — critically — two block types that each
|
|
38
|
+
* declare a field named `alt`, which is what makes an unqualified path
|
|
39
|
+
* ambiguous.
|
|
40
|
+
*/
|
|
41
|
+
const fixture = (overrides = {}) => ({
|
|
42
|
+
path: 'pages',
|
|
43
|
+
labels: { singular: 'Page', plural: 'Pages' },
|
|
44
|
+
useAsTitle: 'title',
|
|
45
|
+
fields: [
|
|
46
|
+
{ name: 'title', label: 'Title', type: 'text' },
|
|
47
|
+
{
|
|
48
|
+
name: 'files',
|
|
49
|
+
label: 'Files',
|
|
50
|
+
type: 'array',
|
|
51
|
+
fields: [
|
|
52
|
+
{
|
|
53
|
+
name: 'filesGroup',
|
|
54
|
+
type: 'group',
|
|
55
|
+
fields: [
|
|
56
|
+
{ name: 'publicationFile', label: 'File', type: 'file', upload: {} },
|
|
57
|
+
{ name: 'caption', label: 'Caption', type: 'text' },
|
|
58
|
+
],
|
|
59
|
+
},
|
|
60
|
+
],
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
name: 'content',
|
|
64
|
+
label: 'Content',
|
|
65
|
+
type: 'blocks',
|
|
66
|
+
blocks: [
|
|
67
|
+
{
|
|
68
|
+
blockType: 'photoBlock',
|
|
69
|
+
fields: [
|
|
70
|
+
{ name: 'display', label: 'Display', type: 'text' },
|
|
71
|
+
{
|
|
72
|
+
name: 'gallery',
|
|
73
|
+
label: 'Gallery',
|
|
74
|
+
type: 'array',
|
|
75
|
+
fields: [
|
|
76
|
+
{
|
|
77
|
+
name: 'alt',
|
|
78
|
+
label: 'Alt',
|
|
79
|
+
type: 'text',
|
|
80
|
+
...(overrides.altVirtual === true ? { virtual: true } : {}),
|
|
81
|
+
},
|
|
82
|
+
{ name: 'heroImage', label: 'Hero', type: 'image', upload: {} },
|
|
83
|
+
],
|
|
84
|
+
},
|
|
85
|
+
],
|
|
86
|
+
},
|
|
87
|
+
// Same leaf name (`alt`) as photoBlock's gallery child. This is the
|
|
88
|
+
// collision that an unqualified declaration path cannot resolve.
|
|
89
|
+
{
|
|
90
|
+
blockType: 'videoBlock',
|
|
91
|
+
fields: [{ name: 'alt', label: 'Alt', type: 'text' }],
|
|
92
|
+
},
|
|
93
|
+
],
|
|
94
|
+
},
|
|
95
|
+
],
|
|
96
|
+
});
|
|
97
|
+
// ---------------------------------------------------------------------------
|
|
98
|
+
// Dialect 1 — upload hook registry keys (DECLARATION paths)
|
|
99
|
+
//
|
|
100
|
+
// Produced by `indexUploadFields` (config/attach-hooks.ts), consumed as the
|
|
101
|
+
// keys of `ServerConfig.hooks.uploads`. Collection-prefixed, index-free, and
|
|
102
|
+
// block types are named explicitly. This is the only dialect that gets
|
|
103
|
+
// declaration paths fully right today.
|
|
104
|
+
// ---------------------------------------------------------------------------
|
|
105
|
+
describe('path dialect — upload hook registry keys', () => {
|
|
106
|
+
const register = (key) => () => prepareHookAttachment({
|
|
107
|
+
collections: [fixture()],
|
|
108
|
+
hooks: { uploads: { [key]: async () => ({}) } },
|
|
109
|
+
});
|
|
110
|
+
// Rejections assert on the message, not merely that something threw — a
|
|
111
|
+
// bare try/catch would let an unrelated failure masquerade as the
|
|
112
|
+
// path-resolution rejection under test.
|
|
113
|
+
const unresolved = /references unknown or non-upload field/;
|
|
114
|
+
it('addresses a nested upload field with a collection-prefixed declaration path', () => {
|
|
115
|
+
expect(register('pages.files.filesGroup.publicationFile')).not.toThrow();
|
|
116
|
+
});
|
|
117
|
+
it('qualifies a path through a blocks field with the block type', () => {
|
|
118
|
+
// The other half of the elision claim. `storage-paths.test.node.ts` in
|
|
119
|
+
// @byline/db-postgres derives this exact declaration path from real
|
|
120
|
+
// flattener output by dropping index segments; here the real registry
|
|
121
|
+
// accepts it. Neither package can import the other's half, so the two
|
|
122
|
+
// tests meet at this literal.
|
|
123
|
+
expect(register('pages.content.photoBlock.gallery.heroImage')).not.toThrow();
|
|
124
|
+
});
|
|
125
|
+
it('rejects the same path with the block type omitted', () => {
|
|
126
|
+
expect(register('pages.content.gallery.heroImage')).toThrow(unresolved);
|
|
127
|
+
});
|
|
128
|
+
it('rejects item indices — these address declarations, not instances', () => {
|
|
129
|
+
expect(register('pages.content.0.photoBlock.gallery.heroImage')).toThrow(unresolved);
|
|
130
|
+
});
|
|
131
|
+
});
|
|
132
|
+
// ---------------------------------------------------------------------------
|
|
133
|
+
// Dialect 2 — admin `fields{}` override keys (DECLARATION paths, blocks barred)
|
|
134
|
+
//
|
|
135
|
+
// Consumed by `resolveSchemaPath` (config/validate-admin-configs.ts). Same
|
|
136
|
+
// declaration-addressing as the registry, plus one deliberate policy: keys
|
|
137
|
+
// may not traverse a `blocks` field at all, because fields inside a block
|
|
138
|
+
// take their overrides from the blockType-keyed `blockAdmin` registry so that
|
|
139
|
+
// one registration applies wherever the block renders.
|
|
140
|
+
// ---------------------------------------------------------------------------
|
|
141
|
+
describe('path dialect — admin fields{} override keys', () => {
|
|
142
|
+
const admin = (key) => ({ slug: 'pages', fields: { [key]: {} } });
|
|
143
|
+
const validate = (key) => () => validateAdminConfigs([admin(key)], [fixture()]);
|
|
144
|
+
it('accepts a top-level field name', () => {
|
|
145
|
+
expect(validate('title')).not.toThrow();
|
|
146
|
+
});
|
|
147
|
+
it('accepts a declaration path through array and group structure', () => {
|
|
148
|
+
expect(validate('files.filesGroup.caption')).not.toThrow();
|
|
149
|
+
});
|
|
150
|
+
it('rejects an item index, naming the instance-path confusion', () => {
|
|
151
|
+
expect(validate('files[0].filesGroup.caption')).toThrow(/index-free schema paths/);
|
|
152
|
+
});
|
|
153
|
+
it('rejects traversal into a block even when correctly block-qualified', () => {
|
|
154
|
+
expect(validate('content.photoBlock.alt')).toThrow(/blockAdmin/);
|
|
155
|
+
});
|
|
156
|
+
it('rejects traversal into a block when unqualified', () => {
|
|
157
|
+
expect(validate('content.alt')).toThrow(/blockAdmin/);
|
|
158
|
+
});
|
|
159
|
+
it('rejects a path that walks through a value field', () => {
|
|
160
|
+
expect(validate('title.anything')).toThrow(/does not resolve to a field declaration/);
|
|
161
|
+
});
|
|
162
|
+
});
|
|
163
|
+
describe('path dialect — block admin fields{} override keys', () => {
|
|
164
|
+
const validate = (blockType, key) => () => validateBlockAdminConfigs([{ blockType, fields: { [key]: {} } }], [fixture()]);
|
|
165
|
+
it('accepts a top-level field name of the block', () => {
|
|
166
|
+
expect(validate('photoBlock', 'display')).not.toThrow();
|
|
167
|
+
});
|
|
168
|
+
it('accepts a declaration path through the block’s array structure', () => {
|
|
169
|
+
expect(validate('photoBlock', 'gallery.alt')).not.toThrow();
|
|
170
|
+
});
|
|
171
|
+
it('rejects a key that does not resolve within the block', () => {
|
|
172
|
+
expect(validate('photoBlock', 'gallery.missing')).toThrow(/does not resolve to a field declaration/);
|
|
173
|
+
});
|
|
174
|
+
it('resolves keys relative to the block root, not the collection', () => {
|
|
175
|
+
// `content.photoBlock.gallery.alt` is the collection-rooted path; from the
|
|
176
|
+
// block root the same field is just `gallery.alt`.
|
|
177
|
+
expect(validate('photoBlock', 'content.photoBlock.gallery.alt')).toThrow(/does not resolve to a field declaration/);
|
|
178
|
+
});
|
|
179
|
+
});
|
|
180
|
+
// ---------------------------------------------------------------------------
|
|
181
|
+
// Dialect 3 — boot-validation error message paths (DECLARATION paths)
|
|
182
|
+
//
|
|
183
|
+
// Produced by `walkFieldsWithPath` (config/validate-collections.ts) and
|
|
184
|
+
// interpolated into user-facing error messages. Now delegates to the shared
|
|
185
|
+
// grammar's `walkFieldDeclarations`, so it agrees with the upload registry.
|
|
186
|
+
// ---------------------------------------------------------------------------
|
|
187
|
+
describe('path dialect — boot-validation error message paths', () => {
|
|
188
|
+
/** The field path quoted in the virtual-field error, or null. */
|
|
189
|
+
const pathInError = () => {
|
|
190
|
+
try {
|
|
191
|
+
validateCollections([fixture({ altVirtual: true })]);
|
|
192
|
+
return null;
|
|
193
|
+
}
|
|
194
|
+
catch (error) {
|
|
195
|
+
const match = error.message.match(/virtual field "([^"]+)"/);
|
|
196
|
+
return match?.[1] ?? null;
|
|
197
|
+
}
|
|
198
|
+
};
|
|
199
|
+
it('qualifies the path with the block type', () => {
|
|
200
|
+
expect(pathInError()).toBe('content.photoBlock.gallery.alt');
|
|
201
|
+
});
|
|
202
|
+
it('identifies which block declaration is at fault', () => {
|
|
203
|
+
// The fixture declares `alt` in both photoBlock and videoBlock. Before the
|
|
204
|
+
// block type was carried through, both rendered as `content.alt` and the
|
|
205
|
+
// message could not say which declaration it meant.
|
|
206
|
+
expect(pathInError()).toContain('photoBlock');
|
|
207
|
+
});
|
|
208
|
+
});
|
|
209
|
+
// ---------------------------------------------------------------------------
|
|
210
|
+
// Dialect 4 — patch paths (INSTANCE paths)
|
|
211
|
+
//
|
|
212
|
+
// Parsed by `parsePatchPath` (patches/apply-patches.ts). Bracket indices, or
|
|
213
|
+
// `[id=…]` for stable item identity. Blocks are transparent: no block-type
|
|
214
|
+
// segment, because the addressed item carries its own `_type`.
|
|
215
|
+
// ---------------------------------------------------------------------------
|
|
216
|
+
describe('path dialect — patch paths', () => {
|
|
217
|
+
it('addresses array items by positional bracket index', () => {
|
|
218
|
+
expect(parsePatchPath('content[0].gallery[1].alt')).toEqual([
|
|
219
|
+
{ kind: 'field', key: 'content' },
|
|
220
|
+
{ kind: 'index', index: 0 },
|
|
221
|
+
{ kind: 'field', key: 'gallery' },
|
|
222
|
+
{ kind: 'index', index: 1 },
|
|
223
|
+
{ kind: 'field', key: 'alt' },
|
|
224
|
+
]);
|
|
225
|
+
});
|
|
226
|
+
it('addresses array items by stable id', () => {
|
|
227
|
+
expect(parsePatchPath('content[id=abc].gallery[id=def].alt')).toEqual([
|
|
228
|
+
{ kind: 'field', key: 'content' },
|
|
229
|
+
{ kind: 'id', id: 'abc' },
|
|
230
|
+
{ kind: 'field', key: 'gallery' },
|
|
231
|
+
{ kind: 'id', id: 'def' },
|
|
232
|
+
{ kind: 'field', key: 'alt' },
|
|
233
|
+
]);
|
|
234
|
+
});
|
|
235
|
+
it('carries no block-type segment — the item resolves its own type', () => {
|
|
236
|
+
const segments = parsePatchPath('content[0].alt');
|
|
237
|
+
expect(segments).not.toContainEqual({ kind: 'field', key: 'photoBlock' });
|
|
238
|
+
});
|
|
239
|
+
it('cannot consume a storage path — dotted indices become field names', () => {
|
|
240
|
+
// Storage emits `content.0.photoBlock.display`. Fed to the patch parser,
|
|
241
|
+
// the index and block type silently become field keys. Not currently
|
|
242
|
+
// reachable in product code, but nothing prevents it.
|
|
243
|
+
expect(parsePatchPath('content.0.photoBlock.display')).toEqual([
|
|
244
|
+
{ kind: 'field', key: 'content' },
|
|
245
|
+
{ kind: 'field', key: '0' },
|
|
246
|
+
{ kind: 'field', key: 'photoBlock' },
|
|
247
|
+
{ kind: 'field', key: 'display' },
|
|
248
|
+
]);
|
|
249
|
+
});
|
|
250
|
+
});
|
|
251
|
+
// ---------------------------------------------------------------------------
|
|
252
|
+
// Cross-dialect — the relationship the reconciliation is built on
|
|
253
|
+
// ---------------------------------------------------------------------------
|
|
254
|
+
describe('path dialects — cross-dialect relationships', () => {
|
|
255
|
+
it('registry keys and admin keys agree on non-block structure', () => {
|
|
256
|
+
// The same declaration, addressed by two dialects, differs only by the
|
|
257
|
+
// collection prefix — no divergence in how group/array nesting is spelled.
|
|
258
|
+
const registryKey = 'pages.files.filesGroup.publicationFile';
|
|
259
|
+
const adminKey = 'files.filesGroup.publicationFile';
|
|
260
|
+
expect(registryKey).toBe(`pages.${adminKey}`);
|
|
261
|
+
expect(() => prepareHookAttachment({
|
|
262
|
+
collections: [fixture()],
|
|
263
|
+
hooks: { uploads: { [registryKey]: async () => ({}) } },
|
|
264
|
+
})).not.toThrow();
|
|
265
|
+
expect(() => validateAdminConfigs([{ slug: 'pages', fields: { [adminKey]: {} } }], [fixture()])).not.toThrow();
|
|
266
|
+
});
|
|
267
|
+
});
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* This Source Code is subject to the terms of the Mozilla Public
|
|
3
|
+
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
4
|
+
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
|
5
|
+
*
|
|
6
|
+
* Copyright (c) Infonomic Company Limited
|
|
7
|
+
*/
|
|
8
|
+
import type { Field } from '../@types/index.js';
|
|
9
|
+
/**
|
|
10
|
+
* One step along a field path.
|
|
11
|
+
*
|
|
12
|
+
* `field` and `blockType` are indistinguishable in source text — both are
|
|
13
|
+
* bare identifiers. Parsing is schema-unaware and therefore produces only
|
|
14
|
+
* `field` segments; `resolveDeclarationPath` reclassifies them against a
|
|
15
|
+
* field set. Producers walking a schema (see `walkFieldDeclarations`) know
|
|
16
|
+
* the difference and emit the correct kind directly.
|
|
17
|
+
*/
|
|
18
|
+
export type PathSegment = {
|
|
19
|
+
readonly kind: 'field';
|
|
20
|
+
readonly name: string;
|
|
21
|
+
} | {
|
|
22
|
+
readonly kind: 'blockType';
|
|
23
|
+
readonly blockType: string;
|
|
24
|
+
} | {
|
|
25
|
+
readonly kind: 'index';
|
|
26
|
+
readonly index: number;
|
|
27
|
+
} | {
|
|
28
|
+
readonly kind: 'id';
|
|
29
|
+
readonly id: string;
|
|
30
|
+
};
|
|
31
|
+
/** Why a path string failed to parse. */
|
|
32
|
+
export type PathParseFailure =
|
|
33
|
+
/** The whole path was empty or whitespace. */
|
|
34
|
+
'empty'
|
|
35
|
+
/** A segment between dots was empty (`a..b`, `.a`, `a.`). */
|
|
36
|
+
| 'emptySegment'
|
|
37
|
+
/** A declaration path carried an item index (`files[0].caption`). */
|
|
38
|
+
| 'index'
|
|
39
|
+
/** Bracket syntax present but unparseable (`a[`, `a[]`, `a[x]`). */
|
|
40
|
+
| 'malformed';
|
|
41
|
+
export type PathParseResult = {
|
|
42
|
+
readonly ok: true;
|
|
43
|
+
readonly segments: readonly PathSegment[];
|
|
44
|
+
} | {
|
|
45
|
+
readonly ok: false;
|
|
46
|
+
readonly reason: PathParseFailure;
|
|
47
|
+
};
|
|
48
|
+
/**
|
|
49
|
+
* Outcome of resolving a declaration path against a field set.
|
|
50
|
+
*
|
|
51
|
+
* `blocks` is distinct from `unresolved` because the two warrant different
|
|
52
|
+
* guidance: a path that correctly names a block type but is used where block
|
|
53
|
+
* traversal is barred should point the author at the `blockAdmin` registry,
|
|
54
|
+
* not tell them their path is wrong.
|
|
55
|
+
*/
|
|
56
|
+
export type PathResolution =
|
|
57
|
+
/** Resolved to a field declaration. `segments` carry block types correctly classified. */
|
|
58
|
+
{
|
|
59
|
+
readonly status: 'ok';
|
|
60
|
+
readonly field: Field;
|
|
61
|
+
readonly segments: readonly PathSegment[];
|
|
62
|
+
}
|
|
63
|
+
/** Traversal reached a `blocks` field while `blocks: 'forbidden'` was in effect. */
|
|
64
|
+
| {
|
|
65
|
+
readonly status: 'blocks';
|
|
66
|
+
readonly at: number;
|
|
67
|
+
}
|
|
68
|
+
/** A segment named nothing, or a value field appeared mid-path. */
|
|
69
|
+
| {
|
|
70
|
+
readonly status: 'unresolved';
|
|
71
|
+
readonly at: number;
|
|
72
|
+
};
|
|
73
|
+
export interface ResolveOptions {
|
|
74
|
+
/**
|
|
75
|
+
* How to treat a `blocks` field encountered mid-path.
|
|
76
|
+
*
|
|
77
|
+
* - `'qualified'` (default) — descend, consuming the next segment as a
|
|
78
|
+
* block type. This is the grammar's normal behaviour, used by the upload
|
|
79
|
+
* hook registry.
|
|
80
|
+
* - `'forbidden'` — stop and report `status: 'blocks'`. Used by the admin
|
|
81
|
+
* `fields{}` maps, where per-field overrides inside a block belong to the
|
|
82
|
+
* blockType-keyed `blockAdmin` registry so that one registration applies
|
|
83
|
+
* wherever the block renders.
|
|
84
|
+
*/
|
|
85
|
+
readonly blocks?: 'qualified' | 'forbidden';
|
|
86
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* This Source Code is subject to the terms of the Mozilla Public
|
|
3
|
+
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
4
|
+
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
|
5
|
+
*
|
|
6
|
+
* Copyright (c) Infonomic Company Limited
|
|
7
|
+
*/
|
|
8
|
+
export {};
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* This Source Code is subject to the terms of the Mozilla Public
|
|
3
|
+
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
4
|
+
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
|
5
|
+
*
|
|
6
|
+
* Copyright (c) Infonomic Company Limited
|
|
7
|
+
*/
|
|
8
|
+
export {};
|