@memberjunction/geo-core 0.0.1 → 5.25.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.
@@ -0,0 +1,120 @@
1
+ import { BaseSingleton } from '@memberjunction/global';
2
+ import { BaseEntity, EntityInfo, UserInfo } from '@memberjunction/core';
3
+ import { MJRecordGeoCodeEntity } from '@memberjunction/core-entities';
4
+ import { GeoFieldMapping, GeocodeResult } from './types.js';
5
+ /**
6
+ * Singleton service that manages the geocoding lifecycle for entity records.
7
+ * Called by CodeGen-generated AfterSave hooks on geo-enabled entities.
8
+ *
9
+ * Responsibilities:
10
+ * - Computes source field hashes for change detection
11
+ * - Checks existing RecordGeoCode rows for staleness
12
+ * - Dispatches geocoding (delegates to registered geocoding providers)
13
+ * - Upserts RecordGeoCode rows with results or error status
14
+ *
15
+ * All geocoding is fire-and-forget — errors are captured in RecordGeoCode
16
+ * and retried by the scheduled geocoding job. Never throws.
17
+ */
18
+ export declare class GeoCodeSyncService extends BaseSingleton<GeoCodeSyncService> {
19
+ constructor();
20
+ static get Instance(): GeoCodeSyncService;
21
+ /**
22
+ * Check if any geo field mappings have changed and dispatch geocoding if needed.
23
+ * Called from the GenericDatabaseProvider OnSaveCompleted hook.
24
+ *
25
+ * This method never throws — all errors are captured in RecordGeoCode rows.
26
+ *
27
+ * @param entity - The entity instance that was just saved
28
+ * @param contextUser - The user context for data operations
29
+ * @param mappings - Field-to-location mappings (if not provided, derived from EntityField.ExtendedType metadata)
30
+ * @returns The geocode result from the first successfully geocoded mapping, or null
31
+ * if no geocoding was performed (e.g., hash unchanged) or all attempts failed.
32
+ * The caller can use this to patch virtual lat/lng fields on the entity's
33
+ * SP result before finalizeSave() loads it into the entity object.
34
+ */
35
+ SyncIfChanged(entity: BaseEntity, contextUser: UserInfo, mappings?: GeoFieldMapping[]): Promise<GeocodeResult | null>;
36
+ /**
37
+ * Derive geo field mappings from EntityField.ExtendedType metadata.
38
+ * Finds all fields with Geo* ExtendedType values and groups them into
39
+ * a Primary location mapping.
40
+ *
41
+ * @param entityInfo - The entity metadata to inspect
42
+ * @returns Array of geo field mappings (empty if no geo fields found)
43
+ */
44
+ static BuildMappingsFromMetadata(entityInfo: EntityInfo): GeoFieldMapping[];
45
+ /**
46
+ * Check if an entity has any geo fields defined in its metadata.
47
+ * @param entityInfo - The entity metadata to check
48
+ * @returns true if the entity has at least one field with a Geo* ExtendedType
49
+ */
50
+ static HasGeoFields(entityInfo: EntityInfo): boolean;
51
+ /**
52
+ * Process a single field mapping for a single entity record.
53
+ * @returns The geocode result if geocoding was performed successfully, or null if
54
+ * no geocoding was needed (hash unchanged) or the attempt failed.
55
+ */
56
+ protected ProcessMapping(entity: BaseEntity, mapping: GeoFieldMapping, contextUser: UserInfo): Promise<GeocodeResult | null>;
57
+ /**
58
+ * Find an existing RecordGeoCode row for a given entity/record/location type.
59
+ */
60
+ protected FindExistingGeoCode(entityID: string, recordID: string, locationType: string, contextUser: UserInfo): Promise<MJRecordGeoCodeEntity | null>;
61
+ /**
62
+ * Create a new RecordGeoCode row.
63
+ */
64
+ protected CreateGeoCodeRow(entityID: string, recordID: string, locationType: string, contextUser: UserInfo): Promise<MJRecordGeoCodeEntity | null>;
65
+ /**
66
+ * Update a RecordGeoCode row with successful geocoding results.
67
+ */
68
+ protected UpdateSuccess(row: MJRecordGeoCodeEntity, result: GeocodeResult, hash: string): Promise<void>;
69
+ /**
70
+ * Mark a RecordGeoCode row as not geocodable — the address can't be resolved
71
+ * to coordinates (e.g., "Conference Room B", "TBD"). Won't be retried by the
72
+ * bulk job. If the source record's address fields change, the hash mismatch
73
+ * in SyncIfChanged will trigger a fresh attempt.
74
+ */
75
+ protected UpdateNotGeocodable(row: MJRecordGeoCodeEntity, reason: string): Promise<void>;
76
+ /**
77
+ * Update a RecordGeoCode row with failure information (transient error, eligible for retry).
78
+ */
79
+ protected UpdateFailure(row: MJRecordGeoCodeEntity, errorMessage: string): Promise<void>;
80
+ /**
81
+ * Perform the actual geocoding using a priority-based strategy:
82
+ *
83
+ * 1. Native lat/lng fields → copy directly (GeocodingSource = 'native')
84
+ * 2. Address-level fields → external geocoding API (GeocodingSource = 'google')
85
+ * 3. Country/state only → reference table centroid lookup (GeocodingSource = 'reference_data')
86
+ */
87
+ protected Geocode(entity: BaseEntity, _mapping: GeoFieldMapping): Promise<GeocodeResult | null>;
88
+ /**
89
+ * Extract geo field values organized by their ExtendedType role.
90
+ */
91
+ private extractGeoFieldValues;
92
+ /**
93
+ * Build an address string suitable for the Google Geocoding API from geo field values.
94
+ * Orders components logically: Address, City, StateProvince, PostalCode, Country.
95
+ */
96
+ private buildAddressString;
97
+ /**
98
+ * Call the Google Geocoding API to convert an address string to coordinates.
99
+ */
100
+ private geocodeViaGoogle;
101
+ /**
102
+ * Resolve CountryID and StateProvinceID from entity geo field values
103
+ * against the reference Country/StateProvince tables.
104
+ */
105
+ /**
106
+ * Resolve CountryID and StateProvinceID from entity geo field values
107
+ * using the in-memory GeoDataEngine (O(1) lookups, no DB queries).
108
+ */
109
+ private resolveReferenceIDs;
110
+ /**
111
+ * Fallback geocoding using reference table centroids from GeoDataEngine.
112
+ * All lookups are O(1) in-memory — no DB queries.
113
+ */
114
+ private geocodeViaReferenceData;
115
+ /**
116
+ * Get the Google Geocoding API key from config or environment.
117
+ */
118
+ private getGoogleApiKey;
119
+ }
120
+ //# sourceMappingURL=GeoCodeSyncService.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"GeoCodeSyncService.d.ts","sourceRoot":"","sources":["../src/GeoCodeSyncService.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,wBAAwB,CAAC;AACvD,OAAO,EAAE,UAAU,EAAmB,UAAU,EAAqB,QAAQ,EAAuB,MAAM,sBAAsB,CAAC;AACjI,OAAO,EAAE,qBAAqB,EAAiB,MAAM,+BAA+B,CAAC;AACrF,OAAO,EAAE,eAAe,EAAE,aAAa,EAAmC,MAAM,SAAS,CAAC;AAuB1F;;;;;;;;;;;;GAYG;AACH,qBAAa,kBAAmB,SAAQ,aAAa,CAAC,kBAAkB,CAAC;;IAKrE,WAAkB,QAAQ,IAAI,kBAAkB,CAE/C;IAED;;;;;;;;;;;;;OAaG;IACU,aAAa,CAAC,MAAM,EAAE,UAAU,EAAE,WAAW,EAAE,QAAQ,EAAE,QAAQ,CAAC,EAAE,eAAe,EAAE,GAAG,OAAO,CAAC,aAAa,GAAG,IAAI,CAAC;IAclI;;;;;;;OAOG;WACW,yBAAyB,CAAC,UAAU,EAAE,UAAU,GAAG,eAAe,EAAE;IAgBlF;;;;OAIG;WACW,YAAY,CAAC,UAAU,EAAE,UAAU,GAAG,OAAO;IAI3D;;;;OAIG;cACa,cAAc,CAAC,MAAM,EAAE,UAAU,EAAE,OAAO,EAAE,eAAe,EAAE,WAAW,EAAE,QAAQ,GAAG,OAAO,CAAC,aAAa,GAAG,IAAI,CAAC;IAkElI;;OAEG;cACa,mBAAmB,CAC/B,QAAQ,EAAE,MAAM,EAChB,QAAQ,EAAE,MAAM,EAChB,YAAY,EAAE,MAAM,EACpB,WAAW,EAAE,QAAQ,GACtB,OAAO,CAAC,qBAAqB,GAAG,IAAI,CAAC;IAcxC;;OAEG;cACa,gBAAgB,CAC5B,QAAQ,EAAE,MAAM,EAChB,QAAQ,EAAE,MAAM,EAChB,YAAY,EAAE,MAAM,EACpB,WAAW,EAAE,QAAQ,GACtB,OAAO,CAAC,qBAAqB,GAAG,IAAI,CAAC;IAiBxC;;OAEG;cACa,aAAa,CACzB,GAAG,EAAE,qBAAqB,EAC1B,MAAM,EAAE,aAAa,EACrB,IAAI,EAAE,MAAM,GACb,OAAO,CAAC,IAAI,CAAC;IAiBhB;;;;;OAKG;cACa,mBAAmB,CAAC,GAAG,EAAE,qBAAqB,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAW9F;;OAEG;cACa,aAAa,CAAC,GAAG,EAAE,qBAAqB,EAAE,YAAY,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAW9F;;;;;;OAMG;cACa,OAAO,CACnB,MAAM,EAAE,UAAU,EAClB,QAAQ,EAAE,eAAe,GAC1B,OAAO,CAAC,aAAa,GAAG,IAAI,CAAC;IA4ChC;;OAEG;IACH,OAAO,CAAC,qBAAqB;IAa7B;;;OAGG;IACH,OAAO,CAAC,kBAAkB;IAW1B;;OAEG;YACW,gBAAgB;IAwC9B;;;OAGG;IACH;;;OAGG;IACH,OAAO,CAAC,mBAAmB;IAqB3B;;;OAGG;IACH,OAAO,CAAC,uBAAuB;IA6C/B;;OAEG;IACH,OAAO,CAAC,eAAe;CAc1B"}
@@ -0,0 +1,435 @@
1
+ import { BaseSingleton } from '@memberjunction/global';
2
+ import { Metadata, RunView, LogError, LogStatus } from '@memberjunction/core';
3
+ import { GeoDataEngine } from '@memberjunction/core-entities';
4
+ import { ComputeGeoSourceHash } from './hash.js';
5
+ /**
6
+ * Singleton service that manages the geocoding lifecycle for entity records.
7
+ * Called by CodeGen-generated AfterSave hooks on geo-enabled entities.
8
+ *
9
+ * Responsibilities:
10
+ * - Computes source field hashes for change detection
11
+ * - Checks existing RecordGeoCode rows for staleness
12
+ * - Dispatches geocoding (delegates to registered geocoding providers)
13
+ * - Upserts RecordGeoCode rows with results or error status
14
+ *
15
+ * All geocoding is fire-and-forget — errors are captured in RecordGeoCode
16
+ * and retried by the scheduled geocoding job. Never throws.
17
+ */
18
+ export class GeoCodeSyncService extends BaseSingleton {
19
+ constructor() {
20
+ super();
21
+ }
22
+ static get Instance() {
23
+ return GeoCodeSyncService.getInstance();
24
+ }
25
+ /**
26
+ * Check if any geo field mappings have changed and dispatch geocoding if needed.
27
+ * Called from the GenericDatabaseProvider OnSaveCompleted hook.
28
+ *
29
+ * This method never throws — all errors are captured in RecordGeoCode rows.
30
+ *
31
+ * @param entity - The entity instance that was just saved
32
+ * @param contextUser - The user context for data operations
33
+ * @param mappings - Field-to-location mappings (if not provided, derived from EntityField.ExtendedType metadata)
34
+ * @returns The geocode result from the first successfully geocoded mapping, or null
35
+ * if no geocoding was performed (e.g., hash unchanged) or all attempts failed.
36
+ * The caller can use this to patch virtual lat/lng fields on the entity's
37
+ * SP result before finalizeSave() loads it into the entity object.
38
+ */
39
+ async SyncIfChanged(entity, contextUser, mappings) {
40
+ const resolvedMappings = mappings ?? GeoCodeSyncService.BuildMappingsFromMetadata(entity.EntityInfo);
41
+ for (const mapping of resolvedMappings) {
42
+ try {
43
+ const result = await this.ProcessMapping(entity, mapping, contextUser);
44
+ if (result)
45
+ return result;
46
+ }
47
+ catch (e) {
48
+ const message = e instanceof Error ? e.message : String(e);
49
+ LogError(`GeoCodeSyncService: Error processing ${mapping.LocationType} for ${entity.EntityInfo.Name} ${entity.PrimaryKey.ToString()}: ${message}`);
50
+ }
51
+ }
52
+ return null;
53
+ }
54
+ /**
55
+ * Derive geo field mappings from EntityField.ExtendedType metadata.
56
+ * Finds all fields with Geo* ExtendedType values and groups them into
57
+ * a Primary location mapping.
58
+ *
59
+ * @param entityInfo - The entity metadata to inspect
60
+ * @returns Array of geo field mappings (empty if no geo fields found)
61
+ */
62
+ static BuildMappingsFromMetadata(entityInfo) {
63
+ const geoExtTypes = new Set([
64
+ 'Geo', 'GeoAddress', 'GeoCity', 'GeoStateProvince',
65
+ 'GeoCountry', 'GeoPostalCode', 'GeoLatitude', 'GeoLongitude'
66
+ ]);
67
+ const geoFields = entityInfo.Fields.filter((f) => f.ExtendedType != null && geoExtTypes.has(f.ExtendedType));
68
+ if (geoFields.length === 0)
69
+ return [];
70
+ return [{
71
+ LocationType: 'Primary',
72
+ Fields: geoFields.map((f) => f.Name)
73
+ }];
74
+ }
75
+ /**
76
+ * Check if an entity has any geo fields defined in its metadata.
77
+ * @param entityInfo - The entity metadata to check
78
+ * @returns true if the entity has at least one field with a Geo* ExtendedType
79
+ */
80
+ static HasGeoFields(entityInfo) {
81
+ return entityInfo.SupportsGeoCoding && GeoCodeSyncService.BuildMappingsFromMetadata(entityInfo).length > 0;
82
+ }
83
+ /**
84
+ * Process a single field mapping for a single entity record.
85
+ * @returns The geocode result if geocoding was performed successfully, or null if
86
+ * no geocoding was needed (hash unchanged) or the attempt failed.
87
+ */
88
+ async ProcessMapping(entity, mapping, contextUser) {
89
+ const hash = ComputeGeoSourceHash(entity, mapping.Fields);
90
+ // Build RecordID matching the format used in the view's LEFT JOIN to vwRecordGeoCodes:
91
+ // - Single PK: bare value as string (e.g., "38CB433E-F36B-1410-84B4-00BD01F02867")
92
+ // - Composite PK: values joined with "||" (e.g., "val1||val2")
93
+ // This must match sql_codegen.ts generateBaseViewJoins() geo JOIN format.
94
+ const pkPairs = entity.PrimaryKey.KeyValuePairs;
95
+ const recordId = pkPairs.length === 1
96
+ ? String(pkPairs[0].Value)
97
+ : pkPairs.map(pk => String(pk.Value)).join('||');
98
+ const existing = await this.FindExistingGeoCode(entity.EntityInfo.ID, recordId, mapping.LocationType, contextUser);
99
+ if (existing && existing.SourceFieldHash === hash && existing.Status === 'success') {
100
+ return null; // No change, already geocoded successfully
101
+ }
102
+ // Upsert a pending row
103
+ const row = existing ?? await this.CreateGeoCodeRow(entity.EntityInfo.ID, recordId, mapping.LocationType, contextUser);
104
+ if (!row) {
105
+ LogError(`GeoCodeSyncService: Failed to create/find RecordGeoCode row`);
106
+ return null;
107
+ }
108
+ row.SourceFieldHash = hash;
109
+ row.Status = 'pending';
110
+ row.RetryCount = 0; // Reset retries — fresh attempt for new/changed address
111
+ row.GeocodedAt = new Date();
112
+ const pendingSaved = await row.Save();
113
+ if (!pendingSaved) {
114
+ LogError(`GeoCodeSyncService: Failed to save pending RecordGeoCode: ${row.LatestResult?.CompleteMessage ?? 'unknown error'}`);
115
+ return null;
116
+ }
117
+ // Attempt geocoding
118
+ try {
119
+ const result = await this.Geocode(entity, mapping);
120
+ if (result) {
121
+ await this.UpdateSuccess(row, result, hash);
122
+ return result;
123
+ }
124
+ else {
125
+ // No result means the address can't be geocoded (e.g., "Conference Room B").
126
+ // Mark as not_geocodable so the retry job skips it. If the user later edits
127
+ // the address, the hash will change and SyncIfChanged will re-attempt.
128
+ await this.UpdateNotGeocodable(row, 'Geocoding returned no result — address may not be a valid location');
129
+ }
130
+ }
131
+ catch (e) {
132
+ // Exception = transient API error — mark as failed for retry
133
+ const message = e instanceof Error ? e.message : String(e);
134
+ await this.UpdateFailure(row, message);
135
+ }
136
+ return null;
137
+ }
138
+ /**
139
+ * Find an existing RecordGeoCode row for a given entity/record/location type.
140
+ */
141
+ async FindExistingGeoCode(entityID, recordID, locationType, contextUser) {
142
+ const rv = new RunView();
143
+ const result = await rv.RunView({
144
+ EntityName: 'MJ: Record Geo Codes',
145
+ ExtraFilter: `EntityID='${entityID}' AND RecordID='${recordID}' AND LocationType='${locationType}'`,
146
+ ResultType: 'entity_object',
147
+ MaxRows: 1
148
+ }, contextUser);
149
+ if (result.Success && result.Results.length > 0) {
150
+ return result.Results[0];
151
+ }
152
+ return null;
153
+ }
154
+ /**
155
+ * Create a new RecordGeoCode row.
156
+ */
157
+ async CreateGeoCodeRow(entityID, recordID, locationType, contextUser) {
158
+ const md = new Metadata();
159
+ const row = await md.GetEntityObject('MJ: Record Geo Codes', contextUser);
160
+ row.NewRecord();
161
+ row.EntityID = entityID;
162
+ row.RecordID = recordID;
163
+ row.LocationType = locationType;
164
+ row.Status = 'pending';
165
+ row.RetryCount = 0;
166
+ const saved = await row.Save();
167
+ if (!saved) {
168
+ LogError(`GeoCodeSyncService: Failed to create RecordGeoCode row: ${row.LatestResult?.CompleteMessage ?? 'unknown error'}`);
169
+ return null;
170
+ }
171
+ return row;
172
+ }
173
+ /**
174
+ * Update a RecordGeoCode row with successful geocoding results.
175
+ */
176
+ async UpdateSuccess(row, result, hash) {
177
+ row.Latitude = result.Latitude;
178
+ row.Longitude = result.Longitude;
179
+ row.Precision = result.Precision;
180
+ row.CountryID = result.CountryID ?? null;
181
+ row.StateProvinceID = result.StateProvinceID ?? null;
182
+ row.Status = 'success';
183
+ row.GeocodingSource = result.Source;
184
+ row.SourceFieldHash = hash;
185
+ row.GeocodedAt = new Date();
186
+ row.ErrorMessage = '';
187
+ const saved = await row.Save();
188
+ if (!saved) {
189
+ LogError(`GeoCodeSyncService: Failed to save successful geocode: ${row.LatestResult?.CompleteMessage ?? 'unknown error'}`);
190
+ }
191
+ }
192
+ /**
193
+ * Mark a RecordGeoCode row as not geocodable — the address can't be resolved
194
+ * to coordinates (e.g., "Conference Room B", "TBD"). Won't be retried by the
195
+ * bulk job. If the source record's address fields change, the hash mismatch
196
+ * in SyncIfChanged will trigger a fresh attempt.
197
+ */
198
+ async UpdateNotGeocodable(row, reason) {
199
+ row.Status = 'failed';
200
+ row.ErrorMessage = reason;
201
+ row.RetryCount = 9999; // Permanently skip retries — hash change will reset this
202
+ row.GeocodedAt = new Date();
203
+ const saved = await row.Save();
204
+ if (!saved) {
205
+ LogError(`GeoCodeSyncService: Failed to save not_geocodable status: ${row.LatestResult?.CompleteMessage ?? 'unknown error'}`);
206
+ }
207
+ }
208
+ /**
209
+ * Update a RecordGeoCode row with failure information (transient error, eligible for retry).
210
+ */
211
+ async UpdateFailure(row, errorMessage) {
212
+ row.Status = 'failed';
213
+ row.ErrorMessage = errorMessage;
214
+ row.RetryCount = (row.RetryCount ?? 0) + 1;
215
+ row.GeocodedAt = new Date();
216
+ const saved = await row.Save();
217
+ if (!saved) {
218
+ LogError(`GeoCodeSyncService: Failed to save failure status: ${row.LatestResult?.CompleteMessage ?? 'unknown error'}`);
219
+ }
220
+ }
221
+ /**
222
+ * Perform the actual geocoding using a priority-based strategy:
223
+ *
224
+ * 1. Native lat/lng fields → copy directly (GeocodingSource = 'native')
225
+ * 2. Address-level fields → external geocoding API (GeocodingSource = 'google')
226
+ * 3. Country/state only → reference table centroid lookup (GeocodingSource = 'reference_data')
227
+ */
228
+ async Geocode(entity, _mapping) {
229
+ // Strategy 1: Check for native (non-virtual) lat/lng fields.
230
+ // Virtual fields like __mj_Latitude/__mj_Longitude come from the RecordGeoCode JOIN
231
+ // and would create circular logic — reading old geocoded values instead of re-geocoding.
232
+ const latField = entity.EntityInfo.Fields.find(f => f.ExtendedType === 'GeoLatitude' && !f.IsVirtual);
233
+ const lngField = entity.EntityInfo.Fields.find(f => f.ExtendedType === 'GeoLongitude' && !f.IsVirtual);
234
+ if (latField && lngField) {
235
+ const latVal = entity.Get(latField.Name);
236
+ const lngVal = entity.Get(lngField.Name);
237
+ const lat = Number(latVal);
238
+ const lng = Number(lngVal);
239
+ if (latVal != null && lngVal != null && !isNaN(lat) && !isNaN(lng)) {
240
+ return {
241
+ Latitude: lat,
242
+ Longitude: lng,
243
+ Precision: 'exact',
244
+ CountryID: null,
245
+ StateProvinceID: null,
246
+ Source: 'native'
247
+ };
248
+ }
249
+ }
250
+ // Collect address field values organized by their geo role
251
+ const geoValues = this.extractGeoFieldValues(entity);
252
+ // Strategy 2: Google Geocoding API (if API key is available)
253
+ const apiKey = this.getGoogleApiKey();
254
+ if (apiKey) {
255
+ const addressString = this.buildAddressString(geoValues);
256
+ if (addressString) {
257
+ const googleResult = await this.geocodeViaGoogle(addressString, apiKey);
258
+ if (googleResult) {
259
+ // Resolve country/state IDs from Google's response
260
+ this.resolveReferenceIDs(googleResult, entity);
261
+ return googleResult;
262
+ }
263
+ }
264
+ }
265
+ // Strategy 3: Reference data centroid lookup (country/state → approximate lat/lng)
266
+ return this.geocodeViaReferenceData(geoValues);
267
+ }
268
+ /**
269
+ * Extract geo field values organized by their ExtendedType role.
270
+ */
271
+ extractGeoFieldValues(entity) {
272
+ const values = {};
273
+ for (const field of entity.EntityInfo.Fields) {
274
+ if (field.ExtendedType && field.ExtendedType.startsWith('Geo')) {
275
+ const val = entity.Get(field.Name);
276
+ if (val != null && String(val).trim() !== '') {
277
+ values[field.ExtendedType] = String(val).trim();
278
+ }
279
+ }
280
+ }
281
+ return values;
282
+ }
283
+ /**
284
+ * Build an address string suitable for the Google Geocoding API from geo field values.
285
+ * Orders components logically: Address, City, StateProvince, PostalCode, Country.
286
+ */
287
+ buildAddressString(geoValues) {
288
+ const parts = [];
289
+ if (geoValues['GeoAddress'])
290
+ parts.push(geoValues['GeoAddress']);
291
+ if (geoValues['GeoCity'])
292
+ parts.push(geoValues['GeoCity']);
293
+ if (geoValues['GeoStateProvince'])
294
+ parts.push(geoValues['GeoStateProvince']);
295
+ if (geoValues['GeoPostalCode'])
296
+ parts.push(geoValues['GeoPostalCode']);
297
+ if (geoValues['GeoCountry'])
298
+ parts.push(geoValues['GeoCountry']);
299
+ if (geoValues['Geo'])
300
+ parts.push(geoValues['Geo']); // generic location field
301
+ return parts.length > 0 ? parts.join(', ') : null;
302
+ }
303
+ /**
304
+ * Call the Google Geocoding API to convert an address string to coordinates.
305
+ */
306
+ async geocodeViaGoogle(address, apiKey) {
307
+ try {
308
+ const url = `https://maps.googleapis.com/maps/api/geocode/json?address=${encodeURIComponent(address)}&key=${apiKey}`;
309
+ const response = await fetch(url);
310
+ if (!response.ok) {
311
+ LogError(`GeoCodeSyncService: Google API HTTP error: ${response.status}`);
312
+ return null;
313
+ }
314
+ const data = await response.json();
315
+ if (data.status !== 'OK' || !data.results?.length) {
316
+ LogStatus(`GeoCodeSyncService: Google returned status "${data.status}" for "${address}"`);
317
+ return null;
318
+ }
319
+ const result = data.results[0];
320
+ const locationType = result.geometry.location_type;
321
+ // Map Google's location_type to our precision enum
322
+ let precision = 'city';
323
+ if (locationType === 'ROOFTOP')
324
+ precision = 'exact';
325
+ else if (locationType === 'RANGE_INTERPOLATED')
326
+ precision = 'exact';
327
+ else if (locationType === 'GEOMETRIC_CENTER')
328
+ precision = 'city';
329
+ else if (locationType === 'APPROXIMATE')
330
+ precision = 'state_province';
331
+ return {
332
+ Latitude: result.geometry.location.lat,
333
+ Longitude: result.geometry.location.lng,
334
+ Precision: precision,
335
+ CountryID: null, // resolved in resolveReferenceIDs
336
+ StateProvinceID: null, // resolved in resolveReferenceIDs
337
+ Source: 'google'
338
+ };
339
+ }
340
+ catch (e) {
341
+ const msg = e instanceof Error ? e.message : String(e);
342
+ LogError(`GeoCodeSyncService: Google geocoding error: ${msg}`);
343
+ return null;
344
+ }
345
+ }
346
+ /**
347
+ * Resolve CountryID and StateProvinceID from entity geo field values
348
+ * against the reference Country/StateProvince tables.
349
+ */
350
+ /**
351
+ * Resolve CountryID and StateProvinceID from entity geo field values
352
+ * using the in-memory GeoDataEngine (O(1) lookups, no DB queries).
353
+ */
354
+ resolveReferenceIDs(result, entity) {
355
+ const geoValues = this.extractGeoFieldValues(entity);
356
+ const geo = GeoDataEngine.Instance;
357
+ const countryVal = geoValues['GeoCountry'];
358
+ if (countryVal) {
359
+ const country = geo.ResolveCountry(countryVal);
360
+ if (country) {
361
+ result.CountryID = country.ID;
362
+ const stateVal = geoValues['GeoStateProvince'];
363
+ if (stateVal) {
364
+ const state = geo.ResolveState(country.ID, stateVal);
365
+ if (state) {
366
+ result.StateProvinceID = state.ID;
367
+ }
368
+ }
369
+ }
370
+ }
371
+ }
372
+ /**
373
+ * Fallback geocoding using reference table centroids from GeoDataEngine.
374
+ * All lookups are O(1) in-memory — no DB queries.
375
+ */
376
+ geocodeViaReferenceData(geoValues) {
377
+ const geo = GeoDataEngine.Instance;
378
+ const countryVal = geoValues['GeoCountry'];
379
+ const stateVal = geoValues['GeoStateProvince'];
380
+ if (!countryVal && !stateVal) {
381
+ return null;
382
+ }
383
+ // Try state-level first (more precise)
384
+ if (countryVal && stateVal) {
385
+ const country = geo.ResolveCountry(countryVal);
386
+ if (country) {
387
+ const state = geo.ResolveState(country.ID, stateVal);
388
+ if (state && state.Latitude != null && state.Longitude != null) {
389
+ return {
390
+ Latitude: state.Latitude,
391
+ Longitude: state.Longitude,
392
+ Precision: 'state_province',
393
+ CountryID: country.ID,
394
+ StateProvinceID: state.ID,
395
+ Source: 'reference_data'
396
+ };
397
+ }
398
+ }
399
+ }
400
+ // Fall back to country-level centroid
401
+ if (countryVal) {
402
+ const country = geo.ResolveCountry(countryVal);
403
+ if (country && country.Latitude != null && country.Longitude != null) {
404
+ return {
405
+ Latitude: country.Latitude,
406
+ Longitude: country.Longitude,
407
+ Precision: 'country',
408
+ CountryID: country.ID,
409
+ StateProvinceID: null,
410
+ Source: 'reference_data'
411
+ };
412
+ }
413
+ }
414
+ return null;
415
+ }
416
+ /**
417
+ * Get the Google Geocoding API key from config or environment.
418
+ */
419
+ getGoogleApiKey() {
420
+ // Check environment variables
421
+ const envKey = process.env.GOOGLE_GEOCODING_API_KEY || process.env.GOOGLE_MAPS_API_KEY;
422
+ if (envKey)
423
+ return envKey;
424
+ // Try mj.config.cjs via cosmiconfig pattern
425
+ try {
426
+ // Dynamic import to avoid hard dependency on cosmiconfig
427
+ const configVal = globalThis['__mj_config_apiIntegrations'];
428
+ return configVal?.google?.geocoding?.apiKey ?? null;
429
+ }
430
+ catch {
431
+ return null;
432
+ }
433
+ }
434
+ }
435
+ //# sourceMappingURL=GeoCodeSyncService.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"GeoCodeSyncService.js","sourceRoot":"","sources":["../src/GeoCodeSyncService.ts"],"names":[],"mappings":"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"}
package/dist/hash.d.ts ADDED
@@ -0,0 +1,12 @@
1
+ import { BaseEntity } from '@memberjunction/core';
2
+ /**
3
+ * Computes a SHA-256 hash of the geo-relevant field values for change detection.
4
+ * When source fields change on entity save, the hash won't match the stored
5
+ * SourceFieldHash in RecordGeoCode, triggering re-geocoding.
6
+ *
7
+ * @param entity - The entity instance to extract field values from
8
+ * @param fieldNames - The field names that contribute to geocoding
9
+ * @returns SHA-256 hex digest of the concatenated field values
10
+ */
11
+ export declare function ComputeGeoSourceHash(entity: BaseEntity, fieldNames: string[]): string;
12
+ //# sourceMappingURL=hash.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"hash.d.ts","sourceRoot":"","sources":["../src/hash.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,UAAU,EAAE,MAAM,sBAAsB,CAAC;AAElD;;;;;;;;GAQG;AACH,wBAAgB,oBAAoB,CAAC,MAAM,EAAE,UAAU,EAAE,UAAU,EAAE,MAAM,EAAE,GAAG,MAAM,CAOrF"}
package/dist/hash.js ADDED
@@ -0,0 +1,19 @@
1
+ import { createHash } from 'crypto';
2
+ /**
3
+ * Computes a SHA-256 hash of the geo-relevant field values for change detection.
4
+ * When source fields change on entity save, the hash won't match the stored
5
+ * SourceFieldHash in RecordGeoCode, triggering re-geocoding.
6
+ *
7
+ * @param entity - The entity instance to extract field values from
8
+ * @param fieldNames - The field names that contribute to geocoding
9
+ * @returns SHA-256 hex digest of the concatenated field values
10
+ */
11
+ export function ComputeGeoSourceHash(entity, fieldNames) {
12
+ const values = fieldNames.map(f => {
13
+ const val = entity.Get(f);
14
+ return val != null ? String(val).trim() : '';
15
+ });
16
+ const input = values.join('|');
17
+ return createHash('sha256').update(input, 'utf8').digest('hex');
18
+ }
19
+ //# sourceMappingURL=hash.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"hash.js","sourceRoot":"","sources":["../src/hash.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,QAAQ,CAAC;AAGpC;;;;;;;;GAQG;AACH,MAAM,UAAU,oBAAoB,CAAC,MAAkB,EAAE,UAAoB;IACzE,MAAM,MAAM,GAAG,UAAU,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE;QAC9B,MAAM,GAAG,GAAY,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC;QACnC,OAAO,GAAG,IAAI,IAAI,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;IACjD,CAAC,CAAC,CAAC;IACH,MAAM,KAAK,GAAG,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;IAC/B,OAAO,UAAU,CAAC,QAAQ,CAAC,CAAC,MAAM,CAAC,KAAK,EAAE,MAAM,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;AACpE,CAAC"}
@@ -0,0 +1,4 @@
1
+ export * from './types.js';
2
+ export * from './hash.js';
3
+ export * from './GeoCodeSyncService.js';
4
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,SAAS,CAAC;AACxB,cAAc,QAAQ,CAAC;AACvB,cAAc,sBAAsB,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,4 @@
1
+ export * from './types.js';
2
+ export * from './hash.js';
3
+ export * from './GeoCodeSyncService.js';
4
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,SAAS,CAAC;AACxB,cAAc,QAAQ,CAAC;AACvB,cAAc,sBAAsB,CAAC"}
@@ -0,0 +1,38 @@
1
+ /**
2
+ * Mapping from entity fields to a geocodable location.
3
+ * CodeGen generates these per-entity based on LLM analysis of field names.
4
+ */
5
+ export interface GeoFieldMapping {
6
+ /** Discriminator for the location. Currently only 'Primary' is generated by CodeGen. */
7
+ LocationType: 'Primary';
8
+ /** Entity field names that contribute to this location's address. */
9
+ Fields: string[];
10
+ }
11
+ /**
12
+ * Precision level of a geocoding result.
13
+ */
14
+ export type GeocodePrecision = 'exact' | 'postal_code' | 'city' | 'county' | 'state_province' | 'country';
15
+ /**
16
+ * Status of a geocoding attempt.
17
+ */
18
+ export type GeocodeStatus = 'success' | 'failed' | 'pending';
19
+ /**
20
+ * Source that produced the geocode.
21
+ */
22
+ export type GeocodingSource = 'google' | 'reference_data' | 'manual' | 'ip_geolocation' | 'native' | 'reverse';
23
+ /**
24
+ * Result from a geocoding operation.
25
+ */
26
+ export interface GeocodeResult {
27
+ Latitude: number;
28
+ Longitude: number;
29
+ Precision: GeocodePrecision;
30
+ CountryID: string | null;
31
+ StateProvinceID: string | null;
32
+ Source: GeocodingSource;
33
+ }
34
+ /**
35
+ * Distance unit for proximity queries.
36
+ */
37
+ export type DistanceUnit = 'mi' | 'km';
38
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,MAAM,WAAW,eAAe;IAC5B,wFAAwF;IACxF,YAAY,EAAE,SAAS,CAAC;IACxB,qEAAqE;IACrE,MAAM,EAAE,MAAM,EAAE,CAAC;CACpB;AAED;;GAEG;AACH,MAAM,MAAM,gBAAgB,GAAG,OAAO,GAAG,aAAa,GAAG,MAAM,GAAG,QAAQ,GAAG,gBAAgB,GAAG,SAAS,CAAC;AAE1G;;GAEG;AACH,MAAM,MAAM,aAAa,GAAG,SAAS,GAAG,QAAQ,GAAG,SAAS,CAAC;AAE7D;;GAEG;AACH,MAAM,MAAM,eAAe,GAAG,QAAQ,GAAG,gBAAgB,GAAG,QAAQ,GAAG,gBAAgB,GAAG,QAAQ,GAAG,SAAS,CAAC;AAE/G;;GAEG;AACH,MAAM,WAAW,aAAa;IAC1B,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,gBAAgB,CAAC;IAC5B,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,eAAe,EAAE,MAAM,GAAG,IAAI,CAAC;IAC/B,MAAM,EAAE,eAAe,CAAC;CAC3B;AAED;;GAEG;AACH,MAAM,MAAM,YAAY,GAAG,IAAI,GAAG,IAAI,CAAC"}
package/dist/types.js ADDED
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":""}
package/package.json CHANGED
@@ -1,10 +1,33 @@
1
1
  {
2
2
  "name": "@memberjunction/geo-core",
3
- "version": "0.0.1",
4
- "description": "OIDC trusted publishing setup package for @memberjunction/geo-core",
5
- "keywords": [
6
- "oidc",
7
- "trusted-publishing",
8
- "setup"
9
- ]
3
+ "type": "module",
4
+ "version": "5.25.0",
5
+ "description": "MemberJunction: Core geo-spatial types, interfaces, and GeoCodeSyncService for geocoding lifecycle management",
6
+ "main": "dist/index.js",
7
+ "types": "dist/index.d.ts",
8
+ "files": [
9
+ "/dist"
10
+ ],
11
+ "scripts": {
12
+ "build": "tsc && tsc-alias -f",
13
+ "test": "vitest run",
14
+ "test:watch": "vitest",
15
+ "test:coverage": "vitest run --coverage"
16
+ },
17
+ "author": "MemberJunction.com",
18
+ "license": "ISC",
19
+ "dependencies": {
20
+ "@memberjunction/global": "5.25.0",
21
+ "@memberjunction/core": "5.25.0",
22
+ "@memberjunction/core-entities": "5.25.0"
23
+ },
24
+ "devDependencies": {
25
+ "@types/node": "^24.10.11",
26
+ "typescript": "^5.9.3",
27
+ "vitest": "^4.0.18"
28
+ },
29
+ "repository": {
30
+ "type": "git",
31
+ "url": "https://github.com/MemberJunction/MJ"
32
+ }
10
33
  }
