@eventconnectors/ndtrc_model 1.0.5 → 1.0.6

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,14 @@
1
+ {
2
+ "permissions": {
3
+ "allow": [
4
+ "Bash(npm test)",
5
+ "Bash(node -e:*)",
6
+ "WebFetch(domain:github.com)",
7
+ "WebFetch(domain:api.github.com)",
8
+ "WebFetch(domain:raw.githubusercontent.com)",
9
+ "Bash(npm publish:*)"
10
+ ],
11
+ "deny": [],
12
+ "ask": []
13
+ }
14
+ }
package/CLAUDE.md ADDED
@@ -0,0 +1,54 @@
1
+ # CLAUDE.md
2
+
3
+ This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
4
+
5
+ ## Project Overview
6
+
7
+ This is the `@eventconnectors/ndtrc_model` npm package - a Node.js data model library for NDTRC (Nationale Databank Toerisme en Recreatie Centraal), the Dutch national tourism and recreation database. It provides JavaScript classes for representing tourism events, locations, and related entities.
8
+
9
+ ## Commands
10
+
11
+ - **Run tests**: `npm test` (uses Jest)
12
+ - **Install dependencies**: `npm install`
13
+
14
+ ## Architecture
15
+
16
+ ### Entry Point
17
+ - `index.js` - Exports all model classes from a single entry point
18
+
19
+ ### Model Classes (`src/model/`)
20
+
21
+ **Core Entry Types:**
22
+ - `FetchedEntry` - Raw data fetched from external sources (with `ExternalLocationInfo`)
23
+ - `ConvertedEntry` - Processed entry containing a validated `TRCItem`
24
+
25
+ **NDTRC Models (`src/model/ndtrc/`):**
26
+ - `TRCItem` - Main entity representing an event/location with static enums `WFStatus` and `EntityType`
27
+ - `TRCItemDetail` - Multilingual details (title, descriptions) per language
28
+ - `Calendar` - Complex scheduling with single dates, pattern dates, and exception handling
29
+ - `Location` - Location info with address and optional `LocationItem` reference
30
+ - `TRCItemCategories` - Category assignments for items
31
+
32
+ **Supporting Models:**
33
+ - `Address`, `ContactInfo`, `GISCoordinate` - Location/contact data
34
+ - `PriceElement`, `ExtraPriceInformation` - Pricing information
35
+ - `File`, `Performer`, `Promotion` - Media and marketing
36
+ - `Translations`, `RouteInfo`, `SubItemGroup`, `TRCItemGroup`, `TRCItemRelation`
37
+
38
+ ### Utilities (`src/util/`)
39
+ - `CategoryMap` - Maps external category names to NDTRC category IDs with associated tags
40
+ - `Categories` - NDTRC category ID lookup table
41
+
42
+ ### Key Patterns
43
+
44
+ 1. **Constructor-based initialization**: All model classes use destructured object parameters with defaults
45
+ 2. **Nested class instantiation**: Parent classes automatically instantiate child classes (e.g., `TRCItem` creates `Calendar`, `Location` instances)
46
+ 3. **Moment.js dates**: Date fields use `moment` for parsing and manipulation
47
+ 4. **Static enums**: Enums are defined as static properties on classes (e.g., `TRCItem.WFStatus`, `Calendar.CalendarType`)
48
+
49
+ ### Entity Types
50
+ - `EVENEMENT` - Event
51
+ - `LOCATIE` - Location/venue
52
+ - `EVENEMENTGROEP` - Event group
53
+ - `PLAATSREGIO` - Place/region
54
+ - `ROUTE` - Route
package/package.json CHANGED
@@ -1,17 +1,17 @@
1
1
  {
2
2
  "name": "@eventconnectors/ndtrc_model",
3
- "version": "1.0.5",
3
+ "version": "1.0.6",
4
4
  "main": "index.js",
5
5
  "scripts": {
6
6
  "test": "jest"
7
7
  },
8
8
  "dependencies": {
9
- "dayjs": "^1.11.13",
9
+ "dayjs": "^1.11.19",
10
10
  "moment": "^2.30.1",
11
11
  "xmlbuilder": "^15.1.1"
12
12
  },
13
13
  "devDependencies": {
14
- "jest": "^29.7.0"
14
+ "jest": "^30.2.0"
15
15
  },
16
16
  "private": false,
17
17
  "author": "wwwappz",
@@ -0,0 +1,327 @@
1
+ const {
2
+ TRCItem,
3
+ FetchedEntry: FetchedEntryModule,
4
+ ExternalLocationInfo,
5
+ ConvertedEntry,
6
+ Location,
7
+ Calendar: CalendarModule,
8
+ Contactinfo,
9
+ File,
10
+ PriceElement,
11
+ Performer,
12
+ TRCItemDetail,
13
+ Address,
14
+ GISCoordinate,
15
+ Categories,
16
+ CategoryMap,
17
+ } = require("../../index.js");
18
+
19
+ const { FetchedEntry } = FetchedEntryModule;
20
+ const { Calendar } = CalendarModule;
21
+
22
+ describe("TRCItem", () => {
23
+ test("should create with default values", () => {
24
+ const item = new TRCItem();
25
+ expect(item.trcid).toBe("");
26
+ expect(item.published).toBe(false);
27
+ expect(item.deleted).toBe(false);
28
+ expect(item.forceOverwrite).toBe(false);
29
+ });
30
+
31
+ test("should create with custom values", () => {
32
+ const item = new TRCItem({
33
+ trcid: "test123",
34
+ published: true,
35
+ forceOverwrite: true,
36
+ entitytype: TRCItem.EntityType.EVENEMENT,
37
+ });
38
+ expect(item.trcid).toBe("test123");
39
+ expect(item.published).toBe(true);
40
+ expect(item.forceOverwrite).toBe(true);
41
+ expect(item.entitytype).toBe("EVENEMENT");
42
+ });
43
+
44
+ test("should have all EntityType values", () => {
45
+ expect(TRCItem.EntityType.EVENEMENT).toBe("EVENEMENT");
46
+ expect(TRCItem.EntityType.LOCATIE).toBe("LOCATIE");
47
+ expect(TRCItem.EntityType.EVENEMENTGROEP).toBe("EVENEMENTGROEP");
48
+ expect(TRCItem.EntityType.PLAATSREGIO).toBe("PLAATSREGIO");
49
+ expect(TRCItem.EntityType.ROUTE).toBe("ROUTE");
50
+ expect(TRCItem.EntityType.VENUE).toBe("VENUE");
51
+ });
52
+
53
+ test("should have all WFStatus values", () => {
54
+ expect(TRCItem.WFStatus.DRAFT).toBe("draft");
55
+ expect(TRCItem.WFStatus.READY_FOR_VALIDATION).toBe("readyforvalidation");
56
+ expect(TRCItem.WFStatus.APPROVED).toBe("approved");
57
+ expect(TRCItem.WFStatus.REJECTED).toBe("rejected");
58
+ expect(TRCItem.WFStatus.DELETED).toBe("deleted");
59
+ expect(TRCItem.WFStatus.ARCHIVED).toBe("archived");
60
+ });
61
+
62
+ test("should create nested objects", () => {
63
+ const item = new TRCItem({
64
+ trcid: "test",
65
+ location: { label: "Test Location" },
66
+ contactinfo: { label: "Test Contact" },
67
+ performers: [{ label: "Artist 1" }],
68
+ files: [{ filename: "image.jpg" }],
69
+ trcItemDetails: [{ lang: "nl", title: "Test" }],
70
+ });
71
+ expect(item.location).toBeInstanceOf(Location);
72
+ expect(item.contactinfo).toBeInstanceOf(Contactinfo);
73
+ expect(item.performers).toHaveLength(1);
74
+ expect(item.files).toHaveLength(1);
75
+ expect(item.trcItemDetails).toHaveLength(1);
76
+ });
77
+ });
78
+
79
+ describe("Location", () => {
80
+ test("should create with default values", () => {
81
+ const loc = new Location();
82
+ expect(loc.label).toBe("");
83
+ expect(loc.address).toBeNull();
84
+ expect(loc.locationItem).toBeNull();
85
+ expect(loc.venueItem).toBeNull();
86
+ });
87
+
88
+ test("should create with locationItem and venueItem", () => {
89
+ const loc = new Location({
90
+ label: "Test",
91
+ locationItem: { id: "1", trcid: "loc1", text: "Location" },
92
+ venueItem: { id: "2", trcid: "venue1", text: "Venue" },
93
+ });
94
+ expect(loc.locationItem.id).toBe("1");
95
+ expect(loc.locationItem.trcid).toBe("loc1");
96
+ expect(loc.venueItem.id).toBe("2");
97
+ expect(loc.venueItem.trcid).toBe("venue1");
98
+ });
99
+
100
+ test("should create address", () => {
101
+ const loc = new Location({
102
+ address: { city: "Amsterdam", street: "Damrak" },
103
+ });
104
+ expect(loc.address).toBeInstanceOf(Address);
105
+ expect(loc.address.city).toBe("Amsterdam");
106
+ });
107
+ });
108
+
109
+ describe("Calendar", () => {
110
+ test("should create with default values", () => {
111
+ const cal = new Calendar();
112
+ expect(cal.singleDates).toEqual([]);
113
+ expect(cal.patternDates).toEqual([]);
114
+ expect(cal.excludeholidays).toBe(false);
115
+ expect(cal.cancelled).toBe(false);
116
+ expect(cal.soldout).toBe(false);
117
+ expect(cal.onrequest).toBe(false);
118
+ expect(cal.alwaysopen).toBe(false);
119
+ expect(cal.comment).toBeNull();
120
+ expect(cal.calendarType).toBeNull();
121
+ });
122
+
123
+ test("should have CalendarType enum", () => {
124
+ expect(Calendar.CalendarType.NONE).toBe("NONE");
125
+ expect(Calendar.CalendarType.ALWAYSOPEN).toBe("ALWAYSOPEN");
126
+ expect(Calendar.CalendarType.ONREQUEST).toBe("ONREQUEST");
127
+ expect(Calendar.CalendarType.OPENINGTIMES).toBe("OPENINGTIMES");
128
+ expect(Calendar.CalendarType.PATTERNDATES).toBe("PATTERNDATES");
129
+ expect(Calendar.CalendarType.SINGLEDATES).toBe("SINGLEDATES");
130
+ });
131
+
132
+ test("should determine ALWAYSOPEN calendar type", () => {
133
+ const cal = new Calendar();
134
+ cal.alwaysopen = true;
135
+ cal.determineCalendarType();
136
+ expect(cal.calendarType).toBe("ALWAYSOPEN");
137
+ });
138
+
139
+ test("should determine ONREQUEST calendar type", () => {
140
+ const cal = new Calendar();
141
+ cal.onrequest = true;
142
+ cal.determineCalendarType();
143
+ expect(cal.calendarType).toBe("ONREQUEST");
144
+ });
145
+ });
146
+
147
+ describe("Contactinfo", () => {
148
+ test("should create with default values", () => {
149
+ const contact = new Contactinfo();
150
+ expect(contact.label).toBe("");
151
+ expect(contact.mails).toEqual([]);
152
+ expect(contact.phones).toEqual([]);
153
+ expect(contact.urls).toEqual([]);
154
+ });
155
+
156
+ test("should create nested Mail objects", () => {
157
+ const contact = new Contactinfo({
158
+ mails: [{ email: "test@example.com" }],
159
+ });
160
+ expect(contact.mails).toHaveLength(1);
161
+ expect(contact.mails[0].email).toBe("test@example.com");
162
+ });
163
+
164
+ test("should create nested Url objects", () => {
165
+ const contact = new Contactinfo({
166
+ urls: [{ url: "https://example.com", urlServiceType: "general" }],
167
+ });
168
+ expect(contact.urls).toHaveLength(1);
169
+ expect(contact.urls[0].url).toBe("https://example.com");
170
+ });
171
+
172
+ test("should have all URLServiceType values", () => {
173
+ const types = Contactinfo.Url.URLServiceType;
174
+ expect(types.general).toBe("general");
175
+ expect(types.booking).toBe("booking");
176
+ expect(types.review).toBe("review");
177
+ expect(types.video).toBe("video");
178
+ expect(types.webshop).toBe("webshop");
179
+ expect(types.socialmedia).toBe("socialmedia");
180
+ expect(types.lastminute).toBe("lastminute");
181
+ expect(types.virtualtour).toBe("virtualtour");
182
+ expect(types.dmo).toBe("dmo");
183
+ expect(types.sustainability).toBe("sustainability");
184
+ expect(types.venuefinder).toBe("venuefinder");
185
+ expect(types.travelbase).toBe("travelbase");
186
+ });
187
+ });
188
+
189
+ describe("File", () => {
190
+ test("should create with default values", () => {
191
+ const file = new File();
192
+ expect(file.trcid).toBe("");
193
+ expect(file.main).toBe(false);
194
+ expect(file.filename).toBe("");
195
+ });
196
+
197
+ test("should have FileType enum", () => {
198
+ expect(File.FileType.jpeg).toBe("jpeg");
199
+ expect(File.FileType.youtube).toBe("youtube");
200
+ expect(File.FileType.vimeo).toBe("vimeo");
201
+ });
202
+
203
+ test("should have MediaType enum", () => {
204
+ expect(File.MediaType.photo).toBe("photo");
205
+ expect(File.MediaType.video).toBe("video");
206
+ expect(File.MediaType.poster).toBe("poster");
207
+ });
208
+
209
+ test("should normalize YouTube URL", () => {
210
+ const shortUrl = "https://youtu.be/dQw4w9WgXcQ";
211
+ const normalized = File.normalizeYouTubeURL(shortUrl);
212
+ expect(normalized).toBe("https://www.youtube.com/watch?v=dQw4w9WgXcQ");
213
+ });
214
+
215
+ test("should extract YouTube video ID", () => {
216
+ const url = "https://www.youtube.com/watch?v=dQw4w9WgXcQ";
217
+ const videoId = File.youtubeVideoID(url);
218
+ expect(videoId).toBe("dQw4w9WgXcQ");
219
+ });
220
+ });
221
+
222
+ describe("PriceElement", () => {
223
+ test("should create with default values", () => {
224
+ const price = new PriceElement();
225
+ expect(price.freeentrance).toBe(false);
226
+ expect(price.priceValue).toBeNull();
227
+ expect(price.comments).toEqual([]);
228
+ });
229
+
230
+ test("should create with price value", () => {
231
+ const price = new PriceElement({
232
+ freeentrance: false,
233
+ priceValue: { from: 10.0, until: 25.0 },
234
+ });
235
+ expect(price.priceValue.from).toBe(10.0);
236
+ expect(price.priceValue.until).toBe(25.0);
237
+ });
238
+
239
+ test("should have PriceDescriptionValue enum", () => {
240
+ const values = PriceElement.Description.PriceDescriptionValue;
241
+ expect(values.Adults).toBe("Adults");
242
+ expect(values.Children).toBe("Children");
243
+ expect(values.Groups).toBe("Groups");
244
+ expect(values.CJP).toBe("CJP");
245
+ });
246
+ });
247
+
248
+ describe("FetchedEntry", () => {
249
+ test("should create with default values", () => {
250
+ const entry = new FetchedEntry();
251
+ expect(entry.feed).toBe("");
252
+ expect(entry.sourceId).toBe("");
253
+ expect(entry.externalId).toBe("");
254
+ });
255
+
256
+ test("should create with custom values", () => {
257
+ const entry = new FetchedEntry({
258
+ feed: "test-feed",
259
+ sourceId: "source-123",
260
+ externalId: "ext-456",
261
+ data: { key: "value" },
262
+ });
263
+ expect(entry.feed).toBe("test-feed");
264
+ expect(entry.sourceId).toBe("source-123");
265
+ expect(entry.data.key).toBe("value");
266
+ });
267
+ });
268
+
269
+ describe("ConvertedEntry", () => {
270
+ test("should create with default values", () => {
271
+ const entry = new ConvertedEntry();
272
+ expect(entry.label).toBe("");
273
+ expect(entry.externalId).toBe("");
274
+ expect(entry.trcItem).toBeNull();
275
+ });
276
+
277
+ test("should accept TRCItem instance", () => {
278
+ const trcItem = new TRCItem({ trcid: "test" });
279
+ const entry = new ConvertedEntry({
280
+ label: "Test Entry",
281
+ trcItem: trcItem,
282
+ });
283
+ expect(entry.trcItem).toBe(trcItem);
284
+ });
285
+ });
286
+
287
+ describe("Address", () => {
288
+ test("should create with default values", () => {
289
+ const address = new Address();
290
+ expect(address.city).toBe("");
291
+ expect(address.country).toBe("NL");
292
+ expect(address.main).toBe(false);
293
+ });
294
+
295
+ test("should normalize zipcode", () => {
296
+ const address = new Address({ zipcode: "1234ab" });
297
+ address.normaliseAdresItems();
298
+ expect(address.zipcode).toBe("1234 AB");
299
+ });
300
+
301
+ test("should check isEmpty", () => {
302
+ const emptyAddress = new Address();
303
+ const filledAddress = new Address({ city: "Amsterdam" });
304
+ expect(emptyAddress.isEmpty()).toBe(true);
305
+ expect(filledAddress.isEmpty()).toBe(false);
306
+ });
307
+ });
308
+
309
+ describe("Categories", () => {
310
+ test("should have category mappings", () => {
311
+ expect(Categories.categories).toBeDefined();
312
+ expect(Categories.categories["Beurs"]).toBe("2.3.1");
313
+ expect(Categories.categories["Film"]).toBe("2.5.2");
314
+ });
315
+ });
316
+
317
+ describe("CategoryMap", () => {
318
+ test("should have categoryMap", () => {
319
+ expect(CategoryMap.CategoryMap.categoryMap).toBeDefined();
320
+ expect(CategoryMap.CategoryMap.categoryMap["concert"].catId).toBe("2.6.5");
321
+ });
322
+
323
+ test("should have categoryType", () => {
324
+ expect(CategoryMap.CategoryMap.categoryType).toBeDefined();
325
+ expect(CategoryMap.CategoryMap.categoryType["Cabaret"]).toBe("2.9.1");
326
+ });
327
+ });
@@ -1,6 +1,7 @@
1
1
  // Calendar.js
2
2
 
3
3
  const moment = require("moment");
4
+ const Contactinfo = require("./ContactInfo");
4
5
 
5
6
  class Calendar {
6
7
  constructor() {
@@ -15,6 +16,10 @@ class Calendar {
15
16
  this.excludeholidays = false;
16
17
  this.cancelled = false;
17
18
  this.soldout = false;
19
+ this.onrequest = false;
20
+ this.alwaysopen = false;
21
+ this.comment = null;
22
+ this.calendarType = null;
18
23
  }
19
24
 
20
25
  static CalendarType = {
@@ -172,6 +177,7 @@ class When {
172
177
  this.status = status;
173
178
  this.statustranslations = [];
174
179
  this.extrainformations = [];
180
+ this.url = Contactinfo.Url ? new Contactinfo.Url() : [];
175
181
  }
176
182
 
177
183
  equals(other) {
@@ -170,7 +170,9 @@ Contactinfo.Url.URLServiceType = {
170
170
  lastminute: 'lastminute',
171
171
  virtualtour: 'virtualtour',
172
172
  dmo: 'dmo',
173
- sustainability: 'sustainability'
173
+ sustainability: 'sustainability',
174
+ venuefinder: 'venuefinder',
175
+ travelbase: 'travelbase'
174
176
  };
175
177
 
176
178
  /**
@@ -1,10 +1,11 @@
1
1
  const Address = require('./Address'); // Importeer de Address-klasse
2
2
 
3
3
  class Location {
4
- constructor({ address = null, label = '', locationItem = null } = {}) {
4
+ constructor({ address = null, label = '', locationItem = null, venueItem = null } = {}) {
5
5
  this.address = address ? new Address(address) : null;
6
6
  this.label = label;
7
7
  this.locationItem = locationItem ? new Location.LocationItem(locationItem) : null;
8
+ this.venueItem = venueItem ? new Location.LocationItem(venueItem) : null;
8
9
  }
9
10
  }
10
11
 
@@ -53,7 +53,8 @@ class TRCItem {
53
53
  extrapriceinformations = [],
54
54
  routeInfo = null,
55
55
  translations = null,
56
- promotions = []
56
+ promotions = [],
57
+ forceOverwrite = false
57
58
  } = {}) {
58
59
  this.trcid = trcid;
59
60
  this.creationdate = creationdate ? moment(creationdate) : null;
@@ -96,6 +97,7 @@ class TRCItem {
96
97
  this.routeInfo = routeInfo ? new RouteInfo(routeInfo) : null;
97
98
  this.translations = translations ? new Translations(translations) : new Translations();
98
99
  this.promotions = promotions.map(promotion => new Promotion(promotion));
100
+ this.forceOverwrite = forceOverwrite;
99
101
  }
100
102
  }
101
103
 
@@ -113,7 +115,8 @@ TRCItem.EntityType = {
113
115
  LOCATIE: 'LOCATIE',
114
116
  EVENEMENTGROEP: 'EVENEMENTGROEP',
115
117
  PLAATSREGIO: 'PLAATSREGIO',
116
- ROUTE: 'ROUTE'
118
+ ROUTE: 'ROUTE',
119
+ VENUE: 'VENUE'
117
120
  };
118
121
 
119
122
  TRCItem.Category = class Category {