1 | const _ = {};
|
2 | _.remove = require('lodash/remove');
|
3 | _.findIndex = require('lodash/findIndex');
|
4 | _.filter = require('lodash/filter');
|
5 |
|
6 | let now = null;
|
7 |
|
8 | function appendZero(number) {
|
9 | 'use strict';
|
10 | return number > 9 ? number : 0 + '' + number;
|
11 | }
|
12 | function toString(date) {
|
13 | 'use strict';
|
14 | if(!date) {
|
15 | return date;
|
16 | }
|
17 | return date.getFullYear() + '-' + appendZero(date.getMonth() + 1) + '-' + appendZero(date.getDate());
|
18 | }
|
19 |
|
20 | function parse(stringDate) {
|
21 | 'use strict';
|
22 | if(!stringDate) {
|
23 | return undefined;
|
24 | }
|
25 | var parts = stringDate.split('-');
|
26 | return new Date(parseInt(parts[0], 10), appendZero(parseInt(parts[1], 10) - 1), appendZero(parseInt(parts[2], 10)));
|
27 | }
|
28 |
|
29 | function getNow() {
|
30 | 'use strict';
|
31 | if(now) {
|
32 | return toString(now);
|
33 | }
|
34 | return toString(new Date());
|
35 | }
|
36 |
|
37 | function setNow(newNow) {
|
38 | 'use strict';
|
39 | now = parse(newNow);
|
40 | }
|
41 |
|
42 | function stripTime(isoDateStr) {
|
43 | 'use strict';
|
44 | return isoDateStr ? isoDateStr.split('T')[0] : isoDateStr;
|
45 | }
|
46 |
|
47 | function isBeforeOrEqual(a, b) {
|
48 | 'use strict';
|
49 | return a === b || !b || (a !== null && a < b);
|
50 | }
|
51 |
|
52 | function isAfter(a, b) {
|
53 | 'use strict';
|
54 | return !isBeforeOrEqual(a, b);
|
55 | }
|
56 |
|
57 | function isAfterOrEqual(a, b) {
|
58 | 'use strict';
|
59 | return a === b || !a || (b !== null && a > b);
|
60 | }
|
61 |
|
62 | function isBefore(a, b) {
|
63 | 'use strict';
|
64 | return !isAfterOrEqual(a, b);
|
65 | }
|
66 |
|
67 | function getFirst(array) {
|
68 | 'use strict';
|
69 | _.remove(array, function (x) {
|
70 | return !x;
|
71 | });
|
72 | const sorted = array.sort(function (a, b) {
|
73 | return a < b ? -1 : 1;
|
74 | });
|
75 | return sorted[0];
|
76 | }
|
77 |
|
78 | function getLast(array) {
|
79 | 'use strict';
|
80 | const index = _.findIndex(array, function (x) {
|
81 | return !x;
|
82 | });
|
83 | if (index > -1) {
|
84 | return null;
|
85 | }
|
86 | const sorted = array.sort(function (a, b) {
|
87 | return a < b ? -1 : 1;
|
88 | });
|
89 | return sorted[sorted.length - 1];
|
90 | }
|
91 |
|
92 | function isOverlapping(a, b) {
|
93 | 'use strict';
|
94 | return isBefore(a.startDate, b.endDate) && isAfter(a.endDate, b.startDate);
|
95 | }
|
96 |
|
97 | function isCovering(a, b) {
|
98 | 'use strict';
|
99 | return isBeforeOrEqual(a.startDate, b.startDate) && isAfterOrEqual(a.endDate, b.endDate);
|
100 | }
|
101 |
|
102 | function isConsecutive(a, b) {
|
103 | return (a.endDate !== null && a.endDate === b.startDate) || (b.endDate !== null && b.endDate === a.startDate);
|
104 | }
|
105 |
|
106 | function isConsecutiveWithOneDayInBetween(a, b) {
|
107 | return (a.endDate !== null && getNextDay(a.endDate) === b.startDate) || (b.endDate !== null && getNextDay(b.endDate) === a.startDate);
|
108 | }
|
109 |
|
110 | function printDate(dateString) {
|
111 | return dateString.split('-').reverse().join('/');
|
112 | }
|
113 |
|
114 | const printFutureForPeriodic = (periodic) => {
|
115 | let ret = '';
|
116 | if (isAfter(periodic.startDate, getNow()) && periodic.endDate && isAfter(periodic.endDate, getNow())) {
|
117 | ret += ' (van ' + printDate(periodic.startDate) + ' tot ' + printDate(periodic.endDate) + ')';
|
118 | } else if (isAfter(periodic.startDate, getNow())) {
|
119 | ret += ' (vanaf ' + printDate(periodic.startDate) + ')';
|
120 | } else if (periodic.endDate && isAfter(periodic.endDate, getNow())) {
|
121 | ret += ' (tot ' + printDate(periodic.endDate) + ')';
|
122 | }
|
123 | return ret;
|
124 | };
|
125 |
|
126 | function getEndofSchoolYear(stringDate) {
|
127 | 'use strict';
|
128 | const date = parse(stringDate) || now || parse(getNow());
|
129 | var ret = null;
|
130 | if (date.getMonth() < 8) {
|
131 | ret = toString(new Date(date.getFullYear(), 8, 1));
|
132 | } else {
|
133 | ret = toString(new Date(date.getFullYear() + 1, 8, 1));
|
134 | }
|
135 | return ret;
|
136 | }
|
137 |
|
138 | function getStartofSchoolYear(stringDate) {
|
139 | 'use strict';
|
140 | const date = parse(stringDate) || now || parse(getNow());
|
141 | var ret = null;
|
142 | if (date.getMonth() < 8) {
|
143 | ret = toString(new Date(date.getFullYear() - 1, 8, 1));
|
144 | } else {
|
145 | ret = toString(new Date(date.getFullYear(), 8, 1));
|
146 | }
|
147 | return ret;
|
148 | }
|
149 |
|
150 | function getStartOfSchoolYearIncludingSummerGap(stringDate) {
|
151 | 'use strict';
|
152 | const date = parse(stringDate) || now || parse(getNow());
|
153 | var ret = null;
|
154 | if (date.getMonth() < 6) {
|
155 | ret = toString(new Date(date.getFullYear() - 1, 8, 1));
|
156 | } else {
|
157 | ret = toString(new Date(date.getFullYear(), 8, 1));
|
158 | }
|
159 | return ret;
|
160 | }
|
161 |
|
162 | function getClosestSchoolYearSwitch (stringDate) {
|
163 | const date = parse(stringDate) || now || parse(getNow());
|
164 | if(date.getMonth() < 2) {
|
165 | return toString(new Date(date.getFullYear() - 1, 8, 1));
|
166 | } else {
|
167 | return toString(new Date(date.getFullYear(), 8, 1));
|
168 | }
|
169 | };
|
170 |
|
171 | function getNextDay(date, nbOfDays = 1) {
|
172 | if (!date) {
|
173 | return date;
|
174 | }
|
175 | let nextDay = parse(date);
|
176 | nextDay.setDate(nextDay.getDate() + nbOfDays);
|
177 | return toString(nextDay);
|
178 | }
|
179 |
|
180 | function getPreviousDay(date, nbOfDays = 1) {
|
181 | if (!date) {
|
182 | return date;
|
183 | }
|
184 | let previousDay = parse(date);
|
185 | previousDay.setDate(previousDay.getDate() - nbOfDays);
|
186 | return toString(previousDay);
|
187 | }
|
188 |
|
189 | function getPreviousMonth(date, nbOfMonths = 1) {
|
190 | if(!date) {
|
191 | return date;
|
192 | }
|
193 | let previousMonth = parse(date);
|
194 | previousMonth.setMonth(previousMonth.getMonth() - nbOfMonths);
|
195 | return toString(previousMonth);
|
196 | }
|
197 |
|
198 | function getNextMonth(date, nbOfMonths = 1) {
|
199 | if(!date) {
|
200 | return date;
|
201 | }
|
202 | let nextYear = parse(date);
|
203 | nextYear.setMonth(nextYear.getMonth() + nbOfMonths);
|
204 | return toString(nextYear);
|
205 | }
|
206 |
|
207 | function getPreviousYear(date, nbOfYears = 1) {
|
208 | if(!date) {
|
209 | return date;
|
210 | }
|
211 | let previousYear = parse(date);
|
212 | previousYear.setFullYear(previousYear.getFullYear() - nbOfYears);
|
213 | return toString(previousYear);
|
214 | }
|
215 |
|
216 | function getNextYear(date, nbOfYears = 1) {
|
217 | if(!date) {
|
218 | return date;
|
219 | }
|
220 | let nextYear = parse(date);
|
221 | nextYear.setFullYear(nextYear.getFullYear() + nbOfYears);
|
222 | return toString(nextYear);
|
223 | }
|
224 |
|
225 | function getActiveResources(array, referenceDate = getNow()) {
|
226 | return array.filter(resource => {
|
227 | if(resource.$$expanded) {
|
228 | resource = resource.$$expanded;
|
229 | }
|
230 | return resource.startDate <= referenceDate && isAfter(resource.endDate, referenceDate);
|
231 | });
|
232 | };
|
233 |
|
234 | function getNonAbolishedResources(array, referenceDate = getNow()) {
|
235 | return array.filter(resource => {
|
236 | if(resource.$$expanded) {
|
237 | resource = resource.$$expanded;
|
238 | }
|
239 | return isAfter(resource.endDate, referenceDate);
|
240 | });
|
241 | };
|
242 |
|
243 | function getAbolishedResources(array, referenceDate = getNow()) {
|
244 | return array.filter(resource => {
|
245 | if(resource.$$expanded) {
|
246 | resource = resource.$$expanded;
|
247 | }
|
248 | return isBeforeOrEqual(resource.endDate, referenceDate);
|
249 | });
|
250 | };
|
251 |
|
252 |
|
253 |
|
254 |
|
255 |
|
256 |
|
257 |
|
258 |
|
259 |
|
260 |
|
261 |
|
262 |
|
263 |
|
264 |
|
265 |
|
266 |
|
267 |
|
268 |
|
269 |
|
270 |
|
271 |
|
272 |
|
273 |
|
274 |
|
275 |
|
276 |
|
277 |
|
278 |
|
279 |
|
280 |
|
281 |
|
282 |
|
283 |
|
284 |
|
285 |
|
286 |
|
287 |
|
288 |
|
289 |
|
290 |
|
291 |
|
292 |
|
293 |
|
294 | class DateError {
|
295 | constructor(message, body) {
|
296 | this.message = message;
|
297 | this.body = body;
|
298 | }
|
299 | }
|
300 |
|
301 | const adaptPeriod = function(resource, options, periodic, referenceOptions) {
|
302 | const onlyEnlargePeriod = referenceOptions && referenceOptions.onlyEnlargePeriod;
|
303 | const onlyShortenPeriod = referenceOptions && referenceOptions.onlyShortenPeriod;
|
304 | const intermediateStrategy = referenceOptions && referenceOptions.intermediateStrategy ? referenceOptions.intermediateStrategy : options.intermediateStrategy;
|
305 | const startDateChanged = options.oldStartDate && (
|
306 | (!onlyEnlargePeriod && !onlyShortenPeriod && options.oldStartDate !== resource.startDate) ||
|
307 | (onlyEnlargePeriod && !onlyShortenPeriod && isBefore(resource.startDate, options.oldStartDate)) ||
|
308 | (onlyShortenPeriod && !onlyEnlargePeriod && isAfter(resource.startDate, options.oldStartDate))
|
309 | );
|
310 | const endDateChanged =
|
311 | (!onlyEnlargePeriod && !onlyShortenPeriod && options.oldEndDate !== resource.endDate) ||
|
312 | (onlyEnlargePeriod && !onlyShortenPeriod && isAfter(resource.endDate, options.oldEndDate)) ||
|
313 | (onlyShortenPeriod && !onlyEnlargePeriod && isBefore(resource.endDate, options.oldEndDate));
|
314 |
|
315 | let ret = false;
|
316 |
|
317 | if(endDateChanged) {
|
318 | if(intermediateStrategy !== 'NONE' && isAfterOrEqual(periodic.startDate, resource.endDate)) {
|
319 | throw new DateError((periodic.$$meta ? periodic.$$meta.permalink : JSON.stringify(periodic)) + ' starts after the new endDate, ' + resource.endDate, {
|
320 | resource: resource,
|
321 | periodic: periodic,
|
322 | property: 'endDate',
|
323 | code: 'starts.after.new.end'
|
324 | });
|
325 | }
|
326 | if(intermediateStrategy !== 'NONE' && isAfter(periodic.endDate, resource.endDate) && isBefore(periodic.endDate, options.oldEndDate)) {
|
327 | if(intermediateStrategy === 'FORCE') {
|
328 | periodic.endDate = resource.endDate;
|
329 | ret = true;
|
330 | } else if(intermediateStrategy === 'ERROR') {
|
331 | throw new DateError(periodic.$$meta ? periodic.$$meta.permalink : JSON.stringify(periodic) + ' has an endDate ('+periodic.endDate+') in between the new endDate and the old endDate.', {
|
332 | resource: resource,
|
333 | periodic: periodic,
|
334 | property: 'endDate',
|
335 | code: 'ends.inbetween'
|
336 | });
|
337 | }
|
338 | } else if(periodic.endDate === options.oldEndDate) {
|
339 | periodic.endDate = resource.endDate;
|
340 | ret = true;
|
341 | }
|
342 | }
|
343 | if(startDateChanged) {
|
344 | if(intermediateStrategy !== 'NONE' && isBeforeOrEqual(periodic.endDate, resource.startDate)) {
|
345 | throw new DateError(periodic.$$meta ? periodic.$$meta.permalink : JSON.stringify(periodic) + ' ends before the new startDate, ' + resource.startDate, {
|
346 | resource: resource,
|
347 | periodic: periodic,
|
348 | property: 'startDate',
|
349 | code: 'ends.before.new.start'
|
350 | });
|
351 | }
|
352 | if(intermediateStrategy !== 'NONE' && periodic.startDate !== options.startDate && isAfter(periodic.startDate, options.oldStartDate) && isBefore(periodic.startDate, resource.startDate)) {
|
353 | if(intermediateStrategy === 'FORCE') {
|
354 | periodic.startDate = resource.startDate;
|
355 | ret = true;
|
356 | } else if(intermediateStrategy === 'ERROR') {
|
357 | throw new DateError(periodic.$$meta ? periodic.$$meta.permalink : JSON.stringify(periodic) + ' has a startDate ('+periodic.startDate+') in between the old startDate and the new startDate.', {
|
358 | resource: resource,
|
359 | periodic: periodic,
|
360 | property: 'startDate',
|
361 | code: 'starts.inbetween'
|
362 | });
|
363 | }
|
364 | } else if(periodic.startDate === options.oldStartDate) {
|
365 | periodic.startDate = resource.startDate;
|
366 | ret = true;
|
367 | }
|
368 | }
|
369 | return ret;
|
370 | };
|
371 |
|
372 | const getDependenciesForReference = async function(resource, reference, api) {
|
373 | reference.parameters = reference.parameters || {};
|
374 | if(reference.subResources) {
|
375 | reference.parameters.expand = reference.parameters.expand || '';
|
376 | reference.subResources.forEach(subResource => {
|
377 | if(reference.parameters.expand !== '') {
|
378 | reference.parameters.expand += ',';
|
379 | }
|
380 | reference.parameters.expand += 'results.'+subResource;
|
381 | });
|
382 | }
|
383 | if(reference.property) {
|
384 | reference.parameters[reference.property] = resource.$$meta.permalink;
|
385 | } else if(reference.commonReference) {
|
386 | reference.parameters[reference.commonReference] = resource[reference.commonReference].href;
|
387 |
|
388 |
|
389 | } else {
|
390 | throw new Error('You either have to add a reference, a commonProperty or a listOfHrefs to the configuration for references.');
|
391 | }
|
392 |
|
393 | let dependencies = await api.getAll(reference.href, reference.parameters, reference.options);
|
394 | if(reference.filter) {
|
395 | dependencies = dependencies.filter(reference.filter);
|
396 | }
|
397 | return dependencies;
|
398 | };
|
399 |
|
400 | const manageDateChanges = async function(resource, options, api) {
|
401 | options.intermediateStrategy = options.intermediateStrategy || 'ERROR';
|
402 | const startDateChanged = options.oldStartDate && options.oldStartDate !== resource.startDate;
|
403 | const endDateChanged = options.oldEndDate !== resource.endDate;
|
404 |
|
405 | if(!startDateChanged && !endDateChanged) {
|
406 | return null;
|
407 | }
|
408 |
|
409 | if(options.properties) {
|
410 | for(let property of options.properties) {
|
411 | if(Array.isArray(resource[property])) {
|
412 | for(let elem of resource[property]) {
|
413 | adaptPeriod(resource, options, elem);
|
414 | }
|
415 | } else {
|
416 | adaptPeriod(resource, options, resource[property]);
|
417 | }
|
418 | }
|
419 | }
|
420 |
|
421 | const ret = {};
|
422 |
|
423 | if(options.references) {
|
424 | if(!Array.isArray(options.references)) {
|
425 | options.references = [options.references];
|
426 | }
|
427 |
|
428 | const errors = [];
|
429 | for(let reference of options.references) {
|
430 | reference.parameters = reference.parameters || {};
|
431 | if(startDateChanged && !endDateChanged && !options.intermediateStrategy) {
|
432 | reference.parameters.startDate = options.oldStartDate;
|
433 | }
|
434 | const dependencies = await getDependenciesForReference(resource, reference, api);
|
435 | const changes = [];
|
436 | dependencies.forEach( (dependency, $index) => {
|
437 | const batchIndex = options.batch ? _.findIndex(options.batch, elem => elem.href === dependency.$$meta.permalink) : -1;
|
438 | const body = batchIndex === -1 ? dependency : options.batch[batchIndex].body;
|
439 |
|
440 | try {
|
441 | const changed = adaptPeriod(resource, options, body, reference);
|
442 | if(changed) {
|
443 | changes.push(body);
|
444 | if(options.batch && batchIndex === -1) {
|
445 | options.batch.push({
|
446 | href: body.$$meta.permalink,
|
447 | verb: 'PUT',
|
448 | body: body
|
449 | });
|
450 | }
|
451 | if(reference.subResources) {
|
452 | reference.subResources.forEach(subResource => {
|
453 | const subResourceChanged = adaptPeriod(resource, options, body[subResource].$$expanded, reference);
|
454 | if(subResourceChanged && options.batch && batchIndex === -1) {
|
455 | options.batch.push({
|
456 | href: body[subResource].href,
|
457 | verb: 'PUT',
|
458 | body: body[subResource].$$expanded
|
459 | });
|
460 | }
|
461 | });
|
462 | }
|
463 | }
|
464 | } catch (error) {
|
465 | if(error instanceof DateError) {
|
466 |
|
467 | const intermediateStrategy = reference.intermediateStrategy ? reference.intermediateStrategy : options.intermediateStrategy;
|
468 | if(intermediateStrategy === 'FORCE' && error.body.code === 'starts.after.new.end' && options.batch) {
|
469 | options.batch.push({
|
470 | href: body.$$meta.permalink,
|
471 | verb: 'DELETE'
|
472 | });
|
473 | } else {
|
474 | errors.push(error);
|
475 | }
|
476 | } else {
|
477 | throw error;
|
478 | }
|
479 | }
|
480 | });
|
481 | if(reference.alias) {
|
482 | ret[reference.alias] = changes;
|
483 | }
|
484 | }
|
485 | if(errors.length > 0) {
|
486 | throw new DateError('There are references with conflicting periods that can not be adapted.', errors);
|
487 | }
|
488 | }
|
489 |
|
490 | return ret;
|
491 | };
|
492 |
|
493 | const manageDeletes = async function(resource, options, api) {
|
494 | const ret = {};
|
495 |
|
496 | if(options.references) {
|
497 | if(!Array.isArray(options.references)) {
|
498 | options.references = [options.references];
|
499 | }
|
500 |
|
501 | for(let reference of options.references) {
|
502 | const dependencies = await getDependenciesForReference(resource, reference, api);
|
503 | dependencies.forEach( (dependency, $index) => {
|
504 | const batchIndex = options.batch ? _.findIndex(options.batch, elem => elem.href === dependency.$$meta.permalink) : -1;
|
505 | options.batch.push({
|
506 | href: dependency.$$meta.permalink,
|
507 | verb: 'DELETE'
|
508 | });
|
509 | if(batchIndex > -1) {
|
510 | options.batch.splice(batchIndex, 1);
|
511 | }
|
512 | if(reference.subResources) {
|
513 | reference.subResources.forEach(subResource => {
|
514 | options.batch.push({
|
515 | href: dependency[subResource].href,
|
516 | verb: 'DELETE'
|
517 | });
|
518 | });
|
519 | }
|
520 | });
|
521 | if(reference.alias) {
|
522 | ret[reference.alias] = dependencies;
|
523 | }
|
524 | }
|
525 | }
|
526 |
|
527 | return ret;
|
528 | };
|
529 |
|
530 | module.exports = {
|
531 | getNow: getNow,
|
532 | setNow: setNow,
|
533 | stripTime: stripTime,
|
534 | toString: toString,
|
535 | printDate: printDate,
|
536 | printFutureForPeriodic: printFutureForPeriodic,
|
537 | parse: parse,
|
538 | isBeforeOrEqual: isBeforeOrEqual,
|
539 | isAfterOrEqual: isAfterOrEqual,
|
540 | isBefore: isBefore,
|
541 | isAfter: isAfter,
|
542 | getFirst: getFirst,
|
543 | getLast: getLast,
|
544 | isOverlapping: isOverlapping,
|
545 | isCovering: isCovering,
|
546 | isConsecutive: isConsecutive,
|
547 | isConsecutiveWithOneDayInBetween: isConsecutiveWithOneDayInBetween,
|
548 | getStartOfSchoolYear: getStartofSchoolYear,
|
549 | getEndOfSchoolYear: getEndofSchoolYear,
|
550 | getStartOfSchoolYearIncludingSummerGap: getStartOfSchoolYearIncludingSummerGap,
|
551 | getClosestSchoolYearSwitch: getClosestSchoolYearSwitch,
|
552 | getPreviousDay: getPreviousDay,
|
553 | getNextDay: getNextDay,
|
554 | getPreviousMonth: getPreviousMonth,
|
555 | getNextMonth: getNextMonth,
|
556 | getPreviousYear: getPreviousYear,
|
557 | getNextYear: getNextYear,
|
558 | getActiveResources: getActiveResources,
|
559 | getNonAbolishedResources: getNonAbolishedResources,
|
560 | getAbolishedResources: getAbolishedResources,
|
561 |
|
562 |
|
563 | manageDateChanges: manageDateChanges,
|
564 | adaptPeriod: adaptPeriod,
|
565 | manageDeletes: manageDeletes,
|
566 | DateError: DateError
|
567 | };
|