@nascentdigital/funnel-cms 5.0.3 → 6.0.1

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.
@@ -169,7 +169,7 @@ export declare namespace PageClient {
169
169
  type Result = Schema.Page.Data.AsGroup;
170
170
  }
171
171
  namespace Get {
172
- type Args = (Query.ById | Query.ByDetails) & Query.Select;
172
+ type Args = Query.ById & Query.Select;
173
173
  type Result = Schema.Page.FlattenedData | undefined;
174
174
  }
175
175
  namespace Create {
@@ -6,7 +6,7 @@ export declare class ContentfulCollectionClient implements CollectionClient {
6
6
  readonly cms: IContentfulContentClient;
7
7
  constructor(cms: IContentfulContentClient);
8
8
  getOverview(args: CollectionClient.GetOverview.Args): Promise<CollectionClient.GetOverview.Result>;
9
- getItems(args: CollectionClient.GetItems.Args): Promise<CollectionClient.GetItems.Result>;
9
+ getItems({ language, ...args }: CollectionClient.GetItems.Args): Promise<CollectionClient.GetItems.Result>;
10
10
  find(args: CollectionClient.Find.Args): Promise<CollectionClient.Find.Result>;
11
11
  getGroup(args: CollectionClient.GetGroup.Args): Promise<ContentfulCollectionClient.GetGroup.Result>;
12
12
  get(args: CollectionClient.Get.Args): Promise<ContentfulCollectionClient.Get.Result>;
@@ -1,4 +1,15 @@
1
1
  "use strict";
2
+ var __rest = (this && this.__rest) || function (s, e) {
3
+ var t = {};
4
+ for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0)
5
+ t[p] = s[p];
6
+ if (s != null && typeof Object.getOwnPropertySymbols === "function")
7
+ for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) {
8
+ if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i]))
9
+ t[p[i]] = s[p[i]];
10
+ }
11
+ return t;
12
+ };
2
13
  Object.defineProperty(exports, "__esModule", { value: true });
3
14
  exports.ContentfulCollectionClient = void 0;
4
15
  const funnel_core_1 = require("@nascentdigital/funnel-core");
@@ -9,10 +20,68 @@ class ContentfulCollectionClient {
9
20
  this.cms = cms;
10
21
  }
11
22
  async getOverview(args) {
12
- throw new funnel_core_1.NotImplementedError();
23
+ // fetch all collections
24
+ const { cms } = this;
25
+ const { entries } = await cms.getEntries(Object.assign({ content_type: models_1.ContentfulCollectionData.contentType, version: args.version, select: getCollectionSelect(funnel_core_1.Schema.Collection.Data.AsRef.props) }, getCollectionQueryParams(args)));
26
+ // convert entries to collection data
27
+ const collectionsData = entries.map(entry => models_1.ContentfulCollectionData.deserialize(entry, funnel_core_1.Schema.Collection.Data.AsRef.props));
28
+ // group by type
29
+ const collectionsByType = new Map();
30
+ for (const data of collectionsData) {
31
+ // get group
32
+ let group = collectionsByType.get(data.type);
33
+ if (!group) {
34
+ collectionsByType.set(data.type, group = []);
35
+ }
36
+ // push to group
37
+ group.push(data);
38
+ }
39
+ // return in overview format
40
+ return funnel_core_1.Schema.Site.current().collections.map(({ type }) => {
41
+ var _a, _b;
42
+ return ({
43
+ type,
44
+ total: (_b = (_a = collectionsByType.get(type)) === null || _a === void 0 ? void 0 : _a.length) !== null && _b !== void 0 ? _b : 0
45
+ });
46
+ });
13
47
  }
