├── LICENSE
├── README.md
└── files
├── Background Script
└── incident_mock.js
├── GraphQL Schema
└── Example.schema.gql
├── GraphQL Scripted Resolver
├── Resolver - Format Date.script.js
├── Resolver - Get Child Incidents.script.js
├── Resolver - Get User by id.script.js
├── Resolver - Get incident by number.script.js
└── Resolver - Get incidents by filter.script.js
└── Script Include
├── GraphQLExampleUtilities.script.js
├── moment.js.script.js
└── underscore.js.script.js
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2020 Marcus Reinhardt
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | > # NO SUPPORT - NO UPDATES
2 | > Since I switched the role inside the company, I'm not longer working with ServiceNow!
3 | > I'll keep this repo as boilerplate, but I mark it as archived, because I will not be able to add updates
4 | > Thanks for reading and trying this example!
5 |
6 | ---
7 |
8 | > # External usage
9 | >
10 | > If you're planning to create an howto, video, blog entry or something else which contains parts or everything from this repo,
11 | > **PLEASE** don't forget to add the link to this repo in your credits ;)
12 |
13 |
14 | # ServiceNow GraphQL Example
15 |
16 | - [ServiceNow GraphQL Example](#servicenow-graphql-example)
17 | - [What do we need](#what-do-we-need)
18 | - [How to use GraphQL in Service Portal](#how-to-use-graphql-in-service-portal)
19 | - [(Useful) Links](#useful-links)
20 | - [What is not included](#what-is-not-included)
21 | - [What is included](#what-is-included)
22 | - [Files explained](#files-explained)
23 | - [GraphQL Schema: Example](#graphql-schema-example)
24 | - [GraphQL Scripted Resolver: Resolver - Format Date](#graphql-scripted-resolver-resolver---format-date)
25 | - [GraphQL Scripted Resolver: Resolver - Get Child Incidents](#graphql-scripted-resolver-resolver---get-child-incidents)
26 | - [GraphQL Scripted Resolver: Resolver - Get incident by number](#graphql-scripted-resolver-resolver---get-incident-by-number)
27 | - [GraphQL Scripted Resolver: Resolver - Get incidents by filter](#graphql-scripted-resolver-resolver---get-incidents-by-filter)
28 | - [GraphQL Scripted Resolver: Resolver - Get User by id](#graphql-scripted-resolver-resolver---get-user-by-id)
29 | - [Script Include: GraphQLExampleUtilities](#script-include-graphqlexampleutilities)
30 | - [Script Include: moment.js](#script-include-momentjs)
31 | - [Script Include: _](#script-include-_)
32 | - [How To](#how-to)
33 | - [Build](#build)
34 | - [Create the script includes](#create-the-script-includes)
35 | - [Enable GraphQL](#enable-graphql)
36 | - [Create a new schema](#create-a-new-schema)
37 | - [Create the schema resolver](#create-the-schema-resolver)
38 | - [Create the resolver mappings](#create-the-resolver-mappings)
39 | - [Test](#test)
40 | - [Get one incident - Simple](#get-one-incident---simple)
41 | - [Get one incident - Advanced](#get-one-incident---advanced)
42 | - [Get all incidents - Simple](#get-all-incidents---simple)
43 | - [Get all incidents - Advanced](#get-all-incidents---advanced)
44 | - [GraphQL filters](#graphql-filters)
45 | - [Examples](#examples)
46 | - [GraphQL Paging & Sorting](#graphql-paging--sorting)
47 | - [Example - Paging](#example---paging)
48 | - [Example - Sorting](#example---sorting)
49 | - [Extend the schema](#extend-the-schema)
50 | - [Visual Studio Code](#visual-studio-code)
51 | - [Mock Generator](#mock-generator)
52 | - [Conclusion](#conclusion)
53 |
54 | ## What do we need
55 |
56 | * ServiceNow Developer Instance with Release: Paris
57 | * Postman, Insomnia, A REST/GraphQL Client
58 | * Javascript Knowledge
59 |
60 | ## (Useful) Links
61 |
62 | * [SN DEV - GraphQL Introduction](https://docs.servicenow.com/bundle/paris-release-notes/page/release-notes/now-platform-app-engine/web-services-rn.html)
63 | * [Postman](https://www.postman.com/)
64 | * [Insomnia](http://insomnia.rest/)
65 | * [MomentJS](https://www.momentjs.com)
66 | * [UnderscoreJS](https://www.underscorejs.org)
67 | * [GraphQL](https://graphql.org/)
68 |
69 | ## How to use GraphQL in Service Portal
70 |
71 | The guys from [serviceportal.io](https://www.serviceportal.io) created a very good video tutorial about "What is GraphQL" and how to use it in the ServiceNow Service Portal.
72 |
73 | You can find the video and their sources here: https://serviceportal.io/graphql-in-servicenow/
74 |
75 | ## What is not included
76 |
77 | I have excluded the mutation handling for this example.
78 | If you need an example for this, please install the Application "GraphQL Framework Demo Application" ( app id: com.glide.graphql.framework.demo ) via the internal SN App Store.
79 |
80 | ## What is included
81 | * Example to fetch one record
82 | * Example to fetch multiple records
83 | * Relationships
84 | * One-To-One (e.g. Opened By)
85 | * One-To-Many (e.g. Child Incidents)
86 | * Possibility to filter child incidents
87 | * Simple filter criteria handling
88 | * Reusable code => **DRY**
89 | * Paging
90 | * Sorting
91 |
92 |
93 | ## Files explained
94 |
95 |
96 |
97 | Click to expand
98 |
99 | ### GraphQL Schema: Example
100 |
101 | The complete GraphQL Schema ;)
102 |
103 | ### GraphQL Scripted Resolver: Resolver - Format Date
104 |
105 | Contains the code to convert a servicenow date/time string.
106 |
107 | > Behind the scenes: It calls the `getFormattedDate` method.
108 |
109 | ### GraphQL Scripted Resolver: Resolver - Get Child Incidents
110 |
111 | Contains the code to fetch multiple child incidents w/ or w/o filter criteria.
112 |
113 | > Behind the scenes: It calls the `generateQuery` and `getRecordList` with module `incident`.
114 | > There is a hardcoded filter criteria to ensure that `parent_incident` is always set.
115 |
116 | ### GraphQL Scripted Resolver: Resolver - Get incident by number
117 |
118 | Contains the code to fetch a incident based on the given number
119 |
120 | > Behind the scenes: It calls the `getRecord` method with module `incident` and the number
121 |
122 | ### GraphQL Scripted Resolver: Resolver - Get incidents by filter
123 |
124 | Contains the code to fetch multiple incidents w/ or w/o filter criteria.
125 |
126 | > Behind the scenes: It calls the `generateQuery` and `getRecordList` with module `incident`.
127 |
128 | ### GraphQL Scripted Resolver: Resolver - Get User by id
129 |
130 | Contains the code to fetch a user (e.g. opened by) in a incident.
131 |
132 | > Behind the scenes: It calls the `getRecord` method with module `user` and the sysid
133 |
134 | ### Script Include: GraphQLExampleUtilities
135 |
136 | We have one generic Script Include which contains all the logic.
137 | The Script Include handles the following:
138 |
139 | - Generating the Objects based on the GraphQL schema
140 | - Fetch one record
141 | - Fetch multiple records
142 | - Convert the given filter conditions into a valid servicenow query
143 | - Format a date value via `moment.js`
144 |
145 |
146 | ### Script Include: moment.js
147 |
148 | Moment.js in the version 2.20.1
149 |
150 | Source: https://github.com/moment/moment/releases/tag/2.20.1
151 |
152 | ### Script Include: underscore.js
153 |
154 | UnderscoreJS in the version: 1.8.2
155 | Duplicated from a existing Script Include.
156 |
157 |
158 |
159 |
160 | ----
161 |
162 | # How To
163 |
164 | ## Build
165 | ### Create the script includes
166 |
167 | As you can see in the `files/Script Include/` directory, there are three files which you have to create:
168 |
169 | | Script Include Name | Content |
170 | |------------------------------|--------------------------------------------------------------|
171 | | `underscore.js` | Use `files/Script Include/underscore.js.script.js` |
172 | | `moment.js` | Use `files/Script Include/moment.js.script.js` |
173 | | `GraphQLExampleUtilities` | Use `files/Script Include/GraphQLExampleUtilities.script.js` |
174 |
175 | ### Enable GraphQL
176 |
177 | In the navigator, go to `System Web Services > GraphQL > Properties`.
178 | In my example, I have activated all checkboxes.
179 |
180 | ### Create a new schema
181 |
182 | In the navigator, go to `System Web Services > GraphQL > GraphQL APIs`.
183 |
184 | Click the `New` button and fill the fields with the following values:
185 |
186 | > Use can use different values if you want ;)
187 |
188 | | Field | Value |
189 | |----------------------------|-----------------------------------------------|
190 | | Name | Example |
191 | | Schema namespace | example |
192 | | Active | Yes |
193 | | Schema | Use `files/GraphQL Schema/Example.schema.qgl` |
194 | | Requires authentication | Yes |
195 | | Requires ACL authorization | No |
196 |
197 |
198 | Click the `Submit` button to create the new GraphQL API.
199 |
200 | ### Create the schema resolver
201 |
202 | In your created schema, you should see now the tab `GraphQL Scripted Resolvers`.
203 |
204 | You have to create the following records:
205 |
206 | > Please make sure, that you update the scope name.
207 | > `x_116934_graphql` will not work in your instance.
208 |
209 | | Resolver Name | Content |
210 | |------------------------------------|-----------------------------------------------|
211 | | Resolver - Format Date | Use `files/GraphQL Scripted Resolver/Resolver - Format Date.script.js` |
212 | | Resolver - Get Child Incidents | Use `files/GraphQL Scripted Resolver/Resolver - Get Child Incidents.script.js` |
213 | | Resolver - Get incident by number | Use `files/GraphQL Scripted Resolver/Resolver - Get incident by number.script.js` |
214 | | Resolver - Get incidents by filter | Use `files/GraphQL Scripted Resolver/Resolver - Get incidents by filter.script.js` |
215 | | Resolver - Get User by id | Use `files/GraphQL Scripted Resolver/Resolver - Get User by id.script.js` |
216 |
217 | ### Create the resolver mappings
218 |
219 | In your created schema, you should see now the tab `GraphQL Resolver Mappings`.
220 |
221 | You have to create the following records:
222 |
223 | | Path | Resolver |
224 | |-------------------------|------------------------------------|
225 | | Query:incident | Resolver - Get incident by number |
226 | | Query:allIncident | Resolver - Get incidents by filter |
227 | | Incident:openedAt | Resolver - Format Date |
228 | | Incident:resolvedAt | Resolver - Format Date |
229 | | Incident:closedAt | Resolver - Format Date |
230 | | Incident:childIncidents | Resolver - Get Child Incidents |
231 | | Incident:parentIncident | Resolver - Get incident by number |
232 | | Incident:openedBy | Resolver - Get User by id |
233 | | Incident:resolvedBy | Resolver - Get User by id |
234 |
235 | ## Test
236 |
237 | All requests have the same config:
238 |
239 | * Method: `POST`
240 | * Endpoint: `https://.service-now.com/api/now/graphql`
241 | * Auth
242 | * Type: Basic
243 | * Username/Password: Only you know it ;)
244 |
245 | ### Get one incident - Simple
246 |
247 | Body:
248 |
249 | > Please make sure, that you replace `x116934Graphql` with your Application namespace.
250 | > You can find your Application namespace in the `GraphQL API` record.
251 |
252 | ```
253 | {
254 | x116934Graphql {
255 | example {
256 | incident(number: "INC0007001") {
257 | id
258 | number
259 | openedBy {
260 | id
261 | email
262 | }
263 | resolvedBy {
264 | id
265 | email
266 | }
267 | openedAt
268 | }
269 |
270 | }
271 | }
272 | }
273 | ```
274 |
275 | The result should be something like:
276 |
277 | ```json
278 | {
279 | "data": {
280 | "x116934Graphql": {
281 | "example": {
282 | "incident": {
283 | "id": "f12ca184735123002728660c4cf6a7ef",
284 | "number": "INC0007001",
285 | "openedBy": {
286 | "id": "6816f79cc0a8016401c5a33be04be441",
287 | "email": "admin@example.com"
288 | },
289 | "resolvedBy": null,
290 | "openedAt": "2018-10-17T12:47:10Z"
291 | }
292 | }
293 | }
294 | }
295 | }
296 | ```
297 |
298 | ### Get one incident - Advanced
299 |
300 | Body:
301 |
302 | > Please make sure, that you replace `x116934Graphql` with your Application namespace.
303 | > You can find your Application namespace in the `GraphQL API` record.
304 |
305 | ```gql
306 | {
307 | x116934Graphql {
308 | example {
309 | incident(number: "INC0007001") {
310 | id
311 | number
312 | state
313 | impact
314 | urgency
315 | priority
316 | openedBy {
317 | id
318 | email
319 | }
320 | resolvedBy {
321 | id
322 | }
323 | formattedOpenedAt : openedAt(format: "DD.MM.Y")
324 | openedAt
325 | resolvedAt
326 | closedAt
327 | parentIncident {
328 | id
329 | number
330 | openedAt
331 | }
332 | newChilds: childIncidents(filter:{state:{eq:NEW}}) {
333 | results {
334 | id
335 | number
336 | state
337 | parentIncident {
338 | number
339 | }
340 | openedBy {
341 | email
342 | }
343 | }
344 | }
345 | allChilds: childIncidents {
346 | results {
347 | id
348 | number
349 | state
350 | }
351 | }
352 | }
353 |
354 | }
355 | }
356 | }
357 | ```
358 |
359 | The result should be something like:
360 |
361 | ```json
362 | {
363 | "data": {
364 | "x116934Graphql": {
365 | "example": {
366 | "incident": {
367 | "id": "f12ca184735123002728660c4cf6a7ef",
368 | "number": "INC0007001",
369 | "state": "NEW",
370 | "impact": "HIGH",
371 | "urgency": "HIGH",
372 | "priority": "CRITICAL",
373 | "openedBy": {
374 | "id": "6816f79cc0a8016401c5a33be04be441",
375 | "email": "admin@example.com"
376 | },
377 | "resolvedBy": null,
378 | "formattedOpenedAt": "17.10.2018",
379 | "openedAt": "2018-10-17T12:47:10Z",
380 | "resolvedAt": null,
381 | "closedAt": null,
382 | "parentIncident": null,
383 | "newChilds": {
384 | "results": [{
385 | "id": "ff4c21c4735123002728660c4cf6a758",
386 | "number": "INC0007002",
387 | "state": "NEW",
388 | "parentIncident": {
389 | "number": "INC0007001"
390 | },
391 | "openedBy": {
392 | "email": "admin@example.com"
393 | }
394 | }]
395 | },
396 | "allChilds": {
397 | "results": [{
398 | "id": "46c03489a9fe19810148cd5b8cbf501e",
399 | "number": "INC0000011",
400 | "state": "CLOSED"
401 | },
402 | {
403 | "id": "e8caedcbc0a80164017df472f39eaed1",
404 | "number": "INC0000003",
405 | "state": "IN_PROGRESS"
406 | },
407 | {
408 | "id": "ff4c21c4735123002728660c4cf6a758",
409 | "number": "INC0007002",
410 | "state": "NEW"
411 | }
412 | ]
413 | }
414 | }
415 | }
416 | }
417 | }
418 | }
419 | ```
420 |
421 | ### Get all incidents - Simple
422 |
423 | Body:
424 |
425 | > Please make sure, that you replace `x116934Graphql` with your Application namespace.
426 | > You can find your Application namespace in the `GraphQL API` record.
427 |
428 | ```gql
429 | {
430 | x116934Graphql {
431 | example {
432 | allIncident {
433 | rowCount
434 | results {
435 | id
436 | number
437 | }
438 | }
439 | }
440 | }
441 | }
442 | ```
443 |
444 | The result should be something like:
445 |
446 | ```json
447 | {
448 | "data": {
449 | "x116934Graphql": {
450 | "example": {
451 | "allIncident": {
452 | "rowCount": 1064,
453 | "results": [
454 | {
455 | "id": "1c741bd70b2322007518478d83673af3",
456 | "number": "INC0000060"
457 | },
458 | {
459 | "id": "1c832706732023002728660c4cf6a7b9",
460 | "number": "INC0009002"
461 | },
462 | //...
463 | ]
464 | }
465 | }
466 | }
467 | }
468 | }
469 | ```
470 |
471 | ### Get all incidents - Advanced
472 |
473 | Body:
474 |
475 | > Please make sure, that you replace `x116934Graphql` with your Application namespace.
476 | > You can find your Application namespace in the `GraphQL API` record.
477 |
478 | ```gql
479 | {
480 | x116934Graphql {
481 | example {
482 | allIncident(filter: {number: {in: ["INC0007001", "INC0007002"]}}) {
483 | rowCount
484 | results {
485 | id
486 | number
487 | parentIncident {
488 | number
489 | }
490 | }
491 | }
492 | }
493 | }
494 | }
495 |
496 | ```
497 |
498 | The result should be something like:
499 |
500 | ```json
501 | {
502 | "data": {
503 | "x116934Graphql": {
504 | "example": {
505 | "allIncident": {
506 | "rowCount": 2,
507 | "results": [
508 | {
509 | "id": "f12ca184735123002728660c4cf6a7ef",
510 | "number": "INC0007001",
511 | "parentIncident": null
512 | },
513 | {
514 | "id": "ff4c21c4735123002728660c4cf6a758",
515 | "number": "INC0007002",
516 | "parentIncident": {
517 | "number": "INC0007001"
518 | }
519 | }
520 | ]
521 | }
522 | }
523 | }
524 | }
525 | }
526 | ```
527 |
528 | ## GraphQL filters
529 |
530 | I'm a [Gridsome](https://www.gridsome.org) lover and here we have some builtin filters.
531 | I used these filters as startpoint for the implementation.
532 |
533 | Currently only the following filter operators are allowed:
534 |
535 | | GraphQL Operator | ServiceNow Operator |
536 | |------------------|---------------------|
537 | | eq | = |
538 | | ne | != |
539 | | in | IN |
540 | | nin | NOT IN |
541 | | lt | < |
542 | | lte | <= |
543 | | gt | > |
544 | | gte | >= |
545 | | between | BETWEEN |
546 |
547 | You can easily extend the query operators
548 | To do this, you have to
549 | 1. extend the `operators` definition in the `initialize` method
550 | - here you can decide between
551 | - the default value transformation (e.g. converting `[A,B]` to `A,B`)
552 | - or define your own logic which returns the needed the query part
553 | 2. create new inputs in the GraphQL schema
554 | 3. update the `IncidentQueryFilter` with the new searchable fields ( like `openedAt` )
555 |
556 | ### Examples
557 |
558 | * Use `allIncident` to find all incidents with state `NEW`(=1) and urgency `LOW` (=3):
559 |
560 | ```
561 | allIncident(filter:{state:{eq:NEW}, urgency: {eq:LOW}})
562 | ```
563 |
564 | * Use `allIncident` to find all incidents which have state `NEW` or `IN_PROGRESS` and have urgency `LOW` or `HIGH`
565 |
566 | ```
567 | allIncident(filter:{state:{in:[NEW, IN_PROGRESS]}, urgency: {in:[LOW, HIGH]}})
568 | ```
569 |
570 | * Use `allIncident` to find all incidents which have state `NEW` or `IN_PROGRESS` but the urgency is not `LOW` or `HIGH`
571 |
572 | ```
573 | allIncident(filter:{state:{in:[NEW, IN_PROGRESS]}, urgency: {nin:[LOW, HIGH]}})
574 | ```
575 |
576 | * Use `allIncident` to get all incidents between `INC0010000` and `INC0010010`
577 |
578 | ```
579 | allIncident(filter:{number:{between:{from:"INC0010000", to: "INC0010010"}}})
580 | ```
581 |
582 | ## GraphQL Paging & Sorting
583 |
584 | Since there is no `@paginate` directive in the ServiceNow GraphQL implementation,
585 | With this example you will get a simple implementation for
586 |
587 | * Paging - uses `chooseWindow()`
588 | * Sorting - uses `orderBy()` / `orderByDesc()`
589 |
590 |
591 | ### Example - Paging
592 |
593 | GraphQL Request:
594 |
595 | ```
596 | {
597 | x116934Graphql {
598 | example {
599 | allIncident(paginate: {perPage:10, page:2}) {
600 | rowCount
601 | pageInfo {
602 | totalPages
603 | currentPage
604 | }
605 | results {
606 | number
607 | }
608 | }
609 | }
610 | }
611 | }
612 |
613 | ```
614 |
615 | Response:
616 |
617 | ```json
618 | {
619 | "data": {
620 | "x116934Graphql": {
621 | "example": {
622 | "allIncident": {
623 | "rowCount": 10,
624 | "pageInfo": {
625 | "totalPages": 106,
626 | "currentPage": 2
627 | },
628 | "results": [{
629 | "number": "INC0010401"
630 | },
631 | {
632 | "number": "INC0010990"
633 | },
634 | //...
635 | ]
636 | }
637 | }
638 | }
639 | }
640 | }
641 | ```
642 |
643 | ### Example - Sorting
644 |
645 | GraphQL Request:
646 |
647 | ```
648 | {
649 | x116934Graphql {
650 | example {
651 | allIncident(paginate: {perPage:10, page:2}, sort: {by: "number", order:DESC}) {
652 | rowCount
653 | pageInfo {
654 | totalPages
655 | currentPage
656 | }
657 | results {
658 | number
659 | }
660 | }
661 | }
662 | }
663 | }
664 | ```
665 |
666 | Response:
667 |
668 | ```json
669 | {
670 | "data": {
671 | "x116934Graphql": {
672 | "example": {
673 | "allIncident": {
674 | "rowCount": 10,
675 | "pageInfo": {
676 | "totalPages": 106,
677 | "currentPage": 2
678 | },
679 | "results": [{
680 | "number": "INC0010991"
681 | },
682 | {
683 | "number": "INC0010990"
684 | },
685 | //...
686 | ]
687 | }
688 | }
689 | }
690 | }
691 | }
692 | ```
693 |
694 | ## Extend the schema
695 |
696 | In case you need other fields available in your schema, you have to do the following steps.
697 | In this example, we're using the `assigned_to` field.
698 |
699 | 1. Update the schema
700 |
701 | We have to create a new field inside the `Incident` type.
702 | Since all fields are lowerCamelCase, the field will be named as `assignedTo`.
703 |
704 | ```js
705 | resolvedBy: User @source(value: "assignedTo.value")
706 | ```
707 |
708 | We're using `User` as field type, otherwise we have no access to the user related fields.
709 |
710 | The addition `@source(value: "assignedTo.value")` is used to have access in the scripted resolver via `getSource()`.
711 |
712 | 2. Update the Script Include
713 |
714 | In the `GraphQLExampleUtilities.initialize` you have to update the `mapping` for the `incident` definition.
715 | Just add the following at the end:
716 |
717 | ```js
718 | assignedTo: { display: false, useQuery: false, field: 'assigned_to' },
719 | ```
720 |
721 | * The key ( `assignedTo` ) is the same as in your schema - It's important that the name is the same. Otherwise you will have a lot of errors ;)
722 | * `display` defines if `getDisplayValue` or `getValue` should be used.
723 | * `useQuery` defines if the database value should be replaced with the defined value in `queryBuilder`
724 | * `field` defines the field name in the database
725 |
726 | 3. Define a new schema mapping
727 |
728 | Like in the how to above, we need a new resolver mapping to get the related user information:
729 |
730 | | Path | Resolver |
731 | |-------------------------|------------------------------------|
732 | | Incident:assignedTo | Resolver - Get User by id |
733 |
734 | 4. You're done
735 |
736 | If you want to implement other fields like `service` or something else, please make sure
737 |
738 | * you have created a `Scripted Resolver`
739 | * you have defined a new `type` in the schema
740 |
741 | # Visual Studio Code
742 |
743 | If you're using the ServiceNow VSC Extension, here the content of my `app.config.json` for the GraphQL tables:
744 |
745 | ```json
746 | {
747 | "CustomFileTypes": {
748 | "sys_graphql_typeresolver": {
749 | "superCoverName": "Miscellaneous",
750 | "create": "no",
751 | "coverName": "GraphQL Type Resolver",
752 | "tags": {
753 | "script": "js"
754 | }
755 | },
756 | "sys_graphql_resolver": {
757 | "superCoverName": "Miscellaneous",
758 | "create": "no",
759 | "coverName": "GraphQL Scripted Resolver",
760 | "tags": {
761 | "script": "js"
762 | }
763 | },
764 | "sys_graphql_schema": {
765 | "superCoverName": "Miscellaneous",
766 | "create": "no",
767 | "coverName": "GraphQL Schema",
768 | "tags": {
769 | "schema": "gql"
770 | }
771 | }
772 | }
773 | }
774 | ```
775 |
776 | # Mock Generator
777 |
778 | I created a small background script, which generates the configured amount of incidents with some basic information.
779 |
780 | You can find the script here: `files/Background Script/incident_mock.js`
781 |
782 | # Conclusion
783 |
784 | I personally love GraphQL and the benefits to have only one API instead of creating new API versions all the time.
785 | Also to have the possibility as consumer to define only the required fields helps to reduce the response size.
786 |
787 |
788 |
--------------------------------------------------------------------------------
/files/Background Script/incident_mock.js:
--------------------------------------------------------------------------------
1 | gs.include('_')
2 |
3 | var urgency = [ 1, 2, 3 ]
4 | var impact = [ 1, 2, 3 ]
5 | var contactType = [ 'email', 'phone' ]
6 | var category = [ 'software', 'hardware', 'network', 'database' ]
7 | var state = [ 1, 2, 3 ]
8 |
9 | var user = [
10 | '77ad8176731313005754660c4cf6a7de', //david miller
11 | '62826bf03710200044e0bfc8bcbe5df1', //abel tuter
12 | '71826bf03710200044e0bfc8bcbe5d3b' //aileen mottern
13 | ]
14 |
15 | /**
16 | * Generates 100 incidents
17 | * First incident has the title ... #1
18 | */
19 | createIncident(1, 100)
20 |
21 | /**
22 | * Generates 100 incidents
23 | * first incident has the title ... #101
24 | */
25 | //createIncident(101, 100)
26 |
27 | /**
28 | * Creates some dummy incidents based on the above defined values
29 | *
30 | * @param {Integer} start The number which should be use as start counter
31 | * @param {*} max The amount of incidents which should be create
32 | *
33 | * @return {void}
34 | */
35 | function createIncident(start, max) {
36 |
37 | for (var count = start; count <= max; count++) {
38 | var gr = new GlideRecord('incident')
39 | gr.newRecord();
40 |
41 | gr.setValue('caller_id', _.sample(user))
42 | gr.setValue('category', _.sample(category))
43 | gr.setValue('contact_type', _.sample(contactType))
44 | gr.setValue('state', _.sample(state))
45 | gr.setValue('impact', _.sample(impact))
46 | gr.setValue('urgency', _.sample(urgency))
47 | gr.setValue('short_description', 'Mock Incident #' + count)
48 | gr.setValue('description', 'This is a mock incident to test the QGL performance')
49 |
50 | var insertResult = gr.insert()
51 | }
52 | }
--------------------------------------------------------------------------------
/files/GraphQL Schema/Example.schema.gql:
--------------------------------------------------------------------------------
1 | schema {
2 | query: Query
3 | }
4 |
5 | type Query {
6 | allIncident(
7 | filter: IncidentQueryFilter
8 | paginate: QueryPaginate
9 | sort: QuerySort
10 | ): IncidentResultList
11 | incident(number: String!): Incident
12 | }
13 |
14 | type Incident {
15 | id: ID!
16 | number: String!
17 | active: Boolean
18 | state: IncidentState @source(value: "state.value")
19 | priority: IncidentPriority @source(value: "priority.value")
20 | impact: IncidentImpact @source(value: "impact.value")
21 | urgency: IncidentUrgency @source(value: "urgency.value")
22 | description: String
23 | resolvedBy: User @source(value: "resolvedBy.value")
24 | openedBy: User @source(value: "openedBy.value")
25 | openedAt(format: String): String @source(value: "openedAt.value")
26 | resolvedAt(format: String): String @source(value: "resolvedAt.value")
27 | closedAt(format: String): String @source(value: "closedAt.value")
28 | parentIncident: Incident @source(value: "parentIncident.value")
29 | childIncidents(filter: IncidentQueryFilter): IncidentResultList
30 | @source(value: "childIncidents.value")
31 | }
32 |
33 | type User {
34 | id: String
35 | email: String
36 | }
37 |
38 | type IncidentResultList {
39 | rowCount: Int
40 | pageInfo: PageInfo
41 | results: [Incident]
42 | }
43 |
44 | type PageInfo {
45 | totalPages: Int
46 | currentPage: Int
47 | }
48 |
49 | input QueryPaginate {
50 | perPage: Int
51 | page: Int
52 | }
53 |
54 | input QuerySort {
55 | by: String!
56 | order: SortOrder
57 | }
58 |
59 | input IncidentQueryFilter {
60 | number: IncidentNumberOperatorInput
61 | state: IncidentStateOperatorInput
62 | contactType: IncidentContactTypeOperatorInput
63 | impact: IncidentImpactOperatorInput
64 | urgency: IncidentUrgencyOperatorInput
65 | priority: IncidentPriorityOperatorInput
66 | }
67 |
68 | enum SortOrder {
69 | ASC
70 | DESC
71 | }
72 | enum IncidentState {
73 | #Incident is logged but not yet triaged.
74 | NEW
75 | #Incident is assigned and is being investigated.
76 | IN_PROGRESS
77 | #The responsibility for the incident shifts temporarily to another entity to provide further information, evidence, or a resolution
78 | ON_HOLD
79 | #A satisfactory fix is provided for the incident to ensure that it does not occur again.
80 | RESOLVED
81 | #Incident is marked Closed after it is in the Resolved state for a specific duration and it is confirmed that the incident is satisfactorily resolved.
82 | CLOSED
83 | #Incident was triaged but found to be a duplicate incident, an unnecessary incident, or not an incident at all.
84 | CANCELED
85 | }
86 |
87 | enum IncidentContactType {
88 | EMAIL
89 | PHONE
90 | SELF_SERVICE
91 | WALK_IN
92 | }
93 |
94 | enum IncidentImpact {
95 | LOW
96 | MEDIUM
97 | HIGH
98 | }
99 |
100 | enum IncidentUrgency {
101 | LOW
102 | MEDIUM
103 | HIGH
104 | }
105 |
106 | enum IncidentPriority {
107 | PLANNING
108 | LOW
109 | MODERATE
110 | HIGH
111 | CRITICAL
112 | }
113 |
114 | input IDQueryOperatorInput {
115 | # Filter by property of (strict) equality.
116 | eq: ID
117 | # Filter by property not equal to provided value.
118 | ne: ID
119 | # Filter by property matching any of the provided values.
120 | in: [ID]
121 | }
122 |
123 | input StringQueryOperatorInput {
124 | # Filter by property of (strict) equality.
125 | eq: String
126 | # Filter by property not equal to provided value.
127 | ne: String
128 | # Filter by property matching any of the provided values.
129 | in: [String]
130 | }
131 |
132 | input StringBetweenQueryFilter {
133 | from: String!
134 | to: String!
135 | }
136 |
137 | input IncidentNumberOperatorInput {
138 | # Filter by property of (strict) equality.
139 | eq: String
140 | # Filter by property not equal to provided value.
141 | ne: String
142 | # Filter by property matching any of the provided values.
143 | in: [String]
144 | nin: [String]
145 | lte: String
146 | lt: String
147 | gt: String
148 | gte: String
149 | between: StringBetweenQueryFilter
150 | }
151 |
152 | input IncidentStateOperatorInput {
153 | # Filter by property of (strict) equality.
154 | eq: IncidentState
155 | # Filter by property not equal to provided value.
156 | ne: IncidentState
157 | # Filter by property matching any of the provided values.
158 | in: [IncidentState]
159 | }
160 |
161 | input IncidentContactTypeOperatorInput {
162 | # Filter by property of (strict) equality.
163 | eq: IncidentContactType
164 | # Filter by property not equal to provided value.
165 | ne: IncidentContactType
166 | # Filter by property matching any of the provided values.
167 | in: [IncidentContactType]
168 | nin: [String]
169 | }
170 |
171 | input IncidentUrgencyOperatorInput {
172 | # Filter by property of (strict) equality.
173 | eq: IncidentUrgency
174 | # Filter by property not equal to provided value.
175 | ne: IncidentUrgency
176 | # Filter by property matching any of the provided values.
177 | in: [IncidentUrgency]
178 | nin: [String]
179 | }
180 |
181 | input IncidentImpactOperatorInput {
182 | # Filter by property of (strict) equality.
183 | eq: IncidentImpact
184 | # Filter by property not equal to provided value.
185 | ne: IncidentImpact
186 | # Filter by property matching any of the provided values.
187 | in: [IncidentImpact]
188 | nin: [String]
189 | }
190 |
191 | input IncidentPriorityOperatorInput {
192 | # Filter by property of (strict) equality.
193 | eq: IncidentPriority
194 | # Filter by property not equal to provided value.
195 | ne: IncidentPriority
196 | # Filter by property matching any of the provided values.
197 | in: [IncidentPriority]
198 | nin: [String]
199 | }
200 |
--------------------------------------------------------------------------------
/files/GraphQL Scripted Resolver/Resolver - Format Date.script.js:
--------------------------------------------------------------------------------
1 | (function process(/*ResolverEnvironment*/ env) {
2 | var dateFormat = env.getArguments().format || '';
3 | var GraphQLUtils = new global.GraphQLExampleUtilities();
4 | return GraphQLUtils.getFormattedDate(env.getSource(), dateFormat);
5 | })(env);
6 |
--------------------------------------------------------------------------------
/files/GraphQL Scripted Resolver/Resolver - Get Child Incidents.script.js:
--------------------------------------------------------------------------------
1 | (function process(/*ResolverEnvironment*/ env) {
2 | var GraphQLUtils = new global.GraphQLExampleUtilities();
3 |
4 | //set parent incident sys id as hardcoded query
5 | var filter = { parentIncident: { eq: env.getSource()} };
6 | var query = GraphQLUtils.generateQuery('incident', filter);
7 |
8 | //if we have additional filter criterias
9 | var childFilter = (env.getArguments().filter != null) ? env.getArguments().filter : {};
10 | var childQuery = GraphQLUtils.generateQuery('incident', childFilter);
11 |
12 | //merge both queries
13 | var listQuery = ( !childQuery ) ? query : query+'^'+childQuery;
14 |
15 | return GraphQLUtils.getRecordList('incident', listQuery);
16 | })(env);
17 |
--------------------------------------------------------------------------------
/files/GraphQL Scripted Resolver/Resolver - Get User by id.script.js:
--------------------------------------------------------------------------------
1 | (function process(/*ResolverEnvironment*/ env) {
2 | var id = env.getArguments().id != null ? env.getArguments().id : env.getSource();
3 | if(!id) return null;
4 |
5 | var GraphQLUtils = new global.GraphQLExampleUtilities();
6 | return GraphQLUtils.getRecord('user', id);
7 | })(env);
8 |
--------------------------------------------------------------------------------
/files/GraphQL Scripted Resolver/Resolver - Get incident by number.script.js:
--------------------------------------------------------------------------------
1 | (function process(/*ResolverEnvironment*/ env) {
2 | var id = env.getArguments().number != null ? env.getArguments().number : env.getSource();
3 | if (!id) return null;
4 |
5 | var GraphQLUtils = new global.GraphQLExampleUtilities();
6 | return GraphQLUtils.getRecord('incident', id);
7 | })(env);
8 |
--------------------------------------------------------------------------------
/files/GraphQL Scripted Resolver/Resolver - Get incidents by filter.script.js:
--------------------------------------------------------------------------------
1 | (function process(/*ResolverEnvironment*/ env) {
2 | var GraphQLUtils = new global.GraphQLExampleUtilities();
3 | var filter = (env.getArguments().filter != null) ? env.getArguments().filter : {};
4 | var paginate = (env.getArguments().paginate != null) ? env.getArguments().paginate : false;
5 | var sort = (env.getArguments().sort != null) ? env.getArguments().sort : false;
6 | var query = GraphQLUtils.generateQuery('incident', filter);
7 | return GraphQLUtils.getRecordList('incident', query, paginate, sort);
8 | })(env);
9 |
--------------------------------------------------------------------------------
/files/Script Include/GraphQLExampleUtilities.script.js:
--------------------------------------------------------------------------------
1 | /**
2 | *
3 | * REPOSITORY: https://github.com/noxify/ServiceNow-GraphQL-Example
4 | *
5 | * ---------------
6 | *
7 | * MIT License
8 | *
9 | * Copyright (c) 2020 Marcus Reinhardt
10 | *
11 | * Permission is hereby granted, free of charge, to any person obtaining a copy
12 | * of this software and associated documentation files (the "Software"), to deal
13 | * in the Software without restriction, including without limitation the rights
14 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
15 | * copies of the Software, and to permit persons to whom the Software is
16 | * furnished to do so, subject to the following conditions:
17 | *
18 | * The above copyright notice and this permission notice shall be included in all
19 | * copies or substantial portions of the Software.
20 | *
21 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
22 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
23 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
24 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
25 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
26 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
27 | * SOFTWARE.
28 | */
29 |
30 | gs.include('underscore.js');
31 | gs.include('moment.js');
32 |
33 | var GraphQLExampleUtilities = Class.create();
34 |
35 | GraphQLExampleUtilities.prototype = {
36 |
37 | type: 'GraphQLExampleUtilities',
38 |
39 | initialize: function () {
40 |
41 | //Table definition for each module
42 | this.table = {
43 | incident: {
44 | tableName: 'incident',
45 | queryField: 'number'
46 | },
47 | user: {
48 | tableName: 'sys_user',
49 | queryField: 'sys_id'
50 | }
51 | };
52 |
53 | //Mapping which will be used as graphl response
54 | this.mapping = {
55 | incident: {
56 | id: { display: false, useQuery: false, field: 'sys_id' },
57 | number: { display: false, useQuery: false, field: 'number' },
58 | openedBy: { display: false, useQuery: false, field: 'opened_by' },
59 | resolvedBy: { display: false, useQuery: false, field: 'resolved_by' },
60 | openedAt: { display: false, useQuery: false, field: 'opened_at' },
61 | resolvedAt: { display: false, useQuery: false, field: 'resolved_at' },
62 | closedAt: { display: false, useQuery: false, field: 'closed_at' },
63 | urgency: { display: false, useQuery: true, field: 'urgency' },
64 | impact: { display: false, useQuery: true, field: 'impact' },
65 | priority: { display: false, useQuery: true, field: 'priority' },
66 | contactType: { display: false, useQuery: true, field: 'contact_type' },
67 | state: { display: false, useQuery: true, field: 'state' },
68 | parentIncident: { display: true, useQuery: false, field: 'parent_incident' },
69 | childIncidents: { display: false, useQuery: false, field: 'sys_id' },
70 | },
71 |
72 | user: {
73 | id: { display: false, field: 'sys_id' },
74 | email: { display: false, field: 'email' }
75 | }
76 | };
77 |
78 | //Definition for the value replacement e.g. the GQL enums
79 | this.queryBuilder = {
80 | incident: {
81 | state: {
82 | NEW: 1,
83 | IN_PROGRESS: 2,
84 | ON_HOLD: 3,
85 | RESOLVED: 6,
86 | CLOSED: 7,
87 | CANCELED: 8,
88 | },
89 | contactType: {
90 | EMAIL: 'email',
91 | PHONE: 'phone',
92 | SELF_SERVICE: 'self-service',
93 | WALK_IN: 'walk-in'
94 | },
95 | impact: {
96 | LOW: 3,
97 | MEDIUM: 2,
98 | HIGH: 1
99 | },
100 | urgency: {
101 | LOW: 3,
102 | MEDIUM: 2,
103 | HIGH: 1
104 | },
105 | priority: {
106 | PLANNING: 5,
107 | LOW: 4,
108 | MODERATE: 3,
109 | HIGH: 2,
110 | CRITICAL: 1
111 | }
112 | }
113 | };
114 |
115 | //Definition for the filter operators
116 | this.operators = {
117 | 'eq': '=',
118 | 'ne': '!=',
119 | 'in': 'IN',
120 | 'nin': 'NOT IN',
121 | 'lt': '<',
122 | 'lte': '<=',
123 | 'gt': '>',
124 | 'gte': '>=',
125 | 'between': function (field, value) {
126 | return field + 'BETWEEN' + value.from + '@' + value.to;
127 | }
128 | };
129 | },
130 |
131 | /**
132 | * Generates the encoded query based on the given
133 | * module and filter criterias
134 | *
135 | * @param {String} module The module name e.g. incident or user
136 | * @param {Object} filter The filter criterias
137 | *
138 | * @return {String|null} The encoded query string or null
139 | */
140 | generateQuery: function (module, filter) {
141 |
142 | if (_.isNull(filter) || _.isUndefined(filter) || _.isEmpty(filter)) {
143 | return null;
144 | }
145 |
146 | var that = this;
147 | var query = _.map(filter, function (definition, field) {
148 |
149 | var targetField = (that.mapping[ module ][ field ]) ? that.mapping[ module ][ field ].field : field;
150 |
151 | return _.map(definition, function (value, op) {
152 | //check if the current operator is a function
153 | if (_.isFunction(that.operators[ op ])) {
154 | return that.operators[ op ](targetField, value);
155 | } else {
156 | //if not, run the default behavior
157 | value = that.convertQueryValue(module, field, value);
158 | return targetField + that.operators[ op ] + value;
159 | }
160 | });
161 | });
162 |
163 | query = _.flatten(query);
164 |
165 | return query.join('^');
166 | },
167 |
168 | /**
169 | * Converts the given query value.
170 | * It uses the definition from `queryBuilder` for the convert
171 | *
172 | * @example
173 | * convertQueryValue('incident', 'number', 'INC1234')
174 | * //returns: INC1234
175 | *
176 | * convertQueryValue('incident', 'state', ['NEW', 'IN_PROGRESS'])
177 | * //returns: 1,2
178 | *
179 | * convertQueryValue('incident', 'contactType', 'WALK_IN')
180 | * //returns: walk-in
181 | *
182 | * @param {String} module The current module
183 | * @param {String} field The current field name
184 | * @param {String|Array} value The value to convert
185 | *
186 | * @return {String} converted value
187 | */
188 | convertQueryValue: function (module, field, value) {
189 |
190 | var that = this;
191 | var newValue = (!_.isArray(value)) ? [ value ] : value;
192 |
193 | return _.map(newValue, function (fieldValue) {
194 | try {
195 | return that.queryBuilder[ module ][ field ][ fieldValue ];
196 | } catch (e) {
197 | return fieldValue;
198 | }
199 | }).join(',');
200 | },
201 |
202 | /**
203 | * Gets a list of records based on the given credentials
204 | * and converts it to the expected graphql object
205 | *
206 | * @param {String} module The current module e.g. incident or user
207 | * @param {String} query The query which should be execute
208 | * @param {Object|Boolean} paginate The pagination config
209 | * @param {Object|Boolean} sort The sort config
210 | *
211 | * @return {Array} The result with the generated objects
212 | */
213 | getRecordList: function (module, query, paginate, sort) {
214 |
215 | var moduleConfig = this.table[ module ];
216 |
217 | //count all records
218 | var grCount = new GlideAggregate(moduleConfig.tableName);
219 | grCount.addAggregate('COUNT');
220 | if (query) {
221 | grCount.addEncodedQuery(query);
222 | }
223 | grCount.query();
224 |
225 | var countResult = (grCount.next()) ? grCount.getAggregate('COUNT') : 0;
226 |
227 | //fetch the records
228 | var grRecord = new GlideRecord(moduleConfig.tableName);
229 |
230 | if (query) {
231 | grRecord.addEncodedQuery(query);
232 | }
233 |
234 | //add paging if defined
235 | if (paginate) {
236 | var rowStart = (paginate.perPage * paginate.page) - paginate.perPage;
237 | var rowEnd = (paginate.perPage * paginate.page);
238 | grRecord.chooseWindow(rowStart, rowEnd);
239 | }
240 |
241 | //add sorting if defined
242 | if (sort) {
243 | if (!sort.order || sort.order == 'ASC') {
244 | grRecord.orderBy(this.mapping[ module ][ sort.by ][ 'field' ]);
245 | } else {
246 | grRecord.orderByDesc(this.mapping[ module ][ sort.by ][ 'field' ]);
247 | }
248 | }
249 |
250 | grRecord.query();
251 | var records = [];
252 | while (grRecord.next()) {
253 | records.push(this.createResponseObject(grRecord, module, this.mapping[ module ]));
254 | }
255 |
256 | return {
257 | pageInfo: {
258 | totalPages: (paginate) ? Math.round(countResult / paginate.perPage) : 1,
259 | currentPage: (paginate) ? paginate.page : 1
260 | },
261 | rowCount: records.length,
262 | results: records
263 | };
264 | },
265 |
266 | /**
267 | * Get a record based on the given credentials
268 | * and converts it to the expected graphql object
269 | *
270 | * @param {String} module The current module e.g. incident or user
271 | * @param {String} value The value which should be used for the `get`
272 | *
273 | * @return {Object} The generated object from `createResponseObject`
274 | */
275 | getRecord: function (module, value) {
276 | var moduleConfig = this.table[ module ];
277 | var grRecord = new GlideRecord(moduleConfig.tableName);
278 | var exists = grRecord.get(moduleConfig.queryField, value);
279 | return (exists) ? this.createResponseObject(grRecord, module, this.mapping[ module ]) : null;
280 | },
281 |
282 | /**
283 | * Converts the given date string based on the given format
284 | * Also the given date will be returned as UTC date
285 | *
286 | * Valid formats can be found here:
287 | * https://momentjs.com/docs/#/displaying/format/
288 | *
289 | * @param {String} value The date/time string
290 | * @param {String} format The format which should be used
291 | *
292 | * @return {String} the formatted date
293 | */
294 | getFormattedDate: function (value, format) {
295 | if (!value) return null;
296 | var currentDate = moment(value);
297 | return currentDate.utc().format(format);
298 | },
299 |
300 | /**
301 | * Generates the graphql response object based
302 | * on the mapping definition
303 | *
304 | * @param {GlideRecord} grRecord The current Glide Record
305 | * @param {Object} objMapping The field mapping
306 | *
307 | * @return {Object}
308 | */
309 | createResponseObject: function (grRecord, module, objMapping) {
310 | var that = this;
311 | return _.mapObject(objMapping, function (fieldDef, propertyName) {
312 | var value = (fieldDef.display === true) ? grRecord.getDisplayValue(fieldDef.field) : grRecord.getValue(fieldDef.field);
313 | if (fieldDef.useQuery) {
314 | var selection = _.invert(that.queryBuilder[ module ][ propertyName ]);
315 | if (selection[ value ]) {
316 | value = selection[ value ];
317 | }
318 | }
319 | return value;
320 | });
321 | }
322 | };
323 |
--------------------------------------------------------------------------------
/files/Script Include/underscore.js.script.js:
--------------------------------------------------------------------------------
1 | // Underscore.js 1.8.2
2 | // http://underscorejs.org
3 | // (c) 2009-2015 Jeremy Ashkenas, DocumentCloud and Investigative Reporters & Editors
4 | // Underscore may be freely distributed under the MIT license.
5 |
6 | (function() {
7 |
8 | // Baseline setup
9 | // --------------
10 |
11 | // Establish the root object, `window` in the browser, or `exports` on the server.
12 | var root = this;
13 |
14 | // Save the previous value of the `_` variable.
15 | var previousUnderscore = root._;
16 |
17 | // Save bytes in the minified (but not gzipped) version:
18 | var ArrayProto = Array.prototype, ObjProto = Object.prototype, FuncProto = Function.prototype;
19 |
20 | // Create quick reference variables for speed access to core prototypes.
21 | var
22 | push = ArrayProto.push,
23 | slice = ArrayProto.slice,
24 | toString = ObjProto.toString,
25 | hasOwnProperty = ObjProto.hasOwnProperty;
26 |
27 | // All **ECMAScript 5** native function implementations that we hope to use
28 | // are declared here.
29 | var
30 | nativeIsArray = Array.isArray,
31 | nativeKeys = Object.keys,
32 | nativeBind = FuncProto.bind,
33 | nativeCreate = Object.create;
34 |
35 | // Naked function reference for surrogate-prototype-swapping.
36 | var Ctor = function(){};
37 |
38 | // Create a safe reference to the Underscore object for use below.
39 | var _ = function(obj) {
40 | if (obj instanceof _) return obj;
41 | if (!(this instanceof _)) return new _(obj);
42 | this._wrapped = obj;
43 | };
44 |
45 | // Export the Underscore object for **Node.js**, with
46 | // backwards-compatibility for the old `require()` API. If we're in
47 | // the browser, add `_` as a global object.
48 | if (typeof exports !== 'undefined') {
49 | if (typeof module !== 'undefined' && module.exports) {
50 | exports = module.exports = _;
51 | }
52 | exports._ = _;
53 | } else {
54 | root._ = _;
55 | }
56 |
57 | // Current version.
58 | _.VERSION = '1.8.2';
59 |
60 | // Internal function that returns an efficient (for current engines) version
61 | // of the passed-in callback, to be repeatedly applied in other Underscore
62 | // functions.
63 | var optimizeCb = function(func, context, argCount) {
64 | if (context === void 0) return func;
65 | switch (argCount == null ? 3 : argCount) {
66 | case 1: return function(value) {
67 | return func.call(context, value);
68 | };
69 | case 2: return function(value, other) {
70 | return func.call(context, value, other);
71 | };
72 | case 3: return function(value, index, collection) {
73 | return func.call(context, value, index, collection);
74 | };
75 | case 4: return function(accumulator, value, index, collection) {
76 | return func.call(context, accumulator, value, index, collection);
77 | };
78 | }
79 | return function() {
80 | return func.apply(context, arguments);
81 | };
82 | };
83 |
84 | // A mostly-internal function to generate callbacks that can be applied
85 | // to each element in a collection, returning the desired result — either
86 | // identity, an arbitrary callback, a property matcher, or a property accessor.
87 | var cb = function(value, context, argCount) {
88 | if (value == null) return _.identity;
89 | if (_.isFunction(value)) return optimizeCb(value, context, argCount);
90 | if (_.isObject(value)) return _.matcher(value);
91 | return _.property(value);
92 | };
93 | _.iteratee = function(value, context) {
94 | return cb(value, context, Infinity);
95 | };
96 |
97 | // An internal function for creating assigner functions.
98 | var createAssigner = function(keysFunc, undefinedOnly) {
99 | return function(obj) {
100 | var length = arguments.length;
101 | if (length < 2 || obj == null) return obj;
102 | for (var index = 1; index < length; index++) {
103 | var source = arguments[index],
104 | keys = keysFunc(source),
105 | l = keys.length;
106 | for (var i = 0; i < l; i++) {
107 | var key = keys[i];
108 | if (!undefinedOnly || obj[key] === void 0) obj[key] = source[key];
109 | }
110 | }
111 | return obj;
112 | };
113 | };
114 |
115 | // An internal function for creating a new object that inherits from another.
116 | var baseCreate = function(prototype) {
117 | if (!_.isObject(prototype)) return {};
118 | if (nativeCreate) return nativeCreate(prototype);
119 | Ctor.prototype = prototype;
120 | var result = new Ctor;
121 | Ctor.prototype = null;
122 | return result;
123 | };
124 |
125 | // Helper for collection methods to determine whether a collection
126 | // should be iterated as an array or as an object
127 | // Related: http://people.mozilla.org/~jorendorff/es6-draft.html#sec-tolength
128 | var MAX_ARRAY_INDEX = Math.pow(2, 53) - 1;
129 | var isArrayLike = function(collection) {
130 | var length = collection && collection.length;
131 | return typeof length == 'number' && length >= 0 && length <= MAX_ARRAY_INDEX;
132 | };
133 |
134 | // Collection Functions
135 | // --------------------
136 |
137 | // The cornerstone, an `each` implementation, aka `forEach`.
138 | // Handles raw objects in addition to array-likes. Treats all
139 | // sparse array-likes as if they were dense.
140 | _.each = _.forEach = function(obj, iteratee, context) {
141 | iteratee = optimizeCb(iteratee, context);
142 | var i, length;
143 | if (isArrayLike(obj)) {
144 | for (i = 0, length = obj.length; i < length; i++) {
145 | iteratee(obj[i], i, obj);
146 | }
147 | } else {
148 | var keys = _.keys(obj);
149 | for (i = 0, length = keys.length; i < length; i++) {
150 | iteratee(obj[keys[i]], keys[i], obj);
151 | }
152 | }
153 | return obj;
154 | };
155 |
156 | // Return the results of applying the iteratee to each element.
157 | _.map = _.collect = function(obj, iteratee, context) {
158 | iteratee = cb(iteratee, context);
159 | var keys = !isArrayLike(obj) && _.keys(obj),
160 | length = (keys || obj).length,
161 | results = Array(length);
162 | for (var index = 0; index < length; index++) {
163 | var currentKey = keys ? keys[index] : index;
164 | results[index] = iteratee(obj[currentKey], currentKey, obj);
165 | }
166 | return results;
167 | };
168 |
169 | // Create a reducing function iterating left or right.
170 | function createReduce(dir) {
171 | // Optimized iterator function as using arguments.length
172 | // in the main function will deoptimize the, see #1991.
173 | function iterator(obj, iteratee, memo, keys, index, length) {
174 | for (; index >= 0 && index < length; index += dir) {
175 | var currentKey = keys ? keys[index] : index;
176 | memo = iteratee(memo, obj[currentKey], currentKey, obj);
177 | }
178 | return memo;
179 | }
180 |
181 | return function(obj, iteratee, memo, context) {
182 | iteratee = optimizeCb(iteratee, context, 4);
183 | var keys = !isArrayLike(obj) && _.keys(obj),
184 | length = (keys || obj).length,
185 | index = dir > 0 ? 0 : length - 1;
186 | // Determine the initial value if none is provided.
187 | if (arguments.length < 3) {
188 | memo = obj[keys ? keys[index] : index];
189 | index += dir;
190 | }
191 | return iterator(obj, iteratee, memo, keys, index, length);
192 | };
193 | }
194 |
195 | // **Reduce** builds up a single result from a list of values, aka `inject`,
196 | // or `foldl`.
197 | _.reduce = _.foldl = _.inject = createReduce(1);
198 |
199 | // The right-associative version of reduce, also known as `foldr`.
200 | _.reduceRight = _.foldr = createReduce(-1);
201 |
202 | // Return the first value which passes a truth test. Aliased as `detect`.
203 | _.find = _.detect = function(obj, predicate, context) {
204 | var key;
205 | if (isArrayLike(obj)) {
206 | key = _.findIndex(obj, predicate, context);
207 | } else {
208 | key = _.findKey(obj, predicate, context);
209 | }
210 | if (key !== void 0 && key !== -1) return obj[key];
211 | };
212 |
213 | // Return all the elements that pass a truth test.
214 | // Aliased as `select`.
215 | _.filter = _.select = function(obj, predicate, context) {
216 | var results = [];
217 | predicate = cb(predicate, context);
218 | _.each(obj, function(value, index, list) {
219 | if (predicate(value, index, list)) results.push(value);
220 | });
221 | return results;
222 | };
223 |
224 | // Return all the elements for which a truth test fails.
225 | _.reject = function(obj, predicate, context) {
226 | return _.filter(obj, _.negate(cb(predicate)), context);
227 | };
228 |
229 | // Determine whether all of the elements match a truth test.
230 | // Aliased as `all`.
231 | _.every = _.all = function(obj, predicate, context) {
232 | predicate = cb(predicate, context);
233 | var keys = !isArrayLike(obj) && _.keys(obj),
234 | length = (keys || obj).length;
235 | for (var index = 0; index < length; index++) {
236 | var currentKey = keys ? keys[index] : index;
237 | if (!predicate(obj[currentKey], currentKey, obj)) return false;
238 | }
239 | return true;
240 | };
241 |
242 | // Determine if at least one element in the object matches a truth test.
243 | // Aliased as `any`.
244 | _.some = _.any = function(obj, predicate, context) {
245 | predicate = cb(predicate, context);
246 | var keys = !isArrayLike(obj) && _.keys(obj),
247 | length = (keys || obj).length;
248 | for (var index = 0; index < length; index++) {
249 | var currentKey = keys ? keys[index] : index;
250 | if (predicate(obj[currentKey], currentKey, obj)) return true;
251 | }
252 | return false;
253 | };
254 |
255 | // Determine if the array or object contains a given value (using `===`).
256 | // Aliased as `includes` and `include`.
257 | _.contains = _.includes = _.include = function(obj, target, fromIndex) {
258 | if (!isArrayLike(obj)) obj = _.values(obj);
259 | return _.indexOf(obj, target, typeof fromIndex == 'number' && fromIndex) >= 0;
260 | };
261 |
262 | // Invoke a method (with arguments) on every item in a collection.
263 | _.invoke = function(obj, method) {
264 | var args = slice.call(arguments, 2);
265 | var isFunc = _.isFunction(method);
266 | return _.map(obj, function(value) {
267 | var func = isFunc ? method : value[method];
268 | return func == null ? func : func.apply(value, args);
269 | });
270 | };
271 |
272 | // Convenience version of a common use case of `map`: fetching a property.
273 | _.pluck = function(obj, key) {
274 | return _.map(obj, _.property(key));
275 | };
276 |
277 | // Convenience version of a common use case of `filter`: selecting only objects
278 | // containing specific `key:value` pairs.
279 | _.where = function(obj, attrs) {
280 | return _.filter(obj, _.matcher(attrs));
281 | };
282 |
283 | // Convenience version of a common use case of `find`: getting the first object
284 | // containing specific `key:value` pairs.
285 | _.findWhere = function(obj, attrs) {
286 | return _.find(obj, _.matcher(attrs));
287 | };
288 |
289 | // Return the maximum element (or element-based computation).
290 | _.max = function(obj, iteratee, context) {
291 | var result = -Infinity, lastComputed = -Infinity,
292 | value, computed;
293 | if (iteratee == null && obj != null) {
294 | obj = isArrayLike(obj) ? obj : _.values(obj);
295 | for (var i = 0, length = obj.length; i < length; i++) {
296 | value = obj[i];
297 | if (value > result) {
298 | result = value;
299 | }
300 | }
301 | } else {
302 | iteratee = cb(iteratee, context);
303 | _.each(obj, function(value, index, list) {
304 | computed = iteratee(value, index, list);
305 | if (computed > lastComputed || computed === -Infinity && result === -Infinity) {
306 | result = value;
307 | lastComputed = computed;
308 | }
309 | });
310 | }
311 | return result;
312 | };
313 |
314 | // Return the minimum element (or element-based computation).
315 | _.min = function(obj, iteratee, context) {
316 | var result = Infinity, lastComputed = Infinity,
317 | value, computed;
318 | if (iteratee == null && obj != null) {
319 | obj = isArrayLike(obj) ? obj : _.values(obj);
320 | for (var i = 0, length = obj.length; i < length; i++) {
321 | value = obj[i];
322 | if (value < result) {
323 | result = value;
324 | }
325 | }
326 | } else {
327 | iteratee = cb(iteratee, context);
328 | _.each(obj, function(value, index, list) {
329 | computed = iteratee(value, index, list);
330 | if (computed < lastComputed || computed === Infinity && result === Infinity) {
331 | result = value;
332 | lastComputed = computed;
333 | }
334 | });
335 | }
336 | return result;
337 | };
338 |
339 | // Shuffle a collection, using the modern version of the
340 | // [Fisher-Yates shuffle](http://en.wikipedia.org/wiki/Fisher–Yates_shuffle).
341 | _.shuffle = function(obj) {
342 | var set = isArrayLike(obj) ? obj : _.values(obj);
343 | var length = set.length;
344 | var shuffled = Array(length);
345 | for (var index = 0, rand; index < length; index++) {
346 | rand = _.random(0, index);
347 | if (rand !== index) shuffled[index] = shuffled[rand];
348 | shuffled[rand] = set[index];
349 | }
350 | return shuffled;
351 | };
352 |
353 | // Sample **n** random values from a collection.
354 | // If **n** is not specified, returns a single random element.
355 | // The internal `guard` argument allows it to work with `map`.
356 | _.sample = function(obj, n, guard) {
357 | if (n == null || guard) {
358 | if (!isArrayLike(obj)) obj = _.values(obj);
359 | return obj[_.random(obj.length - 1)];
360 | }
361 | return _.shuffle(obj).slice(0, Math.max(0, n));
362 | };
363 |
364 | // Sort the object's values by a criterion produced by an iteratee.
365 | _.sortBy = function(obj, iteratee, context) {
366 | iteratee = cb(iteratee, context);
367 | return _.pluck(_.map(obj, function(value, index, list) {
368 | return {
369 | value: value,
370 | index: index,
371 | criteria: iteratee(value, index, list)
372 | };
373 | }).sort(function(left, right) {
374 | var a = left.criteria;
375 | var b = right.criteria;
376 | if (a !== b) {
377 | if (a > b || a === void 0) return 1;
378 | if (a < b || b === void 0) return -1;
379 | }
380 | return left.index - right.index;
381 | }), 'value');
382 | };
383 |
384 | // An internal function used for aggregate "group by" operations.
385 | var group = function(behavior) {
386 | return function(obj, iteratee, context) {
387 | var result = {};
388 | iteratee = cb(iteratee, context);
389 | _.each(obj, function(value, index) {
390 | var key = iteratee(value, index, obj);
391 | behavior(result, value, key);
392 | });
393 | return result;
394 | };
395 | };
396 |
397 | // Groups the object's values by a criterion. Pass either a string attribute
398 | // to group by, or a function that returns the criterion.
399 | _.groupBy = group(function(result, value, key) {
400 | if (_.has(result, key)) result[key].push(value); else result[key] = [value];
401 | });
402 |
403 | // Indexes the object's values by a criterion, similar to `groupBy`, but for
404 | // when you know that your index values will be unique.
405 | _.indexBy = group(function(result, value, key) {
406 | result[key] = value;
407 | });
408 |
409 | // Counts instances of an object that group by a certain criterion. Pass
410 | // either a string attribute to count by, or a function that returns the
411 | // criterion.
412 | _.countBy = group(function(result, value, key) {
413 | if (_.has(result, key)) result[key]++; else result[key] = 1;
414 | });
415 |
416 | // Safely create a real, live array from anything iterable.
417 | _.toArray = function(obj) {
418 | if (!obj) return [];
419 | if (_.isArray(obj)) return slice.call(obj);
420 | if (isArrayLike(obj)) return _.map(obj, _.identity);
421 | return _.values(obj);
422 | };
423 |
424 | // Return the number of elements in an object.
425 | _.size = function(obj) {
426 | if (obj == null) return 0;
427 | return isArrayLike(obj) ? obj.length : _.keys(obj).length;
428 | };
429 |
430 | // Split a collection into two arrays: one whose elements all satisfy the given
431 | // predicate, and one whose elements all do not satisfy the predicate.
432 | _.partition = function(obj, predicate, context) {
433 | predicate = cb(predicate, context);
434 | var pass = [], fail = [];
435 | _.each(obj, function(value, key, obj) {
436 | (predicate(value, key, obj) ? pass : fail).push(value);
437 | });
438 | return [pass, fail];
439 | };
440 |
441 | // Array Functions
442 | // ---------------
443 |
444 | // Get the first element of an array. Passing **n** will return the first N
445 | // values in the array. Aliased as `head` and `take`. The **guard** check
446 | // allows it to work with `_.map`.
447 | _.first = _.head = _.take = function(array, n, guard) {
448 | if (array == null) return void 0;
449 | if (n == null || guard) return array[0];
450 | return _.initial(array, array.length - n);
451 | };
452 |
453 | // Returns everything but the last entry of the array. Especially useful on
454 | // the arguments object. Passing **n** will return all the values in
455 | // the array, excluding the last N.
456 | _.initial = function(array, n, guard) {
457 | return slice.call(array, 0, Math.max(0, array.length - (n == null || guard ? 1 : n)));
458 | };
459 |
460 | // Get the last element of an array. Passing **n** will return the last N
461 | // values in the array.
462 | _.last = function(array, n, guard) {
463 | if (array == null) return void 0;
464 | if (n == null || guard) return array[array.length - 1];
465 | return _.rest(array, Math.max(0, array.length - n));
466 | };
467 |
468 | // Returns everything but the first entry of the array. Aliased as `tail` and `drop`.
469 | // Especially useful on the arguments object. Passing an **n** will return
470 | // the rest N values in the array.
471 | _.rest = _.tail = _.drop = function(array, n, guard) {
472 | return slice.call(array, n == null || guard ? 1 : n);
473 | };
474 |
475 | // Trim out all falsy values from an array.
476 | _.compact = function(array) {
477 | return _.filter(array, _.identity);
478 | };
479 |
480 | // Internal implementation of a recursive `flatten` function.
481 | var flatten = function(input, shallow, strict, startIndex) {
482 | var output = [], idx = 0;
483 | for (var i = startIndex || 0, length = input && input.length; i < length; i++) {
484 | var value = input[i];
485 | if (isArrayLike(value) && (_.isArray(value) || _.isArguments(value))) {
486 | //flatten current level of array or arguments object
487 | if (!shallow) value = flatten(value, shallow, strict);
488 | var j = 0, len = value.length;
489 | output.length += len;
490 | while (j < len) {
491 | output[idx++] = value[j++];
492 | }
493 | } else if (!strict) {
494 | output[idx++] = value;
495 | }
496 | }
497 | return output;
498 | };
499 |
500 | // Flatten out an array, either recursively (by default), or just one level.
501 | _.flatten = function(array, shallow) {
502 | return flatten(array, shallow, false);
503 | };
504 |
505 | // Return a version of the array that does not contain the specified value(s).
506 | _.without = function(array) {
507 | return _.difference(array, slice.call(arguments, 1));
508 | };
509 |
510 | // Produce a duplicate-free version of the array. If the array has already
511 | // been sorted, you have the option of using a faster algorithm.
512 | // Aliased as `unique`.
513 | _.uniq = _.unique = function(array, isSorted, iteratee, context) {
514 | if (array == null) return [];
515 | if (!_.isBoolean(isSorted)) {
516 | context = iteratee;
517 | iteratee = isSorted;
518 | isSorted = false;
519 | }
520 | if (iteratee != null) iteratee = cb(iteratee, context);
521 | var result = [];
522 | var seen = [];
523 | for (var i = 0, length = array.length; i < length; i++) {
524 | var value = array[i],
525 | computed = iteratee ? iteratee(value, i, array) : value;
526 | if (isSorted) {
527 | if (!i || seen !== computed) result.push(value);
528 | seen = computed;
529 | } else if (iteratee) {
530 | if (!_.contains(seen, computed)) {
531 | seen.push(computed);
532 | result.push(value);
533 | }
534 | } else if (!_.contains(result, value)) {
535 | result.push(value);
536 | }
537 | }
538 | return result;
539 | };
540 |
541 | // Produce an array that contains the union: each distinct element from all of
542 | // the passed-in arrays.
543 | _.union = function() {
544 | return _.uniq(flatten(arguments, true, true));
545 | };
546 |
547 | // Produce an array that contains every item shared between all the
548 | // passed-in arrays.
549 | _.intersection = function(array) {
550 | if (array == null) return [];
551 | var result = [];
552 | var argsLength = arguments.length;
553 | for (var i = 0, length = array.length; i < length; i++) {
554 | var item = array[i];
555 | if (_.contains(result, item)) continue;
556 | for (var j = 1; j < argsLength; j++) {
557 | if (!_.contains(arguments[j], item)) break;
558 | }
559 | if (j === argsLength) result.push(item);
560 | }
561 | return result;
562 | };
563 |
564 | // Take the difference between one array and a number of other arrays.
565 | // Only the elements present in just the first array will remain.
566 | _.difference = function(array) {
567 | var rest = flatten(arguments, true, true, 1);
568 | return _.filter(array, function(value){
569 | return !_.contains(rest, value);
570 | });
571 | };
572 |
573 | // Zip together multiple lists into a single array -- elements that share
574 | // an index go together.
575 | _.zip = function() {
576 | return _.unzip(arguments);
577 | };
578 |
579 | // Complement of _.zip. Unzip accepts an array of arrays and groups
580 | // each array's elements on shared indices
581 | _.unzip = function(array) {
582 | var length = array && _.max(array, 'length').length || 0;
583 | var result = Array(length);
584 |
585 | for (var index = 0; index < length; index++) {
586 | result[index] = _.pluck(array, index);
587 | }
588 | return result;
589 | };
590 |
591 | // Converts lists into objects. Pass either a single array of `[key, value]`
592 | // pairs, or two parallel arrays of the same length -- one of keys, and one of
593 | // the corresponding values.
594 | _.object = function(list, values) {
595 | var result = {};
596 | for (var i = 0, length = list && list.length; i < length; i++) {
597 | if (values) {
598 | result[list[i]] = values[i];
599 | } else {
600 | result[list[i][0]] = list[i][1];
601 | }
602 | }
603 | return result;
604 | };
605 |
606 | // Return the position of the first occurrence of an item in an array,
607 | // or -1 if the item is not included in the array.
608 | // If the array is large and already in sort order, pass `true`
609 | // for **isSorted** to use binary search.
610 | _.indexOf = function(array, item, isSorted) {
611 | var i = 0, length = array && array.length;
612 | if (typeof isSorted == 'number') {
613 | i = isSorted < 0 ? Math.max(0, length + isSorted) : isSorted;
614 | } else if (isSorted && length) {
615 | i = _.sortedIndex(array, item);
616 | return array[i] === item ? i : -1;
617 | }
618 | if (item !== item) {
619 | return _.findIndex(slice.call(array, i), _.isNaN);
620 | }
621 | for (; i < length; i++) if (array[i] === item) return i;
622 | return -1;
623 | };
624 |
625 | _.lastIndexOf = function(array, item, from) {
626 | var idx = array ? array.length : 0;
627 | if (typeof from == 'number') {
628 | idx = from < 0 ? idx + from + 1 : Math.min(idx, from + 1);
629 | }
630 | if (item !== item) {
631 | return _.findLastIndex(slice.call(array, 0, idx), _.isNaN);
632 | }
633 | while (--idx >= 0) if (array[idx] === item) return idx;
634 | return -1;
635 | };
636 |
637 | // Generator function to create the findIndex and findLastIndex functions
638 | function createIndexFinder(dir) {
639 | return function(array, predicate, context) {
640 | predicate = cb(predicate, context);
641 | var length = array != null && array.length;
642 | var index = dir > 0 ? 0 : length - 1;
643 | for (; index >= 0 && index < length; index += dir) {
644 | if (predicate(array[index], index, array)) return index;
645 | }
646 | return -1;
647 | };
648 | }
649 |
650 | // Returns the first index on an array-like that passes a predicate test
651 | _.findIndex = createIndexFinder(1);
652 |
653 | _.findLastIndex = createIndexFinder(-1);
654 |
655 | // Use a comparator function to figure out the smallest index at which
656 | // an object should be inserted so as to maintain order. Uses binary search.
657 | _.sortedIndex = function(array, obj, iteratee, context) {
658 | iteratee = cb(iteratee, context, 1);
659 | var value = iteratee(obj);
660 | var low = 0, high = array.length;
661 | while (low < high) {
662 | var mid = Math.floor((low + high) / 2);
663 | if (iteratee(array[mid]) < value) low = mid + 1; else high = mid;
664 | }
665 | return low;
666 | };
667 |
668 | // Generate an integer Array containing an arithmetic progression. A port of
669 | // the native Python `range()` function. See
670 | // [the Python documentation](http://docs.python.org/library/functions.html#range).
671 | _.range = function(start, stop, step) {
672 | if (arguments.length <= 1) {
673 | stop = start || 0;
674 | start = 0;
675 | }
676 | step = step || 1;
677 |
678 | var length = Math.max(Math.ceil((stop - start) / step), 0);
679 | var range = Array(length);
680 |
681 | for (var idx = 0; idx < length; idx++, start += step) {
682 | range[idx] = start;
683 | }
684 |
685 | return range;
686 | };
687 |
688 | // Function (ahem) Functions
689 | // ------------------
690 |
691 | // Determines whether to execute a function as a constructor
692 | // or a normal function with the provided arguments
693 | var executeBound = function(sourceFunc, boundFunc, context, callingContext, args) {
694 | if (!(callingContext instanceof boundFunc)) return sourceFunc.apply(context, args);
695 | var self = baseCreate(sourceFunc.prototype);
696 | var result = sourceFunc.apply(self, args);
697 | if (_.isObject(result)) return result;
698 | return self;
699 | };
700 |
701 | // Create a function bound to a given object (assigning `this`, and arguments,
702 | // optionally). Delegates to **ECMAScript 5**'s native `Function.bind` if
703 | // available.
704 | _.bind = function(func, context) {
705 | if (nativeBind && func.bind === nativeBind) return nativeBind.apply(func, slice.call(arguments, 1));
706 | if (!_.isFunction(func)) throw new TypeError('Bind must be called on a function');
707 | var args = slice.call(arguments, 2);
708 | var bound = function() {
709 | return executeBound(func, bound, context, this, args.concat(slice.call(arguments)));
710 | };
711 | return bound;
712 | };
713 |
714 | // Partially apply a function by creating a version that has had some of its
715 | // arguments pre-filled, without changing its dynamic `this` context. _ acts
716 | // as a placeholder, allowing any combination of arguments to be pre-filled.
717 | _.partial = function(func) {
718 | var boundArgs = slice.call(arguments, 1);
719 | var bound = function() {
720 | var position = 0, length = boundArgs.length;
721 | var args = Array(length);
722 | for (var i = 0; i < length; i++) {
723 | args[i] = boundArgs[i] === _ ? arguments[position++] : boundArgs[i];
724 | }
725 | while (position < arguments.length) args.push(arguments[position++]);
726 | return executeBound(func, bound, this, this, args);
727 | };
728 | return bound;
729 | };
730 |
731 | // Bind a number of an object's methods to that object. Remaining arguments
732 | // are the method names to be bound. Useful for ensuring that all callbacks
733 | // defined on an object belong to it.
734 | _.bindAll = function(obj) {
735 | var i, length = arguments.length, key;
736 | if (length <= 1) throw new Error('bindAll must be passed function names');
737 | for (i = 1; i < length; i++) {
738 | key = arguments[i];
739 | obj[key] = _.bind(obj[key], obj);
740 | }
741 | return obj;
742 | };
743 |
744 | // Memoize an expensive function by storing its results.
745 | _.memoize = function(func, hasher) {
746 | var memoize = function(key) {
747 | var cache = memoize.cache;
748 | var address = '' + (hasher ? hasher.apply(this, arguments) : key);
749 | if (!_.has(cache, address)) cache[address] = func.apply(this, arguments);
750 | return cache[address];
751 | };
752 | memoize.cache = {};
753 | return memoize;
754 | };
755 |
756 | // Delays a function for the given number of milliseconds, and then calls
757 | // it with the arguments supplied.
758 | _.delay = function(func, wait) {
759 | var args = slice.call(arguments, 2);
760 | return setTimeout(function(){
761 | return func.apply(null, args);
762 | }, wait);
763 | };
764 |
765 | // Defers a function, scheduling it to run after the current call stack has
766 | // cleared.
767 | _.defer = _.partial(_.delay, _, 1);
768 |
769 | // Returns a function, that, when invoked, will only be triggered at most once
770 | // during a given window of time. Normally, the throttled function will run
771 | // as much as it can, without ever going more than once per `wait` duration;
772 | // but if you'd like to disable the execution on the leading edge, pass
773 | // `{leading: false}`. To disable execution on the trailing edge, ditto.
774 | _.throttle = function(func, wait, options) {
775 | var context, args, result;
776 | var timeout = null;
777 | var previous = 0;
778 | if (!options) options = {};
779 | var later = function() {
780 | previous = options.leading === false ? 0 : _.now();
781 | timeout = null;
782 | result = func.apply(context, args);
783 | if (!timeout) context = args = null;
784 | };
785 | return function() {
786 | var now = _.now();
787 | if (!previous && options.leading === false) previous = now;
788 | var remaining = wait - (now - previous);
789 | context = this;
790 | args = arguments;
791 | if (remaining <= 0 || remaining > wait) {
792 | if (timeout) {
793 | clearTimeout(timeout);
794 | timeout = null;
795 | }
796 | previous = now;
797 | result = func.apply(context, args);
798 | if (!timeout) context = args = null;
799 | } else if (!timeout && options.trailing !== false) {
800 | timeout = setTimeout(later, remaining);
801 | }
802 | return result;
803 | };
804 | };
805 |
806 | // Returns a function, that, as long as it continues to be invoked, will not
807 | // be triggered. The function will be called after it stops being called for
808 | // N milliseconds. If `immediate` is passed, trigger the function on the
809 | // leading edge, instead of the trailing.
810 | _.debounce = function(func, wait, immediate) {
811 | var timeout, args, context, timestamp, result;
812 |
813 | var later = function() {
814 | var last = _.now() - timestamp;
815 |
816 | if (last < wait && last >= 0) {
817 | timeout = setTimeout(later, wait - last);
818 | } else {
819 | timeout = null;
820 | if (!immediate) {
821 | result = func.apply(context, args);
822 | if (!timeout) context = args = null;
823 | }
824 | }
825 | };
826 |
827 | return function() {
828 | context = this;
829 | args = arguments;
830 | timestamp = _.now();
831 | var callNow = immediate && !timeout;
832 | if (!timeout) timeout = setTimeout(later, wait);
833 | if (callNow) {
834 | result = func.apply(context, args);
835 | context = args = null;
836 | }
837 |
838 | return result;
839 | };
840 | };
841 |
842 | // Returns the first function passed as an argument to the second,
843 | // allowing you to adjust arguments, run code before and after, and
844 | // conditionally execute the original function.
845 | _.wrap = function(func, wrapper) {
846 | return _.partial(wrapper, func);
847 | };
848 |
849 | // Returns a negated version of the passed-in predicate.
850 | _.negate = function(predicate) {
851 | return function() {
852 | return !predicate.apply(this, arguments);
853 | };
854 | };
855 |
856 | // Returns a function that is the composition of a list of functions, each
857 | // consuming the return value of the function that follows.
858 | _.compose = function() {
859 | var args = arguments;
860 | var start = args.length - 1;
861 | return function() {
862 | var i = start;
863 | var result = args[start].apply(this, arguments);
864 | while (i--) result = args[i].call(this, result);
865 | return result;
866 | };
867 | };
868 |
869 | // Returns a function that will only be executed on and after the Nth call.
870 | _.after = function(times, func) {
871 | return function() {
872 | if (--times < 1) {
873 | return func.apply(this, arguments);
874 | }
875 | };
876 | };
877 |
878 | // Returns a function that will only be executed up to (but not including) the Nth call.
879 | _.before = function(times, func) {
880 | var memo;
881 | return function() {
882 | if (--times > 0) {
883 | memo = func.apply(this, arguments);
884 | }
885 | if (times <= 1) func = null;
886 | return memo;
887 | };
888 | };
889 |
890 | // Returns a function that will be executed at most one time, no matter how
891 | // often you call it. Useful for lazy initialization.
892 | _.once = _.partial(_.before, 2);
893 |
894 | // Object Functions
895 | // ----------------
896 |
897 | // Keys in IE < 9 that won't be iterated by `for key in ...` and thus missed.
898 | var hasEnumBug = !{toString: null}.propertyIsEnumerable('toString');
899 | var nonEnumerableProps = ['valueOf', 'isPrototypeOf', 'toString',
900 | 'propertyIsEnumerable', 'hasOwnProperty', 'toLocaleString'];
901 |
902 | function collectNonEnumProps(obj, keys) {
903 | var nonEnumIdx = nonEnumerableProps.length;
904 | var constructor = obj.constructor;
905 | var proto = (_.isFunction(constructor) && constructor.prototype) || ObjProto;
906 |
907 | // Constructor is a special case.
908 | var prop = 'constructor';
909 | if (_.has(obj, prop) && !_.contains(keys, prop)) keys.push(prop);
910 |
911 | while (nonEnumIdx--) {
912 | prop = nonEnumerableProps[nonEnumIdx];
913 | if (prop in obj && obj[prop] !== proto[prop] && !_.contains(keys, prop)) {
914 | keys.push(prop);
915 | }
916 | }
917 | }
918 |
919 | // Retrieve the names of an object's own properties.
920 | // Delegates to **ECMAScript 5**'s native `Object.keys`
921 | _.keys = function(obj) {
922 | if (!_.isObject(obj)) return [];
923 | if (nativeKeys) return nativeKeys(obj);
924 | var keys = [];
925 | for (var key in obj) if (_.has(obj, key)) keys.push(key);
926 | // Ahem, IE < 9.
927 | if (hasEnumBug) collectNonEnumProps(obj, keys);
928 | return keys;
929 | };
930 |
931 | // Retrieve all the property names of an object.
932 | _.allKeys = function(obj) {
933 | if (!_.isObject(obj)) return [];
934 | var keys = [];
935 | for (var key in obj) keys.push(key);
936 | // Ahem, IE < 9.
937 | if (hasEnumBug) collectNonEnumProps(obj, keys);
938 | return keys;
939 | };
940 |
941 | // Retrieve the values of an object's properties.
942 | _.values = function(obj) {
943 | var keys = _.keys(obj);
944 | var length = keys.length;
945 | var values = Array(length);
946 | for (var i = 0; i < length; i++) {
947 | values[i] = obj[keys[i]];
948 | }
949 | return values;
950 | };
951 |
952 | // Returns the results of applying the iteratee to each element of the object
953 | // In contrast to _.map it returns an object
954 | _.mapObject = function(obj, iteratee, context) {
955 | iteratee = cb(iteratee, context);
956 | var keys = _.keys(obj),
957 | length = keys.length,
958 | results = {},
959 | currentKey;
960 | for (var index = 0; index < length; index++) {
961 | currentKey = keys[index];
962 | results[currentKey] = iteratee(obj[currentKey], currentKey, obj);
963 | }
964 | return results;
965 | };
966 |
967 | // Convert an object into a list of `[key, value]` pairs.
968 | _.pairs = function(obj) {
969 | var keys = _.keys(obj);
970 | var length = keys.length;
971 | var pairs = Array(length);
972 | for (var i = 0; i < length; i++) {
973 | pairs[i] = [keys[i], obj[keys[i]]];
974 | }
975 | return pairs;
976 | };
977 |
978 | // Invert the keys and values of an object. The values must be serializable.
979 | _.invert = function(obj) {
980 | var result = {};
981 | var keys = _.keys(obj);
982 | for (var i = 0, length = keys.length; i < length; i++) {
983 | result[obj[keys[i]]] = keys[i];
984 | }
985 | return result;
986 | };
987 |
988 | // Return a sorted list of the function names available on the object.
989 | // Aliased as `methods`
990 | _.functions = _.methods = function(obj) {
991 | var names = [];
992 | for (var key in obj) {
993 | if (_.isFunction(obj[key])) names.push(key);
994 | }
995 | return names.sort();
996 | };
997 |
998 | // Extend a given object with all the properties in passed-in object(s).
999 | _.extend = createAssigner(_.allKeys);
1000 |
1001 | // Assigns a given object with all the own properties in the passed-in object(s)
1002 | // (https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Object/assign)
1003 | _.extendOwn = _.assign = createAssigner(_.keys);
1004 |
1005 | // Returns the first key on an object that passes a predicate test
1006 | _.findKey = function(obj, predicate, context) {
1007 | predicate = cb(predicate, context);
1008 | var keys = _.keys(obj), key;
1009 | for (var i = 0, length = keys.length; i < length; i++) {
1010 | key = keys[i];
1011 | if (predicate(obj[key], key, obj)) return key;
1012 | }
1013 | };
1014 |
1015 | // Return a copy of the object only containing the whitelisted properties.
1016 | _.pick = function(object, oiteratee, context) {
1017 | var result = {}, obj = object, iteratee, keys;
1018 | if (obj == null) return result;
1019 | if (_.isFunction(oiteratee)) {
1020 | keys = _.allKeys(obj);
1021 | iteratee = optimizeCb(oiteratee, context);
1022 | } else {
1023 | keys = flatten(arguments, false, false, 1);
1024 | iteratee = function(value, key, obj) { return key in obj; };
1025 | obj = Object(obj);
1026 | }
1027 | for (var i = 0, length = keys.length; i < length; i++) {
1028 | var key = keys[i];
1029 | var value = obj[key];
1030 | if (iteratee(value, key, obj)) result[key] = value;
1031 | }
1032 | return result;
1033 | };
1034 |
1035 | // Return a copy of the object without the blacklisted properties.
1036 | _.omit = function(obj, iteratee, context) {
1037 | if (_.isFunction(iteratee)) {
1038 | iteratee = _.negate(iteratee);
1039 | } else {
1040 | var keys = _.map(flatten(arguments, false, false, 1), String);
1041 | iteratee = function(value, key) {
1042 | return !_.contains(keys, key);
1043 | };
1044 | }
1045 | return _.pick(obj, iteratee, context);
1046 | };
1047 |
1048 | // Fill in a given object with default properties.
1049 | _.defaults = createAssigner(_.allKeys, true);
1050 |
1051 | // Create a (shallow-cloned) duplicate of an object.
1052 | _.clone = function(obj) {
1053 | if (!_.isObject(obj)) return obj;
1054 | return _.isArray(obj) ? obj.slice() : _.extend({}, obj);
1055 | };
1056 |
1057 | // Invokes interceptor with the obj, and then returns obj.
1058 | // The primary purpose of this method is to "tap into" a method chain, in
1059 | // order to perform operations on intermediate results within the chain.
1060 | _.tap = function(obj, interceptor) {
1061 | interceptor(obj);
1062 | return obj;
1063 | };
1064 |
1065 | // Returns whether an object has a given set of `key:value` pairs.
1066 | _.isMatch = function(object, attrs) {
1067 | var keys = _.keys(attrs), length = keys.length;
1068 | if (object == null) return !length;
1069 | var obj = Object(object);
1070 | for (var i = 0; i < length; i++) {
1071 | var key = keys[i];
1072 | if (attrs[key] !== obj[key] || !(key in obj)) return false;
1073 | }
1074 | return true;
1075 | };
1076 |
1077 |
1078 | // Internal recursive comparison function for `isEqual`.
1079 | var eq = function(a, b, aStack, bStack) {
1080 | // Identical objects are equal. `0 === -0`, but they aren't identical.
1081 | // See the [Harmony `egal` proposal](http://wiki.ecmascript.org/doku.php?id=harmony:egal).
1082 | if (a === b) return a !== 0 || 1 / a === 1 / b;
1083 | // A strict comparison is necessary because `null == undefined`.
1084 | if (a == null || b == null) return a === b;
1085 | // Unwrap any wrapped objects.
1086 | if (a instanceof _) a = a._wrapped;
1087 | if (b instanceof _) b = b._wrapped;
1088 | // Compare `[[Class]]` names.
1089 | var className = toString.call(a);
1090 | if (className !== toString.call(b)) return false;
1091 | switch (className) {
1092 | // Strings, numbers, regular expressions, dates, and booleans are compared by value.
1093 | case '[object RegExp]':
1094 | // RegExps are coerced to strings for comparison (Note: '' + /a/i === '/a/i')
1095 | case '[object String]':
1096 | // Primitives and their corresponding object wrappers are equivalent; thus, `"5"` is
1097 | // equivalent to `new String("5")`.
1098 | return '' + a === '' + b;
1099 | case '[object Number]':
1100 | // `NaN`s are equivalent, but non-reflexive.
1101 | // Object(NaN) is equivalent to NaN
1102 | if (+a !== +a) return +b !== +b;
1103 | // An `egal` comparison is performed for other numeric values.
1104 | return +a === 0 ? 1 / +a === 1 / b : +a === +b;
1105 | case '[object Date]':
1106 | case '[object Boolean]':
1107 | // Coerce dates and booleans to numeric primitive values. Dates are compared by their
1108 | // millisecond representations. Note that invalid dates with millisecond representations
1109 | // of `NaN` are not equivalent.
1110 | return +a === +b;
1111 | }
1112 |
1113 | var areArrays = className === '[object Array]';
1114 | if (!areArrays) {
1115 | if (typeof a != 'object' || typeof b != 'object') return false;
1116 |
1117 | // Objects with different constructors are not equivalent, but `Object`s or `Array`s
1118 | // from different frames are.
1119 | var aCtor = a.constructor, bCtor = b.constructor;
1120 | if (aCtor !== bCtor && !(_.isFunction(aCtor) && aCtor instanceof aCtor &&
1121 | _.isFunction(bCtor) && bCtor instanceof bCtor)
1122 | && ('constructor' in a && 'constructor' in b)) {
1123 | return false;
1124 | }
1125 | }
1126 | // Assume equality for cyclic structures. The algorithm for detecting cyclic
1127 | // structures is adapted from ES 5.1 section 15.12.3, abstract operation `JO`.
1128 |
1129 | // Initializing stack of traversed objects.
1130 | // It's done here since we only need them for objects and arrays comparison.
1131 | aStack = aStack || [];
1132 | bStack = bStack || [];
1133 | var length = aStack.length;
1134 | while (length--) {
1135 | // Linear search. Performance is inversely proportional to the number of
1136 | // unique nested structures.
1137 | if (aStack[length] === a) return bStack[length] === b;
1138 | }
1139 |
1140 | // Add the first object to the stack of traversed objects.
1141 | aStack.push(a);
1142 | bStack.push(b);
1143 |
1144 | // Recursively compare objects and arrays.
1145 | if (areArrays) {
1146 | // Compare array lengths to determine if a deep comparison is necessary.
1147 | length = a.length;
1148 | if (length !== b.length) return false;
1149 | // Deep compare the contents, ignoring non-numeric properties.
1150 | while (length--) {
1151 | if (!eq(a[length], b[length], aStack, bStack)) return false;
1152 | }
1153 | } else {
1154 | // Deep compare objects.
1155 | var keys = _.keys(a), key;
1156 | length = keys.length;
1157 | // Ensure that both objects contain the same number of properties before comparing deep equality.
1158 | if (_.keys(b).length !== length) return false;
1159 | while (length--) {
1160 | // Deep compare each member
1161 | key = keys[length];
1162 | if (!(_.has(b, key) && eq(a[key], b[key], aStack, bStack))) return false;
1163 | }
1164 | }
1165 | // Remove the first object from the stack of traversed objects.
1166 | aStack.pop();
1167 | bStack.pop();
1168 | return true;
1169 | };
1170 |
1171 | // Perform a deep comparison to check if two objects are equal.
1172 | _.isEqual = function(a, b) {
1173 | return eq(a, b);
1174 | };
1175 |
1176 | // Is a given array, string, or object empty?
1177 | // An "empty" object has no enumerable own-properties.
1178 | _.isEmpty = function(obj) {
1179 | if (obj == null) return true;
1180 | if (isArrayLike(obj) && (_.isArray(obj) || _.isString(obj) || _.isArguments(obj))) return obj.length === 0;
1181 | return _.keys(obj).length === 0;
1182 | };
1183 |
1184 | // Is a given value a DOM element?
1185 | _.isElement = function(obj) {
1186 | return !!(obj && obj.nodeType === 1);
1187 | };
1188 |
1189 | // Is a given value an array?
1190 | // Delegates to ECMA5's native Array.isArray
1191 | _.isArray = nativeIsArray || function(obj) {
1192 | return toString.call(obj) === '[object Array]';
1193 | };
1194 |
1195 | // Is a given variable an object?
1196 | _.isObject = function(obj) {
1197 | var type = typeof obj;
1198 | return type === 'function' || type === 'object' && !!obj;
1199 | };
1200 |
1201 | // Add some isType methods: isArguments, isFunction, isString, isNumber, isDate, isRegExp, isError.
1202 | _.each(['Arguments', 'Function', 'String', 'Number', 'Date', 'RegExp', 'Error'], function(name) {
1203 | _['is' + name] = function(obj) {
1204 | return toString.call(obj) === '[object ' + name + ']';
1205 | };
1206 | });
1207 |
1208 | // Define a fallback version of the method in browsers (ahem, IE < 9), where
1209 | // there isn't any inspectable "Arguments" type.
1210 | if (!_.isArguments(arguments)) {
1211 | _.isArguments = function(obj) {
1212 | return _.has(obj, 'callee');
1213 | };
1214 | }
1215 |
1216 | // Optimize `isFunction` if appropriate. Work around some typeof bugs in old v8,
1217 | // IE 11 (#1621), and in Safari 8 (#1929).
1218 | if (typeof /./ != 'function' && typeof Int8Array != 'object') {
1219 | _.isFunction = function(obj) {
1220 | return typeof obj == 'function' || false;
1221 | };
1222 | }
1223 |
1224 | // Is a given object a finite number?
1225 | _.isFinite = function(obj) {
1226 | return isFinite(obj) && !isNaN(parseFloat(obj));
1227 | };
1228 |
1229 | // Is the given value `NaN`? (NaN is the only number which does not equal itself).
1230 | _.isNaN = function(obj) {
1231 | return _.isNumber(obj) && obj !== +obj;
1232 | };
1233 |
1234 | // Is a given value a boolean?
1235 | _.isBoolean = function(obj) {
1236 | return obj === true || obj === false || toString.call(obj) === '[object Boolean]';
1237 | };
1238 |
1239 | // Is a given value equal to null?
1240 | _.isNull = function(obj) {
1241 | return obj === null;
1242 | };
1243 |
1244 | // Is a given variable undefined?
1245 | _.isUndefined = function(obj) {
1246 | return obj === void 0;
1247 | };
1248 |
1249 | // Shortcut function for checking if an object has a given property directly
1250 | // on itself (in other words, not on a prototype).
1251 | _.has = function(obj, key) {
1252 | return obj != null && hasOwnProperty.call(obj, key);
1253 | };
1254 |
1255 | // Utility Functions
1256 | // -----------------
1257 |
1258 | // Run Underscore.js in *noConflict* mode, returning the `_` variable to its
1259 | // previous owner. Returns a reference to the Underscore object.
1260 | _.noConflict = function() {
1261 | root._ = previousUnderscore;
1262 | return this;
1263 | };
1264 |
1265 | // Keep the identity function around for default iteratees.
1266 | _.identity = function(value) {
1267 | return value;
1268 | };
1269 |
1270 | // Predicate-generating functions. Often useful outside of Underscore.
1271 | _.constant = function(value) {
1272 | return function() {
1273 | return value;
1274 | };
1275 | };
1276 |
1277 | _.noop = function(){};
1278 |
1279 | _.property = function(key) {
1280 | return function(obj) {
1281 | return obj == null ? void 0 : obj[key];
1282 | };
1283 | };
1284 |
1285 | // Generates a function for a given object that returns a given property.
1286 | _.propertyOf = function(obj) {
1287 | return obj == null ? function(){} : function(key) {
1288 | return obj[key];
1289 | };
1290 | };
1291 |
1292 | // Returns a predicate for checking whether an object has a given set of
1293 | // `key:value` pairs.
1294 | _.matcher = _.matches = function(attrs) {
1295 | attrs = _.extendOwn({}, attrs);
1296 | return function(obj) {
1297 | return _.isMatch(obj, attrs);
1298 | };
1299 | };
1300 |
1301 | // Run a function **n** times.
1302 | _.times = function(n, iteratee, context) {
1303 | var accum = Array(Math.max(0, n));
1304 | iteratee = optimizeCb(iteratee, context, 1);
1305 | for (var i = 0; i < n; i++) accum[i] = iteratee(i);
1306 | return accum;
1307 | };
1308 |
1309 | // Return a random integer between min and max (inclusive).
1310 | _.random = function(min, max) {
1311 | if (max == null) {
1312 | max = min;
1313 | min = 0;
1314 | }
1315 | return min + Math.floor(Math.random() * (max - min + 1));
1316 | };
1317 |
1318 | // A (possibly faster) way to get the current timestamp as an integer.
1319 | _.now = Date.now || function() {
1320 | return new Date().getTime();
1321 | };
1322 |
1323 | // List of HTML entities for escaping.
1324 | var escapeMap = {
1325 | '&': '&',
1326 | '<': '<',
1327 | '>': '>',
1328 | '"': '"',
1329 | "'": ''',
1330 | '`': '`'
1331 | };
1332 | var unescapeMap = _.invert(escapeMap);
1333 |
1334 | // Functions for escaping and unescaping strings to/from HTML interpolation.
1335 | var createEscaper = function(map) {
1336 | var escaper = function(match) {
1337 | return map[match];
1338 | };
1339 | // Regexes for identifying a key that needs to be escaped
1340 | var source = '(?:' + _.keys(map).join('|') + ')';
1341 | var testRegexp = RegExp(source);
1342 | var replaceRegexp = RegExp(source, 'g');
1343 | return function(string) {
1344 | string = string == null ? '' : '' + string;
1345 | return testRegexp.test(string) ? string.replace(replaceRegexp, escaper) : string;
1346 | };
1347 | };
1348 | _.escape = createEscaper(escapeMap);
1349 | _.unescape = createEscaper(unescapeMap);
1350 |
1351 | // If the value of the named `property` is a function then invoke it with the
1352 | // `object` as context; otherwise, return it.
1353 | _.result = function(object, property, fallback) {
1354 | var value = object == null ? void 0 : object[property];
1355 | if (value === void 0) {
1356 | value = fallback;
1357 | }
1358 | return _.isFunction(value) ? value.call(object) : value;
1359 | };
1360 |
1361 | // Generate a unique integer id (unique within the entire client session).
1362 | // Useful for temporary DOM ids.
1363 | var idCounter = 0;
1364 | _.uniqueId = function(prefix) {
1365 | var id = ++idCounter + '';
1366 | return prefix ? prefix + id : id;
1367 | };
1368 |
1369 | // By default, Underscore uses ERB-style template delimiters, change the
1370 | // following template settings to use alternative delimiters.
1371 | _.templateSettings = {
1372 | evaluate : /<%([\s\S]+?)%>/g,
1373 | interpolate : /<%=([\s\S]+?)%>/g,
1374 | escape : /<%-([\s\S]+?)%>/g
1375 | };
1376 |
1377 | // When customizing `templateSettings`, if you don't want to define an
1378 | // interpolation, evaluation or escaping regex, we need one that is
1379 | // guaranteed not to match.
1380 | var noMatch = /(.)^/;
1381 |
1382 | // Certain characters need to be escaped so that they can be put into a
1383 | // string literal.
1384 | var escapes = {
1385 | "'": "'",
1386 | '\\': '\\',
1387 | '\r': 'r',
1388 | '\n': 'n',
1389 | '\u2028': 'u2028',
1390 | '\u2029': 'u2029'
1391 | };
1392 |
1393 | var escaper = /\\|'|\r|\n|\u2028|\u2029/g;
1394 |
1395 | var escapeChar = function(match) {
1396 | return '\\' + escapes[match];
1397 | };
1398 |
1399 | // JavaScript micro-templating, similar to John Resig's implementation.
1400 | // Underscore templating handles arbitrary delimiters, preserves whitespace,
1401 | // and correctly escapes quotes within interpolated code.
1402 | // NB: `oldSettings` only exists for backwards compatibility.
1403 | _.template = function(text, settings, oldSettings) {
1404 | if (!settings && oldSettings) settings = oldSettings;
1405 | settings = _.defaults({}, settings, _.templateSettings);
1406 |
1407 | // Combine delimiters into one regular expression via alternation.
1408 | var matcher = RegExp([
1409 | (settings.escape || noMatch).source,
1410 | (settings.interpolate || noMatch).source,
1411 | (settings.evaluate || noMatch).source
1412 | ].join('|') + '|$', 'g');
1413 |
1414 | // Compile the template source, escaping string literals appropriately.
1415 | var index = 0;
1416 | var source = "__p+='";
1417 | text.replace(matcher, function(match, escape, interpolate, evaluate, offset) {
1418 | source += text.slice(index, offset).replace(escaper, escapeChar);
1419 | index = offset + match.length;
1420 |
1421 | if (escape) {
1422 | source += "'+\n((__t=(" + escape + "))==null?'':_.escape(__t))+\n'";
1423 | } else if (interpolate) {
1424 | source += "'+\n((__t=(" + interpolate + "))==null?'':__t)+\n'";
1425 | } else if (evaluate) {
1426 | source += "';\n" + evaluate + "\n__p+='";
1427 | }
1428 |
1429 | // Adobe VMs need the match returned to produce the correct offest.
1430 | return match;
1431 | });
1432 | source += "';\n";
1433 |
1434 | // If a variable is not specified, place data values in local scope.
1435 | if (!settings.variable) source = 'with(obj||{}){\n' + source + '}\n';
1436 |
1437 | source = "var __t,__p='',__j=Array.prototype.join," +
1438 | "print=function(){__p+=__j.call(arguments,'');};\n" +
1439 | source + 'return __p;\n';
1440 |
1441 | try {
1442 | var render = new Function(settings.variable || 'obj', '_', source);
1443 | } catch (e) {
1444 | e.source = source;
1445 | throw e;
1446 | }
1447 |
1448 | var template = function(data) {
1449 | return render.call(this, data, _);
1450 | };
1451 |
1452 | // Provide the compiled source as a convenience for precompilation.
1453 | var argument = settings.variable || 'obj';
1454 | template.source = 'function(' + argument + '){\n' + source + '}';
1455 |
1456 | return template;
1457 | };
1458 |
1459 | // Add a "chain" function. Start chaining a wrapped Underscore object.
1460 | _.chain = function(obj) {
1461 | var instance = _(obj);
1462 | instance._chain = true;
1463 | return instance;
1464 | };
1465 |
1466 | // OOP
1467 | // ---------------
1468 | // If Underscore is called as a function, it returns a wrapped object that
1469 | // can be used OO-style. This wrapper holds altered versions of all the
1470 | // underscore functions. Wrapped objects may be chained.
1471 |
1472 | // Helper function to continue chaining intermediate results.
1473 | var result = function(instance, obj) {
1474 | return instance._chain ? _(obj).chain() : obj;
1475 | };
1476 |
1477 | // Add your own custom functions to the Underscore object.
1478 | _.mixin = function(obj) {
1479 | _.each(_.functions(obj), function(name) {
1480 | var func = _[name] = obj[name];
1481 | _.prototype[name] = function() {
1482 | var args = [this._wrapped];
1483 | push.apply(args, arguments);
1484 | return result(this, func.apply(_, args));
1485 | };
1486 | });
1487 | };
1488 |
1489 | // Add all of the Underscore functions to the wrapper object.
1490 | _.mixin(_);
1491 |
1492 | // Add all mutator Array functions to the wrapper.
1493 | _.each(['pop', 'push', 'reverse', 'shift', 'sort', 'splice', 'unshift'], function(name) {
1494 | var method = ArrayProto[name];
1495 | _.prototype[name] = function() {
1496 | var obj = this._wrapped;
1497 | method.apply(obj, arguments);
1498 | if ((name === 'shift' || name === 'splice') && obj.length === 0) delete obj[0];
1499 | return result(this, obj);
1500 | };
1501 | });
1502 |
1503 | // Add all accessor Array functions to the wrapper.
1504 | _.each(['concat', 'join', 'slice'], function(name) {
1505 | var method = ArrayProto[name];
1506 | _.prototype[name] = function() {
1507 | return result(this, method.apply(this._wrapped, arguments));
1508 | };
1509 | });
1510 |
1511 | // Extracts the result from a wrapped and chained object.
1512 | _.prototype.value = function() {
1513 | return this._wrapped;
1514 | };
1515 |
1516 | // Provide unwrapping proxy for some methods used in engine operations
1517 | // such as arithmetic and JSON stringification.
1518 | _.prototype.valueOf = _.prototype.toJSON = _.prototype.value;
1519 |
1520 | _.prototype.toString = function() {
1521 | return '' + this._wrapped;
1522 | };
1523 |
1524 | // AMD registration happens at the end for compatibility with AMD loaders
1525 | // that may not enforce next-turn semantics on modules. Even though general
1526 | // practice for AMD registration is to be anonymous, underscore registers
1527 | // as a named module because, like jQuery, it is a base library that is
1528 | // popular enough to be bundled in a third party lib, but not be part of
1529 | // an AMD load request. Those cases could generate an error when an
1530 | // anonymous define() is called outside of a loader request.
1531 | if (typeof define === 'function' && define.amd) {
1532 | define('underscore', [], function() {
1533 | return _;
1534 | });
1535 | }
1536 | }.call(this));
1537 |
--------------------------------------------------------------------------------