@google-ads/datamanager-util 0.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/src/formatter.d.ts +34 -0
- package/dist/src/formatter.js +261 -0
- package/dist/src/index.d.ts +1 -0
- package/dist/src/index.js +30 -0
- package/package.json +32 -0
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Utility for normalizing and formatting user data.
|
|
3
|
+
* @see {@link https://developers.google.com/google-ads/api/docs/conversions/upload-clicks#javascript}
|
|
4
|
+
*/
|
|
5
|
+
export declare class UserDataFormatter {
|
|
6
|
+
formatEmailAddress(emailAddress: string): string;
|
|
7
|
+
formatPhoneNumber(phoneNumber: string): string;
|
|
8
|
+
formatGivenName(givenName: string): string;
|
|
9
|
+
formatFamilyName(familyName: string): string;
|
|
10
|
+
formatRegionCode(regionCode: string): string;
|
|
11
|
+
formatPostalCode(postalCode: string): string;
|
|
12
|
+
private formatLocationString;
|
|
13
|
+
formatAddressLine(addressLine: string): string;
|
|
14
|
+
formatCity(city: string): string;
|
|
15
|
+
formatAdministrativeArea(administrativeArea: string): string;
|
|
16
|
+
hashString(s: string): Buffer;
|
|
17
|
+
hexEncode(bytes: Buffer): string;
|
|
18
|
+
base64Encode(bytes: Buffer): string;
|
|
19
|
+
processEmailAddress(email: string, encoding: Encoding): string;
|
|
20
|
+
processPhoneNumber(phoneNumber: string, encoding: Encoding): string;
|
|
21
|
+
processGivenName(givenName: string, encoding: Encoding): string;
|
|
22
|
+
processFamilyName(familyName: string, encoding: Encoding): string;
|
|
23
|
+
processRegionCode(regionCode: string): string;
|
|
24
|
+
processPostalCode(postalCode: string): string;
|
|
25
|
+
processAddressLine(addressLine: string, encoding: Encoding): string;
|
|
26
|
+
processCity(city: string): string;
|
|
27
|
+
processAdministrativeArea(administrativeArea: string): string;
|
|
28
|
+
private hashAndEncode;
|
|
29
|
+
private encode;
|
|
30
|
+
}
|
|
31
|
+
export declare enum Encoding {
|
|
32
|
+
HEX = "hex",
|
|
33
|
+
BASE64 = "base64"
|
|
34
|
+
}
|
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// Copyright 2025 Google LLC
|
|
3
|
+
//
|
|
4
|
+
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
// you may not use this file except in compliance with the License.
|
|
6
|
+
// You may obtain a copy of the License at
|
|
7
|
+
//
|
|
8
|
+
// http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
//
|
|
10
|
+
// Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
// See the License for the specific language governing permissions and
|
|
14
|
+
// limitations under the License.
|
|
15
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
16
|
+
if (k2 === undefined) k2 = k;
|
|
17
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
18
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
19
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
20
|
+
}
|
|
21
|
+
Object.defineProperty(o, k2, desc);
|
|
22
|
+
}) : (function(o, m, k, k2) {
|
|
23
|
+
if (k2 === undefined) k2 = k;
|
|
24
|
+
o[k2] = m[k];
|
|
25
|
+
}));
|
|
26
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
27
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
28
|
+
}) : function(o, v) {
|
|
29
|
+
o["default"] = v;
|
|
30
|
+
});
|
|
31
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
32
|
+
var ownKeys = function(o) {
|
|
33
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
34
|
+
var ar = [];
|
|
35
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
36
|
+
return ar;
|
|
37
|
+
};
|
|
38
|
+
return ownKeys(o);
|
|
39
|
+
};
|
|
40
|
+
return function (mod) {
|
|
41
|
+
if (mod && mod.__esModule) return mod;
|
|
42
|
+
var result = {};
|
|
43
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
44
|
+
__setModuleDefault(result, mod);
|
|
45
|
+
return result;
|
|
46
|
+
};
|
|
47
|
+
})();
|
|
48
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
49
|
+
exports.Encoding = exports.UserDataFormatter = void 0;
|
|
50
|
+
const crypto = __importStar(require("crypto"));
|
|
51
|
+
/**
|
|
52
|
+
* Utility for normalizing and formatting user data.
|
|
53
|
+
* @see {@link https://developers.google.com/google-ads/api/docs/conversions/upload-clicks#javascript}
|
|
54
|
+
*/
|
|
55
|
+
class UserDataFormatter {
|
|
56
|
+
formatEmailAddress(emailAddress) {
|
|
57
|
+
if (!emailAddress) {
|
|
58
|
+
throw new Error('Email address is null or empty.');
|
|
59
|
+
}
|
|
60
|
+
const trimmedEmail = emailAddress.trim();
|
|
61
|
+
if (trimmedEmail === '') {
|
|
62
|
+
throw new Error('Email address is empty or blank.');
|
|
63
|
+
}
|
|
64
|
+
if (trimmedEmail.includes(' ')) {
|
|
65
|
+
throw new Error('Email address contains intermediate whitespace.');
|
|
66
|
+
}
|
|
67
|
+
const emailParts = trimmedEmail.toLowerCase().split('@');
|
|
68
|
+
if (emailParts.length !== 2) {
|
|
69
|
+
throw new Error('Email is not of the form user@domain');
|
|
70
|
+
}
|
|
71
|
+
let username = emailParts[0];
|
|
72
|
+
const domain = emailParts[1];
|
|
73
|
+
if (!username) {
|
|
74
|
+
throw new Error('Email address without the domain is empty');
|
|
75
|
+
}
|
|
76
|
+
if (!domain) {
|
|
77
|
+
throw new Error('Domain of email address is empty');
|
|
78
|
+
}
|
|
79
|
+
if (domain === 'gmail.com' || domain === 'googlemail.com') {
|
|
80
|
+
username = username.split('+')[0];
|
|
81
|
+
username = username.replace(/\./g, '');
|
|
82
|
+
}
|
|
83
|
+
if (!username) {
|
|
84
|
+
throw new Error('Email address without the domain name is empty after normalization');
|
|
85
|
+
}
|
|
86
|
+
return `${username}@${domain}`;
|
|
87
|
+
}
|
|
88
|
+
formatPhoneNumber(phoneNumber) {
|
|
89
|
+
if (!phoneNumber) {
|
|
90
|
+
throw new Error('Phone number is null or empty.');
|
|
91
|
+
}
|
|
92
|
+
const trimmedPhone = phoneNumber.trim();
|
|
93
|
+
if (trimmedPhone === '') {
|
|
94
|
+
throw new Error('Phone number is empty or blank.');
|
|
95
|
+
}
|
|
96
|
+
const digitsOnly = trimmedPhone.replace(/\D/g, '');
|
|
97
|
+
if (digitsOnly === '') {
|
|
98
|
+
throw new Error('Phone number contains no digits.');
|
|
99
|
+
}
|
|
100
|
+
return `+${digitsOnly}`;
|
|
101
|
+
}
|
|
102
|
+
formatGivenName(givenName) {
|
|
103
|
+
if (!givenName) {
|
|
104
|
+
throw new Error('Given name is null or empty.');
|
|
105
|
+
}
|
|
106
|
+
const trimmedGivenName = givenName.trim().toLowerCase();
|
|
107
|
+
if (trimmedGivenName === '') {
|
|
108
|
+
throw new Error('Given name is empty or blank.');
|
|
109
|
+
}
|
|
110
|
+
const withoutPrefix = trimmedGivenName
|
|
111
|
+
.replace(/^(?:mr|mrs|ms|dr)\.(?:\s|$)/, '')
|
|
112
|
+
.trim();
|
|
113
|
+
if (withoutPrefix === '') {
|
|
114
|
+
throw new Error('Given name consists solely of a prefix.');
|
|
115
|
+
}
|
|
116
|
+
return withoutPrefix;
|
|
117
|
+
}
|
|
118
|
+
formatFamilyName(familyName) {
|
|
119
|
+
if (!familyName) {
|
|
120
|
+
throw new Error('Family name is null or empty.');
|
|
121
|
+
}
|
|
122
|
+
const trimmedFamilyName = familyName.trim().toLowerCase();
|
|
123
|
+
if (trimmedFamilyName === '') {
|
|
124
|
+
throw new Error('Family name is empty or blank.');
|
|
125
|
+
}
|
|
126
|
+
let withoutSuffix = trimmedFamilyName;
|
|
127
|
+
const suffixPattern = new RegExp(String.raw `
|
|
128
|
+
(?:,\s*|\s+)
|
|
129
|
+
(?:jr\.?|sr\.?|2nd|3rd|ii|iii|iv|v|vi|cpa|dc|dds|vm|jd|md|phd)
|
|
130
|
+
\s?$
|
|
131
|
+
`.replace(/\s+/g, ''));
|
|
132
|
+
while (suffixPattern.test(withoutSuffix)) {
|
|
133
|
+
withoutSuffix = withoutSuffix.replace(suffixPattern, '');
|
|
134
|
+
}
|
|
135
|
+
if (withoutSuffix === '') {
|
|
136
|
+
throw new Error('Family name consists solely of a suffix.');
|
|
137
|
+
}
|
|
138
|
+
return withoutSuffix;
|
|
139
|
+
}
|
|
140
|
+
formatRegionCode(regionCode) {
|
|
141
|
+
if (!regionCode) {
|
|
142
|
+
throw new Error('Region code is null or empty.');
|
|
143
|
+
}
|
|
144
|
+
const trimmedRegionCode = regionCode.trim().toUpperCase();
|
|
145
|
+
if (trimmedRegionCode === '') {
|
|
146
|
+
throw new Error('Region code is empty or blank.');
|
|
147
|
+
}
|
|
148
|
+
if (trimmedRegionCode.length !== 2) {
|
|
149
|
+
throw new Error(`Region code length is ${trimmedRegionCode.length}. Length must be 2`);
|
|
150
|
+
}
|
|
151
|
+
if (!/^[A-Z]+$/.test(trimmedRegionCode)) {
|
|
152
|
+
throw new Error('Region code contains characters other than A-Z');
|
|
153
|
+
}
|
|
154
|
+
return trimmedRegionCode;
|
|
155
|
+
}
|
|
156
|
+
formatPostalCode(postalCode) {
|
|
157
|
+
if (!postalCode) {
|
|
158
|
+
throw new Error('Postal code is null or empty.');
|
|
159
|
+
}
|
|
160
|
+
const trimmedPostalCode = postalCode.trim();
|
|
161
|
+
if (trimmedPostalCode === '') {
|
|
162
|
+
throw new Error('Postal code is empty or blank.');
|
|
163
|
+
}
|
|
164
|
+
return trimmedPostalCode;
|
|
165
|
+
}
|
|
166
|
+
formatLocationString(value, label) {
|
|
167
|
+
if (!value) {
|
|
168
|
+
throw new Error(`${label} is null or empty.`);
|
|
169
|
+
}
|
|
170
|
+
let formattedValue = value.trim().toLowerCase();
|
|
171
|
+
formattedValue = formattedValue.replace(/[^\p{L}\p{N}\s]/gu, '');
|
|
172
|
+
if (!formattedValue) {
|
|
173
|
+
throw new Error(`${label} is blank or empty.`);
|
|
174
|
+
}
|
|
175
|
+
return formattedValue;
|
|
176
|
+
}
|
|
177
|
+
formatAddressLine(addressLine) {
|
|
178
|
+
return this.formatLocationString(addressLine, 'Address line');
|
|
179
|
+
}
|
|
180
|
+
formatCity(city) {
|
|
181
|
+
return this.formatLocationString(city, 'City');
|
|
182
|
+
}
|
|
183
|
+
formatAdministrativeArea(administrativeArea) {
|
|
184
|
+
return this.formatLocationString(administrativeArea, 'Administrative area');
|
|
185
|
+
}
|
|
186
|
+
hashString(s) {
|
|
187
|
+
if (s === null || s === undefined) {
|
|
188
|
+
throw new Error('String is null.');
|
|
189
|
+
}
|
|
190
|
+
if (s.trim() === '') {
|
|
191
|
+
throw new Error('String is empty or blank.');
|
|
192
|
+
}
|
|
193
|
+
return crypto.createHash('sha256').update(s).digest();
|
|
194
|
+
}
|
|
195
|
+
hexEncode(bytes) {
|
|
196
|
+
if (!bytes) {
|
|
197
|
+
throw new Error('Byte array is null.');
|
|
198
|
+
}
|
|
199
|
+
if (bytes.length === 0) {
|
|
200
|
+
throw new Error('Byte array is empty.');
|
|
201
|
+
}
|
|
202
|
+
return bytes.toString('hex');
|
|
203
|
+
}
|
|
204
|
+
base64Encode(bytes) {
|
|
205
|
+
if (!bytes) {
|
|
206
|
+
throw new Error('Byte array is null.');
|
|
207
|
+
}
|
|
208
|
+
if (bytes.length === 0) {
|
|
209
|
+
throw new Error('Byte array is empty.');
|
|
210
|
+
}
|
|
211
|
+
return bytes.toString('base64');
|
|
212
|
+
}
|
|
213
|
+
processEmailAddress(email, encoding) {
|
|
214
|
+
return this.hashAndEncode(this.formatEmailAddress(email), encoding);
|
|
215
|
+
}
|
|
216
|
+
processPhoneNumber(phoneNumber, encoding) {
|
|
217
|
+
return this.hashAndEncode(this.formatPhoneNumber(phoneNumber), encoding);
|
|
218
|
+
}
|
|
219
|
+
processGivenName(givenName, encoding) {
|
|
220
|
+
return this.hashAndEncode(this.formatGivenName(givenName), encoding);
|
|
221
|
+
}
|
|
222
|
+
processFamilyName(familyName, encoding) {
|
|
223
|
+
return this.hashAndEncode(this.formatFamilyName(familyName), encoding);
|
|
224
|
+
}
|
|
225
|
+
processRegionCode(regionCode) {
|
|
226
|
+
return this.formatRegionCode(regionCode);
|
|
227
|
+
}
|
|
228
|
+
processPostalCode(postalCode) {
|
|
229
|
+
return this.formatPostalCode(postalCode);
|
|
230
|
+
}
|
|
231
|
+
processAddressLine(addressLine, encoding) {
|
|
232
|
+
return this.hashAndEncode(this.formatAddressLine(addressLine), encoding);
|
|
233
|
+
}
|
|
234
|
+
processCity(city) {
|
|
235
|
+
return this.formatCity(city);
|
|
236
|
+
}
|
|
237
|
+
processAdministrativeArea(administrativeArea) {
|
|
238
|
+
return this.formatAdministrativeArea(administrativeArea);
|
|
239
|
+
}
|
|
240
|
+
hashAndEncode(normalizedString, encoding) {
|
|
241
|
+
const hashBytes = this.hashString(normalizedString);
|
|
242
|
+
return this.encode(hashBytes, encoding);
|
|
243
|
+
}
|
|
244
|
+
encode(bytes, encoding) {
|
|
245
|
+
if (encoding === 'hex') {
|
|
246
|
+
return this.hexEncode(bytes);
|
|
247
|
+
}
|
|
248
|
+
else if (encoding === 'base64') {
|
|
249
|
+
return this.base64Encode(bytes);
|
|
250
|
+
}
|
|
251
|
+
else {
|
|
252
|
+
throw new Error(`Invalid encoding: ${encoding}`);
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
exports.UserDataFormatter = UserDataFormatter;
|
|
257
|
+
var Encoding;
|
|
258
|
+
(function (Encoding) {
|
|
259
|
+
Encoding["HEX"] = "hex";
|
|
260
|
+
Encoding["BASE64"] = "base64";
|
|
261
|
+
})(Encoding || (exports.Encoding = Encoding = {}));
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './formatter';
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// Copyright 2025 Google LLC
|
|
3
|
+
//
|
|
4
|
+
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
// you may not use this file except in compliance with the License.
|
|
6
|
+
// You may obtain a copy of the License at
|
|
7
|
+
//
|
|
8
|
+
// http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
//
|
|
10
|
+
// Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
// See the License for the specific language governing permissions and
|
|
14
|
+
// limitations under the License.
|
|
15
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
16
|
+
if (k2 === undefined) k2 = k;
|
|
17
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
18
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
19
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
20
|
+
}
|
|
21
|
+
Object.defineProperty(o, k2, desc);
|
|
22
|
+
}) : (function(o, m, k, k2) {
|
|
23
|
+
if (k2 === undefined) k2 = k;
|
|
24
|
+
o[k2] = m[k];
|
|
25
|
+
}));
|
|
26
|
+
var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
27
|
+
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
28
|
+
};
|
|
29
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
30
|
+
__exportStar(require("./formatter"), exports);
|
package/package.json
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@google-ads/datamanager-util",
|
|
3
|
+
"version": "0.4.0",
|
|
4
|
+
"description": "A utility library for the Data Manager API for Node.js.",
|
|
5
|
+
"main": "dist/src/index.js",
|
|
6
|
+
"types": "dist/src/index.d.ts",
|
|
7
|
+
"license": "Apache-2.0",
|
|
8
|
+
"author": "Google LLC",
|
|
9
|
+
"publishConfig": {
|
|
10
|
+
"access": "public"
|
|
11
|
+
},
|
|
12
|
+
"engines": {
|
|
13
|
+
"node": ">=22"
|
|
14
|
+
},
|
|
15
|
+
"files": [
|
|
16
|
+
"dist/src"
|
|
17
|
+
],
|
|
18
|
+
"scripts": {
|
|
19
|
+
"test": "c8 mocha --timeout 600000 dist/test/*.js",
|
|
20
|
+
"lint": "gts lint",
|
|
21
|
+
"fix": "gts fix",
|
|
22
|
+
"build": "tsc",
|
|
23
|
+
"prepare": "npm run build",
|
|
24
|
+
"pretest": "npm run build"
|
|
25
|
+
},
|
|
26
|
+
"dependencies": {},
|
|
27
|
+
"devDependencies": {
|
|
28
|
+
"chai": "^4.2.0",
|
|
29
|
+
"@types/chai": "^4.3.0",
|
|
30
|
+
"@types/mocha": "^10.0.0"
|
|
31
|
+
}
|
|
32
|
+
}
|