14
- async getItems(args) {
15
- throw new funnel_core_1.NotImplementedError();
48
+ async getItems(_a) {
49
+ var { language } = _a, args = __rest(_a, ["language"]);
50
+ // fetch all collections
51
+ const { cms } = this;
52
+ const { entries } = await cms.getEntries(Object.assign({ content_type: models_1.ContentfulCollectionData.contentType, version: args.version, select: getCollectionSelect(funnel_core_1.Schema.Collection.Data.AsRef.props) }, getCollectionQueryParams(args)));
53
+ // convert entries to collection data
54
+ const collectionsData = entries.map(entry => models_1.ContentfulCollectionData.deserialize(entry, funnel_core_1.Schema.Collection.Data.AsRef.props));
55
+ // group by group id
56
+ const collectionsByGroupId = new Map();
57
+ for (const data of collectionsData) {
58
+ // get group
59
+ let group = collectionsByGroupId.get(data.groupId);
60
+ if (!group) {
61
+ collectionsByGroupId.set(data.groupId, group = []);
62
+ }
63
+ // push to group
64
+ group.push(data);
65
+ }
66
+ // return in correct format
67
+ return collectionsByGroupId.entries().map(([groupId, refs]) => {
68
+ var _a;
69
+ // find ref with specified language
70
+ const target = (_a = refs.find(ref => ref.language === language)) !== null && _a !== void 0 ? _a : refs[0];
71
+ // return item
72
+ return {
73
+ id: target.id,
74
+ groupId: target.groupId,
75
+ type: args.type,
76
+ name: target.name,
77
+ refs: refs.reduce((refs, ref) => {
78
+ // map ref
79
+ refs[ref.language] = ref;
80
+ // keep reducing
81
+ return refs;
82
+ }, {})
83
+ };
84
+ }).toArray();
16
85
  }
17
86
  async find(args) {
18
87
  throw new funnel_core_1.NotImplementedError();
@@ -59,10 +128,109 @@ class ContentfulCollectionClient {
59
128
  entryId: args.id,
60
129
  version: args.version
61
130
  });
131
+ // skip if there's no entry
132
+ if (!entry) {
133
+ return undefined;
134
+ }
135
+ // or return it
62
136
  return Object.assign({ data: models_1.ContentfulCollectionData.deserialize(entry) }, models_1.ContentfulCollectionData.deserializeRefs(references));
63
137
  }
64
138
  async create(args) {
65
- throw new funnel_core_1.NotImplementedError();
139
+ // create a new language of an existing collection
140
+ if (funnel_core_1.Schema.Collection.Data.AsNewLanguage.filter(args)) {
141
+ // fetch all collections in group
142
+ const groupResult = await this.getGroup({
143
+ version: 'draft',
144
+ type: args.type,
145
+ groupId: args.groupId,
146
+ language: args.language
147
+ });
148
+ // throw if group doesn't exist
149
+ if (!groupResult) {
150
+ throw new funnel_core_1.ArgumentError(`Collection group "${args.groupId}" no longer exists`);
151
+ }
152
+ // return existing collection if it's there
153
+ if (groupResult.collection) {
154
+ return groupResult;
155
+ }
156
+ // try to create page
157
+ try {
158
+ // create entry
159
+ const { group } = groupResult;
160
+ const collectionData = await this.cms.createEntry(models_1.ContentfulCollectionData.contentType, {
161
+ type: args.type,
162
+ brand: args.brand,
163
+ version: args.version,
164
+ groupId: args.groupId,
165
+ language: args.language,
166
+ name: group.name,
167
+ references: [],
168
+ tags: [],
169
+ fields: {},
170
+ state: 'draft',
171
+ errors: true
172
+ });
173
+ // return updated group
174
+ return {
175
+ group: Object.assign(Object.assign({}, group), { instances: [
176
+ ...group.instances,
177
+ models_1.ContentfulCollectionData.deserialize(collectionData, funnel_core_1.Schema.Collection.Data.AsGroup.Instance.props)
178
+ ] }),
179
+ collection: {
180
+ data: models_1.ContentfulCollectionData.deserialize(collectionData),
181
+ files: {},
182
+ collections: {},
183
+ pages: {}
184
+ }
185
+ };
186
+ }
187
+ // or throw
188
+ catch (e) {
189
+ throw funnel_core_1.IsometricErrorUtils.coerceError(e);
190
+ }
191
+ }
192
+ // or create a net-new collection
193
+ else {
194
+ // try to create
195
+ try {
196
+ // create entry
197
+ const collectionData = await this.cms.createEntry(models_1.ContentfulCollectionData.contentType, {
198
+ type: args.type,
199
+ brand: args.brand,
200
+ version: args.version,
201
+ groupId: funnel_core_1.Value.uuid(),
202
+ language: args.language,
203
+ name: args.name,
204
+ references: [],
205
+ tags: [],
206
+ fields: {},
207
+ state: 'draft',
208
+ errors: true
209
+ });
210
+ // return updated group
211
+ return {
212
+ group: {
213
+ brand: collectionData.brand,
214
+ type: collectionData.type,
215
+ groupId: collectionData.groupId,
216
+ name: collectionData.name,
217
+ instances: [
218
+ models_1.ContentfulCollectionData.deserialize(collectionData, funnel_core_1.Schema.Collection.Data.AsGroup.Instance.props)
219
+ ]
220
+ },
221
+ collection: {
222
+ data: models_1.ContentfulCollectionData.deserialize(collectionData),
223
+ files: {},
224
+ collections: {},
225
+ pages: {}
226
+ }
227
+ };
228
+ }
229
+ // or throw
230
+ catch (e) {
231
+ throw funnel_core_1.IsometricErrorUtils.coerceError(e);
232
+ }
233
+ }
66
234
  }
