@adobe/spacecat-shared-data-access 1.60.1 → 1.60.2

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 CHANGED
@@ -1,3 +1,10 @@
1
+ # [@adobe/spacecat-shared-data-access-v1.60.2](https://github.com/adobe/spacecat-shared/compare/@adobe/spacecat-shared-data-access-v1.60.1...@adobe/spacecat-shared-data-access-v1.60.2) (2024-12-19)
2
+
3
+
4
+ ### Bug Fixes
5
+
6
+ * reduce number of indexes ([#499](https://github.com/adobe/spacecat-shared/issues/499)) ([95601cc](https://github.com/adobe/spacecat-shared/commit/95601cca8ca37f989a650fa841e0dae678cebc25))
7
+
1
8
  # [@adobe/spacecat-shared-data-access-v1.60.1](https://github.com/adobe/spacecat-shared/compare/@adobe/spacecat-shared-data-access-v1.60.0...@adobe/spacecat-shared-data-access-v1.60.1) (2024-12-18)
2
9
 
3
10
 
package/package.json CHANGED
@@ -1,11 +1,11 @@
1
1
  {
2
2
  "name": "@adobe/spacecat-shared-data-access",
3
- "version": "1.60.1",
3
+ "version": "1.60.2",
4
4
  "description": "Shared modules of the Spacecat Services - Data Access",
5
5
  "type": "module",
6
6
  "engines": {
7
7
  "node": ">=20.0.0 <23.0.0",
8
- "npm": ">=10.0.0 <11.0.0"
8
+ "npm": ">=10.0.0 <12.0.0"
9
9
  },
10
10
  "main": "src/index.js",
11
11
  "types": "src/index.d.ts",
@@ -69,12 +69,10 @@ const schema = new SchemaBuilder(ApiKey, ApiKeyCollection)
69
69
  },
70
70
  })
71
71
  .addIndex(
72
- 'byHashedApiKey',
73
72
  { composite: ['hashedApiKey'] },
74
73
  { composite: ['updatedAt'] },
75
74
  )
76
75
  .addIndex(
77
- 'byImsOrgIdAndImsUserId',
78
76
  { composite: ['imsOrgId', 'imsUserId'] },
79
77
  { composite: ['updatedAt'] },
80
78
  );
@@ -24,7 +24,6 @@ import { guardId } from '../../util/guards.js';
24
24
  import {
25
25
  entityNameToAllPKValue,
26
26
  isNonEmptyArray,
27
- keyNamesToIndexName,
28
27
  removeElectroProperties,
29
28
  } from '../../util/util.js';
30
29
  import { INDEX_TYPES } from './constants.js';
@@ -40,26 +39,24 @@ function isValidParent(parent, child) {
40
39
  }
41
40
 
42
41
  /**
43
- * Attempts to find an index name matching a generated name from the given keyNames.
44
- * If no exact match is found, it progressively shortens the keyNames by removing the last one
45
- * and tries again. If still no match, it tries the "all" index, and then "primary".
46
- *
47
- * @param {object} indexes - The available indexes, keyed by their names.
48
- * @param {object} keys - The keys to find an index name for.
49
- * @returns {object} The found index.
42
+ * Finds the index name by the keys provided. The index is searched
43
+ * keys to match the combination of partition and sort keys. If no
44
+ * index is found, we fall back to the "all" index, then the "primary".
45
+ * @param {Schema} schema - The schema to search for the index.
46
+ * @param {Object} keys - The keys to search for.
47
+ * @return {*|string} - The index name.
50
48
  */