package/README.md DELETED
@@ -1,45 +0,0 @@
1
- # @memberjunction/geo-core
2
-
3
- ## ⚠️ IMPORTANT NOTICE ⚠️
4
-
5
- **This package is created solely for the purpose of setting up OIDC (OpenID Connect) trusted publishing with npm.**
6
-
7
- This is **NOT** a functional package and contains **NO** code or functionality beyond the OIDC setup configuration.
8
-
9
- ## Purpose
10
-
11
- This package exists to:
12
- 1. Configure OIDC trusted publishing for the package name `@memberjunction/geo-core`
13
- 2. Enable secure, token-less publishing from CI/CD workflows
14
- 3. Establish provenance for packages published under this name
15
-
16
- ## What is OIDC Trusted Publishing?
17
-
18
- OIDC trusted publishing allows package maintainers to publish packages directly from their CI/CD workflows without needing to manage npm access tokens. Instead, it uses OpenID Connect to establish trust between the CI/CD provider (like GitHub Actions) and npm.
19
-
20
- ## Setup Instructions
21
-
22
- To properly configure OIDC trusted publishing for this package:
23
-
24
- 1. Go to [npmjs.com](https://www.npmjs.com/) and navigate to your package settings
25
- 2. Configure the trusted publisher (e.g., GitHub Actions)
26
- 3. Specify the repository and workflow that should be allowed to publish
27
- 4. Use the configured workflow to publish your actual package
28
-
29
- ## DO NOT USE THIS PACKAGE
30
-
31
- This package is a placeholder for OIDC configuration only. It:
32
- - Contains no executable code
33
- - Provides no functionality
34
- - Should not be installed as a dependency
35
- - Exists only for administrative purposes
36
-
37
- ## More Information
38
-
39
- For more details about npm's trusted publishing feature, see:
40
- - [npm Trusted Publishing Documentation](https://docs.npmjs.com/generating-provenance-statements)
41
- - [GitHub Actions OIDC Documentation](https://docs.github.com/en/actions/deployment/security-hardening-your-deployments/about-security-hardening-with-openid-connect)
42
-
43
- ---
44
-
45
- **Maintained for OIDC setup purposes only**