67
235
  async duplicate(args) {
68
236
  throw new funnel_core_1.NotImplementedError();
@@ -33,7 +33,7 @@ export declare class ContentfulContentClient implements ContentClient, IContentf
33
33
  set token(token: AuthClient.AccessToken);
34
34
  clearToken(): void;
35
35
  createEntry<T extends ContentfulData>(contentTypeId: string, entry: ContentfulData.Draft<T>): Promise<T>;
36
- getEntry<TEntry extends ContentfulData>({ entryId, includeLinks, version }: IContentfulContentClient.GetEntry.Args<TEntry>): Promise<IContentfulContentClient.GetEntry.Result<TEntry>>;
36
+ getEntry<TEntry extends ContentfulData>({ entryId, includeLinks, version, ...query }: IContentfulContentClient.GetEntry.Args<TEntry>): Promise<IContentfulContentClient.GetEntry.Result<TEntry>>;
37
37
  getEntries<TEntry extends ContentfulData>({ pagination, version, includeLinks, ...query }: IContentfulContentClient.GetEntries.Args<TEntry>): Promise<IContentfulContentClient.GetEntries.Result<TEntry>>;
38
38
  uploadAsset(args: IContentfulContentClient.UploadAsset.Args): Promise<IContentfulContentClient.UploadAsset.Result>;
39
39
  static forEditor(sdk: KnownAppSDK): Promise<ContentfulContentClient>;
@@ -88,23 +88,22 @@ class ContentfulContentClient {
88
88
  // transform + return new entry
89
89
  return models_2.ContentfulData.fromCmaData(data, locale);
90
90
  }
91
- async getEntry({ entryId, includeLinks = false, version = 'draft' }) {
92
- var _a, _b;
91
+ async getEntry(_a) {
92
+ var _b, _c;
93
+ var { entryId, includeLinks = false, version = 'draft' } = _a, query = __rest(_a, ["entryId", "includeLinks", "version"]);
93
94
  // get entry
94
95
  const client = version === 'published' ? this.cda : this.cpa;
95
96
  const result = await client.entry.getMany({
96
- query: {
97
- 'sys.id': entryId,
98
- include: includeLinks === true
97
+ query: Object.assign(Object.assign({ 'sys.id': entryId }, query), { include: includeLinks === true
99
98
  ? 10
100
- : includeLinks || undefined
101
- }
99
+ : includeLinks || undefined })
102
100
  });
103
101
  const { items: [item], includes } = result;
104
102
  console.debug('[ContentfulContentClient/getEntry] fetched', result);
103
+ // skip
105
104
  // bind references
106
- const entry = models_2.ContentfulData.fromCdaData(item);
107
- const references = (_b = (_a = includes === null || includes === void 0 ? void 0 : includes.Entry) === null || _a === void 0 ? void 0 : _a.map(link => models_2.ContentfulData.fromCdaData(link))) !== null && _b !== void 0 ? _b : [];
105
+ const entry = item ? models_2.ContentfulData.fromCdaData(item) : undefined;
106
+ const references = (_c = (_b = includes === null || includes === void 0 ? void 0 : includes.Entry) === null || _b === void 0 ? void 0 : _b.map(link => models_2.ContentfulData.fromCdaData(link))) !== null && _c !== void 0 ? _c : [];
108
107
  // return normalized item
109
108
  return { entry, references };
110
109
  }
@@ -21,7 +21,7 @@ export declare class ContentfulPageClient implements PageClient {
21
21
  unpublish(args: PageClient.Unpublish.Args): Promise<PageClient.Unpublish.Result>;
22
22
  getDependants(args: PageClient.GetDependants.Args): Promise<PageClient.GetDependants.Result>;
23
23
  getDependencies(args: PageClient.GetDependencies.Args): Promise<PageClient.GetDependencies.Result>;
24
- validateSlug(args: PageClient.ValidateSlug.Args): Promise<PageClient.ValidateSlug.Result>;
24
+ validateSlug({ brand, type, region, slug, groupId }: PageClient.ValidateSlug.Args): Promise<PageClient.ValidateSlug.Result>;
25
25
  }
26
26
  export declare namespace ContentfulPageClient {
27
27
  type FlattedPageData = Omit<Schema.Page.FlattenedData, 'data'> & {
@@ -119,19 +119,29 @@ class ContentfulPageClient {
119
119
  };
120
120
  }
121
121
  async get(args) {
122
- // create args for query
123
- const queryArgs = 'id' in args
124
- ? { ids: [args.id], version: args.version }
125
- : args;
126
122
  // fetch entries (should be one entry)
127
123
  const { cms } = this;
128
- const { entries, references } = await cms.getEntries(Object.assign({ content_type: models_1.ContentfulPageData.contentType, includeLinks: true, version: args.version, pagination: { page: 1, pageSize: 1 } }, getPageQueryParams(queryArgs)));
124
+ const [{ entries, references }, { entry: entrySnapshot }] = await Promise.all([
125
+ // query target (could be published/draft)
126
+ cms.getEntries(Object.assign({ content_type: models_1.ContentfulPageData.contentType, includeLinks: true, version: args.version, pagination: { page: 1, pageSize: 1 }, select: getPageSelect(args.select === 'content'
127
+ ? funnel_core_1.Schema.Page.Data.AsContent.props
128
+ : funnel_core_1.Schema.Page.Data.Props.all) }, getPageQueryParams({ ids: [args.id] }))),
129
+ // query published (if pulling draft above)
130
+ args.version === 'draft'
131
+ ? cms.getEntry(Object.assign({ content_type: models_1.ContentfulPageData.contentType, entryId: args.id, includeLinks: false, version: 'published', select: getPageSelect(funnel_core_1.Schema.PageSnapshot.props) }, getPageQueryParams({ ids: [args.id] })))
132
+ : Promise.resolve({ entry: undefined, references: [] })
133
+ ]);
129
134
  // skip if no entries
130
135
  if (entries.length === 0) {
131
136
  return undefined;
132
137
  }
138
+ // stitch together snapshot with entry (if available
139
+ const data = models_1.ContentfulPageData.deserialize(entries[0]);
140
+ const snapshot = entrySnapshot
141
+ ? models_1.ContentfulPageData.Snapshot.deserialize(entrySnapshot)
142
+ : undefined;
133
143
  // use first entry and deserialize
134
- return Object.assign({ data: models_1.ContentfulPageData.deserialize(entries[0]) }, models_1.ContentfulPageData.deserializeRefs(references));
144
+ return Object.assign({ data: Object.assign(Object.assign({}, data), { snapshotId: snapshot === null || snapshot === void 0 ? void 0 : snapshot.id, snapshot }) }, models_1.ContentfulPageData.deserializeRefs(references));
135
145
  }
136
146
  async create(args) {
137
147
  console.debug('[ContentfulPageClient/create] processing create page request', args);
@@ -353,8 +363,26 @@ class ContentfulPageClient {
353
363
  getDependencies(args) {
354
364
  throw new funnel_core_1.NotImplementedError();
355
365
  }
356
- validateSlug(args) {
357
- throw new funnel_core_1.NotImplementedError();
366
+ async validateSlug({ brand, type, region, slug, groupId }) {
367
+ // fetch items (bail if there are no instances)
368
+ const { cms } = this;
369
+ const { entries } = await cms.getEntries(Object.assign({ content_type: models_1.ContentfulPageData.contentType, version: 'draft', select: getPageSelect(funnel_core_1.Schema.Page.Data.AsLink.props) }, getPageQueryParams({ brand, type, region, slug })));
370
+ const links = entries.map(entry => models_1.ContentfulPageData.deserialize(entry, funnel_core_1.Schema.Page.Data.AsLink.props));
371
+ // ignore pages in same group (if group specified)
372
+ const matchedPages = groupId !== undefined
373
+ ? links.filter(link => link.groupId !== groupId)
374
+ : links;
375
+ // available if no pages matched
376
+ if (matchedPages.length === 0) {
377
+ return { status: 'available' };
378
+ }
379
+ // otherwise not available
380
+ else {
381
+ return {
382
+ status: 'unavailable',
383
+ takenBy: links[0]
384
+ };
385
+ }
358
386
  }
359
387
  }
360
388
  exports.ContentfulPageClient = ContentfulPageClient;
@@ -34,11 +34,11 @@ export declare namespace IContentfulContentClient {
34
34
  version?: Environment;
35
35
  };
36
36
  namespace GetEntry {
37
- type Args<TEntry extends ContentfulData> = EntryQueryOptions & {
37
+ type Args<TEntry extends ContentfulData> = Pick<QueryOptions, 'content_type' | 'select'> & EntryQueryOptions & {
38
38
  entryId: ContentfulData.EntryId<TEntry>;
39
39
  };
40
40
  type Result<TEntry extends ContentfulData> = {
41
- entry: TEntry;
41
+ entry: TEntry | undefined;
42
42
  references: ContentfulData[];
43
43
  };
44
44
  }
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.ContentfulCollectionData = void 0;
4
4
  const funnel_core_1 = require("@nascentdigital/funnel-core");
5
5
  const ContentfulAssetData_1 = require("./ContentfulAssetData");
6
+ const ContentfulPageData_1 = require("./ContentfulPageData");
6
7
  // extension
7
8
  var ContentfulCollectionData;
8
9
  (function (ContentfulCollectionData) {
@@ -71,6 +72,7 @@ var ContentfulCollectionData;
71
72
  // setup
72
73
  const files = {};
73
74
  const collections = {};
75
+ const pages = {};
74
76
  // loop over references and serialize
75
77
  for (const ref of refs) {
76
78
  switch (ref.__type) {
@@ -86,12 +88,18 @@ var ContentfulCollectionData;
86
88
  collections[collectionRef.id] = ContentfulCollectionData.deserialize(collectionRef, funnel_core_1.Schema.Collection.Data.AsLink.props);
87
89
  break;
88
90
  }
91
+ // page
92
+ case ContentfulPageData_1.ContentfulPageData.contentType: {
93
+ const pageRef = ref;
94
+ pages[pageRef.id] = ContentfulPageData_1.ContentfulPageData.deserialize(pageRef, funnel_core_1.Schema.Page.Data.AsLink.props);
95
+ }
89
96
  }
90
97
  }
91
98
  // return
92
99
  return {
93
100
  files,
94
- collections
101
+ collections,
102
+ pages
95
103
  };
96
104
  }
97
105
  ContentfulCollectionData.deserializeRefs = deserializeRefs;
@@ -12,6 +12,10 @@ export declare namespace ContentfulPageData {
12
12
  readonly asset: "files";
13
13
  };
14
14
  type ReferencesSpec = typeof referencesSpec;
15
+ type Snapshot = ContentfulData<ContentfulPageData.ContentType, Schema.PageSnapshot.Id> & Omit<Schema.PageSnapshot, 'id' | 'changeSetId'>;
16
+ namespace Snapshot {
17
+ function deserialize(snapshot: Snapshot): Schema.PageSnapshot;
18
+ }
15
19
  type ForSchema = Schema.Page.Data;
16
20
  type Potential = RequiredBy<Partial<ContentfulPageData>, keyof ContentfulData<ContentfulPageData.ContentType, Schema.Page.Id>>;
17
21
  function deserialize(data: Potential, props?: ReadonlyArray<Schema.Page.Data.Props.All>): ForSchema;
@@ -9,11 +9,21 @@ var ContentfulPageData;
9
9
  (function (ContentfulPageData) {
10
10
  // constants
11
11
  ContentfulPageData.contentType = 'page';
12
+ // references
12
13
  ContentfulPageData.referencesSpec = {
13
14
  'page': 'pages',
14
15
  'collection': 'collections',
15
16
  'asset': 'files'
16
17
  };
18
+ let Snapshot;
19
+ (function (Snapshot) {
20
+ function deserialize(snapshot) {
21
+ var _a, _b;
22
+ const data = ContentfulPageData.deserialize(snapshot, funnel_core_1.Schema.PageSnapshot.props);
23
+ return Object.assign(Object.assign({}, data), { changeSetId: ((_b = (_a = snapshot === null || snapshot === void 0 ? void 0 : snapshot.__version) === null || _a === void 0 ? void 0 : _a.toString()) !== null && _b !== void 0 ? _b : 'unknown') });
24
+ }
25
+ Snapshot.deserialize = deserialize;
26
+ })(Snapshot = ContentfulPageData.Snapshot || (ContentfulPageData.Snapshot = {}));
17
27
  function deserialize(data, props = funnel_core_1.Schema.Page.Data.Props.all) {
18
28
  var _a;
19
29
  // create base page
@@ -187,33 +187,16 @@ class StrapiPageClient {
187
187
  const resultProps = args.select === 'content'
188
188
  ? funnel_core_1.Schema.Page.Data.AsContent.props
189
189
  : funnel_core_1.Schema.Page.Data.Props.all;
190
- let data;
191
- if ('id' in args) {
192
- // find entry by document id
193
- const { data: rawData } = await collection.findOne(args.id, {
194
- fields: serialization_1.PageData.getStrapiFilter(resultProps),
195
- status: args.version,
196
- populate: serialization_1.PageData.Populate.full
197
- });
198
- // skip if there's no entry
199
- if (!rawData) {
200
- console.debug(`[StrapiPageClient/getPage] no "${args.version}" entry found for id ${args.id}`);
201
- return undefined;
202
- }
203
- // or capture it
204
- data = rawData;
205
- }
206
- // or by querying type (including region, and language)
207
- else {
208
- // lookup entry from collection (might be a miss)
209
- const { data: rawData } = await collection.find(Object.assign({ fields: serialization_1.PageData.getStrapiFilter(resultProps), sort: ['updatedAt:desc'], populate: serialization_1.PageData.Populate.full }, getPageQueryParams(args)));
210
- // skip if there's no entry
211
- if (rawData.length === 0) {
212
- console.warn(`[StrapiPageClient/getStaticPage] no "${args.version}" entry found for type ${args.type} in region ${args.region}/${args.language}`);
213
- return undefined;
214
- }
215
- // or capture entry
216
- data = rawData[0];
190
+ // find entry by document id
191
+ const { data } = await collection.findOne(args.id, {
192
+ fields: serialization_1.PageData.getStrapiFilter(resultProps),
193
+ status: args.version,
194
+ populate: serialization_1.PageData.Populate.full
195
+ });
196
+ // skip if there's no entry
197
+ if (!data) {
198
+ console.debug(`[StrapiPageClient/getPage] no "${args.version}" entry found for id ${args.id}`);
199
+ return undefined;
217
200
  }
218
201
  // convert and return
219
202
  return serialization_1.PageData.deserializeFlattened(data, strapiUrl, resultProps);
@@ -181,7 +181,8 @@ var CollectionData;
181
181
  // return
182
182
  return {
183
183
  files,
184
- collections
184
+ collections,
185
+ pages: {}
185
186
  };
186
187
  }
187
188
  CollectionData.deserializeRefs = deserializeRefs;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nascentdigital/funnel-cms",
3
- "version": "5.0.3",
3
+ "version": "6.0.1",
4
4
  "repository": {
5
5
  "url": "git+https://github.com/nascentdigital/funnel.git",
6
6
  "directory": "libs/cms"
@@ -48,5 +48,5 @@
48
48
  "ts-jest": "^29",
49
49
  "type-fest": "^4"
50
50
  },
51
- "gitHead": "0d1cc7d78504e8819998d928ffb342d622f75690"
51
+ "gitHead": "83e335312f91fb90aae73dfc31b3df1ec1895f76"
52
52
  }