51
- function findIndexNameByKeys(indexes, keys) {
49
+ function findIndexNameByKeys(schema, keys) {
52
50
  const keyNames = Object.keys(keys);
53
- for (let { length } = keyNames; length > 0; length -= 1) {
54
- const subKeyNames = keyNames.slice(0, length);
55
- const candidateName = keyNamesToIndexName(subKeyNames);
56
- if (indexes[candidateName]) {
57
- return candidateName;
58
- }
51
+
52
+ const index = schema.findIndexBySortKeys(keyNames);
53
+ if (index) {
54
+ return index.index;
59
55
  }
60
56
 
61
- if (indexes.all) {
62
- return INDEX_TYPES.ALL;
57
+ const allIndex = schema.findIndexByType(INDEX_TYPES.ALL);
58
+ if (allIndex) {
59
+ return allIndex.index;
63
60
  }
64
61
 
65
62
  return INDEX_TYPES.PRIMARY;
@@ -176,11 +173,11 @@ class BaseCollection {
176
173
  throw new Error(message);
177
174
  }
178
175
 
179
- const indexName = options.index || findIndexNameByKeys(this.entity.query, keys);
176
+ const indexName = options.index || findIndexNameByKeys(this.schema, keys);
180
177
  const index = this.entity.query[indexName];
181
178
 
182
179
  if (!index) {
183
- const message = `Failed to query [${this.entityName}]: index [${indexName}] not found`;
180
+ const message = `Failed to query [${this.entityName}]: query proxy [${indexName}] not found`;
184
181
  this.log.error(message);
185
182
  throw new Error(message);
186
183
  }
@@ -243,7 +240,8 @@ class BaseCollection {
243
240
  * Finds a single entity from the "all" index. Requires an index named "all" with a partition key
244
241
  * named "pk" with a static value of "ALL_<ENTITYNAME>".
245
242
  * @param {Object} [sortKeys] - The sort keys to use for the query.
246
- * @param {{index?: string, attributes?: string[]}} [options] - Additional options for the query.
243
+ * @param {{index?: string, attributes?: string[], order?: string}} [options] -
244
+ * Additional options for the query.
247
245
  * @return {Promise<BaseModel|Array<BaseModel>|null>}
248
246
  */
249
247
  async findByAll(sortKeys = {}, options = {}) {
@@ -254,7 +252,7 @@ class BaseCollection {
254
252
  }
255
253
 
256
254
  const keys = { pk: entityNameToAllPKValue(this.entityName), ...sortKeys };
257
- return this.#queryByIndexKeys(keys, { ...options, index: INDEX_TYPES.ALL, limit: 1 });
255
+ return this.#queryByIndexKeys(keys, { ...options, limit: 1 });
258
256
  }
259
257
 
260
258
  /**
@@ -58,12 +58,20 @@ export interface Reference {
58
58
  isRemoveDependents(): boolean;
59
59
  }
60
60
 
61
+ export interface IndexAccessor {
62
+ indexName: string;
63
+ keySets: string[][];
64
+ }
65
+
61
66
  export interface Schema {
67
+ findIndexBySortKeys(sortKeys: string[]): object | null;
68
+ findIndexByType(type: string): object | null;
62
69
  getAttribute(name: string): object;
63
70
  getAttributes(): object;
64
71
  getCollectionName(): string;
65
72
  getEntityName(): string;
66
73
  getIdName(): string;
74
+ getIndexAccessors(): Array<IndexAccessor>;
67
75
  getIndexes(): object;
68
76
  getIndexKeys(indexName: string): string[];
69
77
  getModelClass(): object;
@@ -75,8 +83,7 @@ export interface Schema {
75
83
 
76
84
  export interface SchemaBuilder {
77
85
  addAttribute(name: string, data: object): SchemaBuilder;
78
- addAllIndexWithComposite(...attributeNames: string[]): SchemaBuilder
79
- addAllIndexWithTemplateField(fieldName: string, template: string): SchemaBuilder;
86
+ addAllIndex(sortKeys: string[]): SchemaBuilder;
80
87
  addIndex(name: string, partitionKey: object, sortKey: object): SchemaBuilder;
81
88
  addReference(referenceType: string, entityName: string, sortKeys?: string[]): SchemaBuilder;
82
89
  build(): Schema;
@@ -15,10 +15,10 @@ import { hasText, isInteger, isNonEmptyObject } from '@adobe/spacecat-shared-uti
15
15
  import { v4 as uuid, validate as uuidValidate } from 'uuid';
16
16
 
17
17
  import {
18
- capitalize,
19
18
  decapitalize,
20
19
  entityNameToAllPKValue,
21
- entityNameToIdName, isNonEmptyArray,
20
+ entityNameToIdName,
21
+ isNonEmptyArray,
22
22
  } from '../../util/util.js';
23
23
 
24
24
  import { INDEX_TYPES } from './constants.js';
@@ -70,52 +70,6 @@ const UPDATED_AT_ATTRIBUTE_DATA = {
70
70
  set: () => new Date().toISOString(),
71
71
  };
72
72
 
73
- /** Certain index names (primary, all) are reserved and cannot be reused. */
74
- const RESERVED_INDEX_NAMES = [INDEX_TYPES.PRIMARY, INDEX_TYPES.ALL];
75
-
76
- /**
77
- * Constructs a fully qualified index name.
78
- * @param {string} service - The name of the service.
79
- * @param {string} entity - The name of the entity.
80
- * @param {string} name - The index name (e.g., 'all', 'byForeignKey').
81
- * @returns {string} The fully qualified index name.
82
- */
83
- const createdIndexName = (service, entity, name) => `${service.toLowerCase()}-data-${entity}-${name}`;
84
-
85
- /**
86
- * Sorts an indexes object by its keys alphabetically.
87
- * @param {object} indexes - An object whose keys are index names and values are index definitions.
88
- * @returns {object} A new object with the same entries, but keys sorted alphabetically.
89
- */
90
- const sortIndexes = (indexes) => Object.fromEntries(
91
- Object.entries(indexes).sort((a, b) => a[0].localeCompare(b[0])),
92
- );
93
-
94
- /**
95
- * Assigns GSI field names to indexes that don't have them yet.
96
- * Ensures that if an "all" index exists, it uses gsi1 (already assigned)
97
- * and other indexes continue numbering from gsi2 onwards.
98
- *
99
- * @param {object} indexes - Object of indexes that require naming.
100
- * @param {object|null} all - The "all" index object if present, null otherwise.
101
- */
102
- const numberGSIsIndexes = (indexes, all) => {
103
- // if there's an "all" index, we start indexing subsequent GSIs from 2,
104
- // because "all" index already occupies gsi1.
105
- // if no "all" index exists, start from 1.
106
- let gsiCounter = isNonEmptyObject(all) ? 1 : 0;
107
-
108
- Object.values(indexes).forEach((index) => { /* eslint-disable no-param-reassign */
109
- // only assign new field names and number through if none are provided.
110
- if (!index.pk.field || !index.sk.field) {
111
- gsiCounter += 1;
112
- }
113
-
114
- index.pk.field = index.pk.field || `gsi${gsiCounter}pk`;
115
- index.sk.field = index.sk.field || `gsi${gsiCounter}sk`;
116
- });
117
- };
118
-
119
73
  /**
120
74
  * The SchemaBuilder class allows for constructing a schema definition
121
75
  * including attributes, indexes, and references to other entities.
@@ -159,9 +113,9 @@ class SchemaBuilder {
159
113
 
160
114
  this.rawIndexes = {
161
115
  primary: null,
162
- all: null,
163
- belongs_to: {},
164
- other: {},
116
+ all: [],
117
+ belongs_to: [],
118
+ other: [],
165
119
  };
166
120
 
167
121
  this.attributes = {};
@@ -189,16 +143,14 @@ class SchemaBuilder {
189
143
  };
190
144
  }
191
145
 
192
- #internalAddIndex(name, partitionKey, sortKey, type) {
193
- const indexFullName = createdIndexName(this.serviceName, this.entityName, name);
194
-
146
+ #internalAddIndex(partitionKey, sortKey, type) {
195
147
  // store index config without assigning fields yet
196
148
  // the fields will be assigned in build phase based on sorting and presence of "all" index
197
- this.rawIndexes[type][name] = {
198
- ...(indexFullName && { index: indexFullName }),
149
+ this.rawIndexes[type].push({
150
+ type,
199
151
  pk: { ...partitionKey },
200
152
  sk: { ...sortKey },
201
- };
153
+ });
202
154
  }
203
155
 
204
156
  /**
@@ -224,52 +176,24 @@ class SchemaBuilder {
224
176
  }
225
177
 
226
178
  /**
227
- * Adds an "all" index based on composite attributes.
228
- * The "all" index is a special index listing all entities, sorted by given attributes.
229
- * Useful for global queries across all entities of this type.
230
- * Will overwrite any existing "all" index.
179
+ * Adds an "all" index with composite partition and sort keys, or a template-based sort key.
180
+ * Useful for querying all entities of this type. Only one "all" index is allowed and a
181
+ * pre-existing "all" index will be overwritten.
231
182
  *
232
- * @param {...string} attributeNames - The attribute names forming the composite sort key.
183
+ * @param {Array<string>} sortKeys - The attributes to form the sort key.
233
184
  * @returns {SchemaBuilder} Returns this builder for method chaining.
234
- * @throws {Error} If no attribute names are provided.
185
+ * @throws {Error} If composite attribute names or template are not provided.
235
186
  */
236
- addAllIndexWithComposite(...attributeNames) {
237
- if (attributeNames.length === 0) {
238
- throw new Error('At least one composite attribute name is required.');
187
+ addAllIndex(sortKeys) {
188
+ if (!isNonEmptyArray(sortKeys)) {
189
+ throw new Error('Sort keys are required and must be a non-empty array.');
239
190
  }
240
191
 
241
- this.rawIndexes.all = {
242
- index: createdIndexName(this.serviceName, this.entityName, INDEX_TYPES.ALL),
243
- pk: { field: 'gsi1pk', template: entityNameToAllPKValue(this.entityName) },
244
- sk: { field: 'gsi1sk', composite: attributeNames },
245
- };
246
-
247
- return this;
248
- }
249
-
250
- /**
251
- * Adds an "all" index with a template-based sort key.
252
- * Useful if a single value template defines how entries are sorted.
253
- *
254
- * @param {string} fieldName - The sort key field name.
255
- * @param {string} template - A template string defining how to generate the sort key value.
256
- * @returns {SchemaBuilder} Returns this builder for method chaining.
257
- * @throws {Error} If fieldName or template are not valid strings.
258
- */
259
- addAllIndexWithTemplateField(fieldName, template) {
260
- if (!hasText(fieldName)) {
261
- throw new Error('fieldName is required and must be a non-empty string.');
262
- }
263
-
264
- if (!hasText(template)) {
265
- throw new Error('template is required and must be a non-empty string.');
266
- }
267
-
268
- this.rawIndexes.all = {
269
- index: createdIndexName(this.serviceName, this.entityName, 'all'),
270
- pk: { field: 'gsi1pk', template: entityNameToAllPKValue(this.entityName) },
271
- sk: { field: fieldName, template },
272
- };
192
+ this.#internalAddIndex(
193
+ { template: entityNameToAllPKValue(this.entityName) },
194
+ { composite: sortKeys },
195
+ INDEX_TYPES.ALL,
196
+ );
273
197
 
274
198
  return this;
275
199
  }
@@ -277,22 +201,13 @@ class SchemaBuilder {
277
201
  /**
278
202
  * Adds a generic secondary index (GSI).
279
203
  *
280
- * @param {string} name - The index name. Cannot be 'primary' or 'all'.
281
204
  * @param {object} partitionKey - The partition key definition
282
205
  * (e.g., { composite: [attributeName] }).
283
206
  * @param {object} sortKey - The sort key definition.
284
207
  * @returns {SchemaBuilder} Returns this builder for method chaining.
285
208
  * @throws {Error} If index name is reserved or pk/sk configs are invalid.
286
209
  */
287
- addIndex(name, partitionKey, sortKey) {
288
- if (!hasText(name)) {
289
- throw new Error('Index name is required and must be a non-empty string.');
290
- }
291
-
292
- if (RESERVED_INDEX_NAMES.includes(name)) {
293
- throw new Error(`Index name "${name}" is reserved.`);
294
- }
295
-
210
+ addIndex(partitionKey, sortKey) {
296
211
  if (!isNonEmptyObject(partitionKey)) {
297
212
  throw new Error('Partition key configuration (pk) is required and must be a non-empty object.');
298
213
  }
@@ -301,7 +216,7 @@ class SchemaBuilder {
301
216
  throw new Error('Sort key configuration (sk) is required and must be a non-empty object.');
302
217
  }
303
218
 
304
- this.#internalAddIndex(name, partitionKey, sortKey, INDEX_TYPES.OTHER);
219
+ this.#internalAddIndex(partitionKey, sortKey, INDEX_TYPES.OTHER);
305
220
 
306
221
  return this;
307
222
  }
@@ -357,7 +272,6 @@ class SchemaBuilder {
357
272
  });
358
273
 
359
274
  this.#internalAddIndex(
360
- `by${capitalize(foreignKeyName)}`,
361
275
  { composite: [decapitalize(foreignKeyName)] },
362
276
  { composite: isNonEmptyArray(sortKeys) ? sortKeys : ['updatedAt'] },
363
277
  INDEX_TYPES.BELONGS_TO,
@@ -378,21 +292,37 @@ class SchemaBuilder {
378
292
  */
379
293
  #buildIndexes() {
380
294
  // eslint-disable-next-line camelcase
381
- const { belongs_to, other } = this.rawIndexes;
295
+ const { all, belongs_to, other } = this.rawIndexes;
296
+
297
+ // set the order of indexes
298
+ const orderedIndexes = [
299
+ ...all,
300
+ // eslint-disable-next-line camelcase
301
+ ...belongs_to,
302
+ ...other,
303
+ ];
304
+
305
+ if (orderedIndexes.length > 5) {
306
+ throw new Error('Cannot have more than 5 indexes.');
307
+ }
382
308
 
383
- // belongs_to indexes come before other indexes
384
- const indexes = {
385
- ...sortIndexes(belongs_to),
386
- ...sortIndexes(other),
387
- };
309
+ this.indexes = { primary: this.rawIndexes.primary };
388
310
 
389
- numberGSIsIndexes(indexes, this.rawIndexes.all);
311
+ let indexCounter = 0;
312
+ Object.values(orderedIndexes).forEach((index) => {
313
+ indexCounter += 1;
390
314
 
391
- this.indexes = {
392
- primary: this.rawIndexes.primary,
393
- ...(this.rawIndexes.all && { all: this.rawIndexes.all }),
394
- ...indexes,
395
- };
315
+ const pkFieldName = `gsi${indexCounter}pk`;
316
+ const skFieldName = `gsi${indexCounter}sk`;
317
+ const indexName = `${this.serviceName.toLowerCase()}-data-${pkFieldName}-${skFieldName}`;
318
+
319
+ this.indexes[indexName] = {
320
+ index: indexName,
321
+ indexType: index.type,
322
+ pk: { field: pkFieldName, ...index.pk },
323
+ sk: { field: skFieldName, ...index.sk },
324
+ };
325
+ });
396
326
  }
397
327
 
398
328
  /**
@@ -140,6 +140,31 @@ class Schema {
140
140
  return this.indexes[indexName];
141
141
  }
142
142
 
143
+ findIndexBySortKeys(sortKeys) {
144
+ // find index that has same sort keys, then remove the last sort key
145
+ // and find the index that has the remaining sort keys, etc.
146
+ for (let { length } = sortKeys; length > 0; length -= 1) {
147
+ const subKeyNames = sortKeys.slice(0, length);
148
+ const index = Object.values(this.indexes).find((candidate) => {
149
+ const { pk, sk } = candidate;
150
+ const allKeys = [...(pk?.facets || []), ...(sk?.facets || [])];
151
+
152
+ // check if all keys in the index are in the sort keys
153
+ return subKeyNames.every((key) => allKeys.includes(key));
154
+ });
155
+
156
+ if (isNonEmptyObject(index)) {
157
+ return index;
158
+ }
159
+ }
160
+
161
+ return null;
162
+ }
163
+
164
+ findIndexByType(type) {
165
+ return Object.values(this.indexes).find((index) => index.indexType === type) || null;
166
+ }
167
+
143
168
  /**
144
169
  * Returns the indexes for the schema. By default, this returns all indexes.
145
170
  * You can use the `exclude` parameter to exclude certain indexes.
@@ -170,7 +195,7 @@ class Schema {
170
195
  }
171
196
 
172
197
  const pkKeys = Array.isArray(index.pk?.facets) ? index.pk.facets : [];
173
- const skKeys = Array.isArray(index.sk?.facets) ? index.sk.facets : [index.sk?.field];
198
+ const skKeys = Array.isArray(index.sk?.facets) ? index.sk.facets : [];
174
199
 
175
200
  return [...pkKeys, ...skKeys];
176
201
  }
@@ -10,7 +10,7 @@
10
10
  * governing permissions and limitations under the License.
11
11
  */
12
12
 
13
- import { incrementVersion, sanitizeIdAndAuditFields } from '../../util/util.js';
13
+ import { incrementVersion, sanitizeIdAndAuditFields, zeroPad } from '../../util/util.js';
14
14
  import BaseCollection from '../base/base.collection.js';
15
15
 
16
16
  /**
@@ -31,6 +31,10 @@ class ConfigurationCollection extends BaseCollection {
31
31
  return super.create(sanitizedData);
32
32
  }
33
33
 
34
+ async findByVersion(version) {
35
+ return this.findByAll({ versionString: zeroPad(version, 10) });
36
+ }
37
+
34
38
  async findLatest() {
35
39
  return this.findByAll({}, { order: 'desc' });
36
40
  }
@@ -19,6 +19,7 @@ import Joi from 'joi';
19
19
  import SchemaBuilder from '../base/schema.builder.js';
20
20
  import Configuration from './configuration.model.js';
21
21
  import ConfigurationCollection from './configuration.collection.js';
22
+ import { zeroPad } from '../../util/util.js';
22
23
 
23
24
  const handlerSchema = Joi.object().pattern(Joi.string(), Joi.object(
24
25
  {
@@ -97,7 +98,13 @@ const schema = new SchemaBuilder(Configuration, ConfigurationCollection)
97
98
  required: true,
98
99
  readOnly: true,
99
100
  })
100
- // eslint-disable-next-line no-template-curly-in-string
101
- .addAllIndexWithTemplateField('version', '${version}');
101
+ .addAttribute('versionString', { // used for indexing/sorting
102
+ type: 'string',
103
+ required: true,
104
+ readOnly: true,
105
+ default: '0', // setting the default forces set() to run, to transform the version number to a string
106
+ set: (value, all) => zeroPad(all.version, 10),
107
+ })
108
+ .addAllIndex(['versionString']);
102
109
 
103
110
  export default schema.build();
@@ -142,9 +142,8 @@ const schema = new SchemaBuilder(ImportJob, ImportJobCollection)
142
142
  default: 0,
143
143
  validate: (value) => !value || isInteger(value),
144
144
  })
145
- .addAllIndexWithComposite('startedAt')
145
+ .addAllIndex(['startedAt'])
146
146
  .addIndex(
147
- 'byStatus',
148
147
  { composite: ['status'] },
149
148
  { composite: ['updatedAt'] },
150
149
  );
@@ -46,6 +46,6 @@ const schema = new SchemaBuilder(Organization, OrganizationCollection)
46
46
  type: 'any',
47
47
  validate: (value) => !value || isNonEmptyObject(value),
48
48
  })
49
- .addAllIndexWithComposite('imsOrgId');
49
+ .addAllIndex(['imsOrgId']);
50
50
 
51
51
  export default schema.build();
@@ -81,9 +81,8 @@ const schema = new SchemaBuilder(Site, SiteCollection)
81
81
  set: () => new Date().toISOString(),
82
82
  validate: (value) => !value || isIsoDate(value),
83
83
  })
84
- .addAllIndexWithComposite('baseURL')
84
+ .addAllIndex(['baseURL'])
85
85
  .addIndex(
86
- 'byDeliveryType',
87
86
  { composite: ['deliveryType'] },
88
87
  { composite: ['updatedAt'] },
89
88
  );
@@ -54,6 +54,6 @@ const schema = new SchemaBuilder(SiteCandidate, SiteCandidateCollection)
54
54
  .addAttribute('updatedBy', {
55
55
  type: 'string',
56
56
  })
57
- .addAllIndexWithComposite('baseURL');
57
+ .addAllIndex(['baseURL']);
58
58
 
59
59
  export default schema.build();
package/src/v2/readme.md CHANGED
@@ -135,7 +135,7 @@ const userSchema = new SchemaBuilder(User, UserCollection)
135
135
  validate: (value) => value.includes('@'),
136
136
  })
137
137
  .addAttribute('name', { type: 'string', required: true })
138
- .addAllIndexWithComposite('email')
138
+ .addAllIndex(['email'])
139
139
  .addReference('belongs_to', 'Organization') // Adds organizationId and byOrganizationId index
140
140
  .build();
141
141
 
@@ -82,6 +82,13 @@ const incrementVersion = (version) => (isInteger(version) ? parseInt(version, 10
82
82
 
83
83
  const isNonEmptyArray = (value) => Array.isArray(value) && value.length > 0;
84
84
 
85
+ const zeroPad = (num, length) => {
86
+ const str = String(num);
87
+ return str.length >= length
88
+ ? str
89
+ : '0'.repeat(length - str.length) + str;
90
+ };
91
+
85
92
  export {
86
93
  capitalize,
87
94
  classExtends,
@@ -101,4 +108,5 @@ export {
101
108
  removeElectroProperties,
102
109
  sanitizeIdAndAuditFields,
103
110
  sanitizeTimestamps,
111
+ zeroPad,
104
112
  };