houdini 0.17.4 → 0.17.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.turbo/turbo-compile.log +2 -2
- package/.turbo/turbo-typedefs.log +2 -2
- package/CHANGELOG.md +18 -0
- package/build/cmd/init.d.ts +2 -2
- package/build/cmd-cjs/index.js +9344 -10447
- package/build/cmd-esm/index.js +9344 -10447
- package/build/codegen/transforms/paginate.d.ts +10 -11
- package/build/codegen-cjs/index.js +9143 -10227
- package/build/codegen-esm/index.js +9143 -10227
- package/build/lib/constants.d.ts +7 -0
- package/build/lib/fs.d.ts +2 -0
- package/build/lib-cjs/index.js +9265 -10347
- package/build/lib-esm/index.js +9264 -10347
- package/build/runtime/cache/subscription.d.ts +2 -1
- package/build/runtime/lib/network.d.ts +5 -2
- package/build/runtime/lib/networkUtils.d.ts +8 -0
- package/build/runtime-cjs/cache/cache.js +5 -3
- package/build/runtime-cjs/cache/subscription.d.ts +2 -1
- package/build/runtime-cjs/cache/subscription.js +6 -4
- package/build/runtime-cjs/cache/tests/subscriptions.test.js +101 -0
- package/build/runtime-cjs/lib/network.d.ts +5 -2
- package/build/runtime-cjs/lib/network.js +39 -3
- package/build/runtime-cjs/lib/networkUtils.d.ts +8 -0
- package/build/runtime-cjs/lib/networkUtils.js +85 -0
- package/build/runtime-esm/cache/cache.js +5 -3
- package/build/runtime-esm/cache/subscription.d.ts +2 -1
- package/build/runtime-esm/cache/subscription.js +6 -4
- package/build/runtime-esm/cache/tests/subscriptions.test.js +101 -0
- package/build/runtime-esm/lib/network.d.ts +5 -2
- package/build/runtime-esm/lib/network.js +39 -3
- package/build/runtime-esm/lib/networkUtils.d.ts +8 -0
- package/build/runtime-esm/lib/networkUtils.js +60 -0
- package/build/test-cjs/index.js +9144 -10228
- package/build/test-esm/index.js +9144 -10228
- package/build/vite-cjs/index.js +9283 -10367
- package/build/vite-esm/index.js +9283 -10367
- package/package.json +2 -2
- package/src/cmd/init.ts +169 -187
- package/src/codegen/generators/artifacts/artifacts.test.ts +99 -66
- package/src/codegen/generators/artifacts/pagination.test.ts +12 -8
- package/src/codegen/generators/artifacts/policy.test.ts +12 -8
- package/src/codegen/generators/definitions/schema.test.ts +12 -36
- package/src/codegen/generators/persistedQueries/persistedQuery.test.ts +2 -2
- package/src/codegen/generators/runtime/index.ts +2 -2
- package/src/codegen/transforms/fragmentVariables.test.ts +24 -16
- package/src/codegen/transforms/paginate.test.ts +9 -6
- package/src/codegen/transforms/paginate.ts +2 -2
- package/src/lib/config.ts +3 -2
- package/src/lib/constants.ts +10 -0
- package/src/lib/fs.ts +59 -12
- package/src/runtime/cache/cache.ts +3 -1
- package/src/runtime/cache/subscription.ts +6 -4
- package/src/runtime/cache/tests/subscriptions.test.ts +115 -0
- package/src/runtime/lib/network.ts +65 -2
- package/src/runtime/lib/networkUtils.ts +151 -0
|
@@ -156,16 +156,17 @@ export class InMemorySubscriptions {
|
|
|
156
156
|
selection,
|
|
157
157
|
variables,
|
|
158
158
|
subscribers,
|
|
159
|
+
parentType,
|
|
159
160
|
}: {
|
|
160
161
|
parent: string
|
|
161
162
|
selection: SubscriptionSelection
|
|
162
163
|
variables: {}
|
|
163
164
|
subscribers: SubscriptionSpec[]
|
|
165
|
+
parentType: string
|
|
164
166
|
}) {
|
|
165
167
|
// look at every field in the selection and add the subscribers
|
|
166
168
|
for (const fieldSelection of Object.values(selection)) {
|
|
167
|
-
const { keyRaw, fields } = fieldSelection
|
|
168
|
-
|
|
169
|
+
const { type: linkedType, keyRaw, fields } = fieldSelection
|
|
169
170
|
const key = evaluateKey(keyRaw, variables)
|
|
170
171
|
|
|
171
172
|
// add the subscriber to the
|
|
@@ -175,7 +176,7 @@ export class InMemorySubscriptions {
|
|
|
175
176
|
key,
|
|
176
177
|
selection: fieldSelection,
|
|
177
178
|
spec,
|
|
178
|
-
parentType
|
|
179
|
+
parentType,
|
|
179
180
|
variables,
|
|
180
181
|
})
|
|
181
182
|
}
|
|
@@ -183,6 +184,7 @@ export class InMemorySubscriptions {
|
|
|
183
184
|
// if there are fields under this
|
|
184
185
|
if (fields) {
|
|
185
186
|
const { value: link } = this.cache._internal_unstable.storage.get(parent, key)
|
|
187
|
+
|
|
186
188
|
// figure out who else needs subscribers
|
|
187
189
|
const children = !Array.isArray(link)
|
|
188
190
|
? ([link] as string[])
|
|
@@ -192,13 +194,13 @@ export class InMemorySubscriptions {
|
|
|
192
194
|
if (!linkedRecord) {
|
|
193
195
|
continue
|
|
194
196
|
}
|
|
195
|
-
|
|
196
197
|
// insert the subscriber
|
|
197
198
|
this.addMany({
|
|
198
199
|
parent: linkedRecord,
|
|
199
200
|
selection: fields,
|
|
200
201
|
variables,
|
|
201
202
|
subscribers,
|
|
203
|
+
parentType: linkedType,
|
|
202
204
|
})
|
|
203
205
|
}
|
|
204
206
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { test, expect, vi } from 'vitest'
|
|
2
2
|
|
|
3
3
|
import { testConfigFile } from '../../../test'
|
|
4
|
+
import { RefetchUpdateMode, SubscriptionSelection } from '../../lib'
|
|
4
5
|
import { Cache } from '../cache'
|
|
5
6
|
|
|
6
7
|
const config = testConfigFile()
|
|
@@ -1637,6 +1638,120 @@ test('clearing a display layer updates subscribers', function () {
|
|
|
1637
1638
|
})
|
|
1638
1639
|
})
|
|
1639
1640
|
|
|
1641
|
+
test('ensure parent type is properly passed for nested lists', function () {
|
|
1642
|
+
// instantiate a cache
|
|
1643
|
+
const cache = new Cache(config)
|
|
1644
|
+
|
|
1645
|
+
const selection: SubscriptionSelection = {
|
|
1646
|
+
cities: {
|
|
1647
|
+
type: 'City',
|
|
1648
|
+
keyRaw: 'cities',
|
|
1649
|
+
list: {
|
|
1650
|
+
name: 'City_List',
|
|
1651
|
+
connection: false,
|
|
1652
|
+
type: 'City',
|
|
1653
|
+
},
|
|
1654
|
+
update: RefetchUpdateMode.append,
|
|
1655
|
+
fields: {
|
|
1656
|
+
id: {
|
|
1657
|
+
type: 'ID',
|
|
1658
|
+
keyRaw: 'id',
|
|
1659
|
+
},
|
|
1660
|
+
name: {
|
|
1661
|
+
type: 'String',
|
|
1662
|
+
keyRaw: 'name',
|
|
1663
|
+
},
|
|
1664
|
+
libraries: {
|
|
1665
|
+
type: 'Library',
|
|
1666
|
+
keyRaw: 'libraries',
|
|
1667
|
+
update: RefetchUpdateMode.append,
|
|
1668
|
+
list: {
|
|
1669
|
+
name: 'Library_List',
|
|
1670
|
+
connection: false,
|
|
1671
|
+
type: 'Library',
|
|
1672
|
+
},
|
|
1673
|
+
fields: {
|
|
1674
|
+
id: {
|
|
1675
|
+
type: 'ID',
|
|
1676
|
+
keyRaw: 'id',
|
|
1677
|
+
},
|
|
1678
|
+
name: {
|
|
1679
|
+
type: 'String',
|
|
1680
|
+
keyRaw: 'name',
|
|
1681
|
+
},
|
|
1682
|
+
books: {
|
|
1683
|
+
type: 'Book',
|
|
1684
|
+
keyRaw: 'books',
|
|
1685
|
+
list: {
|
|
1686
|
+
name: 'Book_List',
|
|
1687
|
+
connection: false,
|
|
1688
|
+
type: 'Book',
|
|
1689
|
+
},
|
|
1690
|
+
fields: {
|
|
1691
|
+
id: {
|
|
1692
|
+
type: 'ID',
|
|
1693
|
+
keyRaw: 'id',
|
|
1694
|
+
},
|
|
1695
|
+
title: {
|
|
1696
|
+
type: 'String',
|
|
1697
|
+
keyRaw: 'title',
|
|
1698
|
+
},
|
|
1699
|
+
},
|
|
1700
|
+
},
|
|
1701
|
+
},
|
|
1702
|
+
},
|
|
1703
|
+
},
|
|
1704
|
+
},
|
|
1705
|
+
}
|
|
1706
|
+
|
|
1707
|
+
// a function to spy on that will play the role of set
|
|
1708
|
+
const set = vi.fn()
|
|
1709
|
+
|
|
1710
|
+
// subscribe to the fields
|
|
1711
|
+
cache.subscribe({
|
|
1712
|
+
rootType: 'Query',
|
|
1713
|
+
selection,
|
|
1714
|
+
set,
|
|
1715
|
+
})
|
|
1716
|
+
|
|
1717
|
+
// add a city to the list by hand since using the list util adds type information
|
|
1718
|
+
|
|
1719
|
+
cache.write({
|
|
1720
|
+
selection,
|
|
1721
|
+
data: {
|
|
1722
|
+
cities: [
|
|
1723
|
+
{
|
|
1724
|
+
id: '1',
|
|
1725
|
+
name: 'Alexandria',
|
|
1726
|
+
libraries: [
|
|
1727
|
+
{
|
|
1728
|
+
id: '1',
|
|
1729
|
+
name: 'The Library of Alexandria',
|
|
1730
|
+
books: [],
|
|
1731
|
+
},
|
|
1732
|
+
{
|
|
1733
|
+
id: '2',
|
|
1734
|
+
name: 'Bibliotheca Alexandrina',
|
|
1735
|
+
books: [],
|
|
1736
|
+
},
|
|
1737
|
+
],
|
|
1738
|
+
},
|
|
1739
|
+
{
|
|
1740
|
+
id: '2',
|
|
1741
|
+
name: 'Aalborg',
|
|
1742
|
+
libraries: [],
|
|
1743
|
+
},
|
|
1744
|
+
],
|
|
1745
|
+
},
|
|
1746
|
+
})
|
|
1747
|
+
|
|
1748
|
+
// since there are multiple lists inside of City_List, we need to
|
|
1749
|
+
// specify the parentID of the city in order to add a library to City:3
|
|
1750
|
+
expect(() => cache.list('Library_List', '2')).not.toThrow()
|
|
1751
|
+
// same with Books_List for Library:2
|
|
1752
|
+
expect(() => cache.list('Book_List', '2')).not.toThrow()
|
|
1753
|
+
})
|
|
1754
|
+
|
|
1640
1755
|
test.todo('can write to and resolve layers')
|
|
1641
1756
|
|
|
1642
1757
|
test.todo("resolving a layer with the same value as the most recent doesn't notify subscribers")
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import cache from '../cache'
|
|
3
3
|
import type { ConfigFile } from './config'
|
|
4
4
|
import * as log from './log'
|
|
5
|
+
import { extractFiles } from './networkUtils'
|
|
5
6
|
import {
|
|
6
7
|
CachePolicy,
|
|
7
8
|
DataSource,
|
|
@@ -22,6 +23,56 @@ export class HoudiniClient {
|
|
|
22
23
|
this.socket = subscriptionHandler
|
|
23
24
|
}
|
|
24
25
|
|
|
26
|
+
handleMultipart(
|
|
27
|
+
params: FetchParams,
|
|
28
|
+
args: Parameters<FetchContext['fetch']>
|
|
29
|
+
): Parameters<FetchContext['fetch']> | undefined {
|
|
30
|
+
// process any files that could be included
|
|
31
|
+
const { clone, files } = extractFiles({
|
|
32
|
+
query: params.text,
|
|
33
|
+
variables: params.variables,
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
// if there are files in the request
|
|
37
|
+
if (files.size) {
|
|
38
|
+
const [url, req] = args
|
|
39
|
+
let headers: Record<string, string> = {}
|
|
40
|
+
|
|
41
|
+
// filters `content-type: application/json` if received by client.ts
|
|
42
|
+
if (req?.headers) {
|
|
43
|
+
const filtered = Object.entries(req?.headers).filter(([key, value]) => {
|
|
44
|
+
return !(
|
|
45
|
+
key.toLowerCase() == 'content-type' &&
|
|
46
|
+
value.toLowerCase() == 'application/json'
|
|
47
|
+
)
|
|
48
|
+
})
|
|
49
|
+
headers = Object.fromEntries(filtered)
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// See the GraphQL multipart request spec:
|
|
53
|
+
// https://github.com/jaydenseric/graphql-multipart-request-spec
|
|
54
|
+
const form = new FormData()
|
|
55
|
+
const operationJSON = JSON.stringify(clone)
|
|
56
|
+
|
|
57
|
+
form.set('operations', operationJSON)
|
|
58
|
+
|
|
59
|
+
const map: Record<string, Array<string>> = {}
|
|
60
|
+
|
|
61
|
+
let i = 0
|
|
62
|
+
files.forEach((paths) => {
|
|
63
|
+
map[++i] = paths
|
|
64
|
+
})
|
|
65
|
+
form.set('map', JSON.stringify(map))
|
|
66
|
+
|
|
67
|
+
i = 0
|
|
68
|
+
files.forEach((paths, file) => {
|
|
69
|
+
form.set(`${++i}`, file as Blob, (file as File).name)
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
return [url, { ...req, headers, body: form as any }]
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
25
76
|
async sendRequest<_Data>(
|
|
26
77
|
ctx: FetchContext,
|
|
27
78
|
params: FetchParams
|
|
@@ -33,7 +84,11 @@ export class HoudiniClient {
|
|
|
33
84
|
// wrap the user's fetch function so we can identify SSR by checking
|
|
34
85
|
// the response.url
|
|
35
86
|
fetch: async (...args: Parameters<FetchContext['fetch']>) => {
|
|
36
|
-
|
|
87
|
+
// figure out if we need to do something special for multipart uploads
|
|
88
|
+
const newArgs = this.handleMultipart(params, args)
|
|
89
|
+
|
|
90
|
+
// use the new args if they exist, otherwise the old ones are good
|
|
91
|
+
const response = await ctx.fetch(...(newArgs || args))
|
|
37
92
|
if (response.url) {
|
|
38
93
|
url = response.url
|
|
39
94
|
}
|
|
@@ -118,6 +173,7 @@ export async function executeQuery<_Data extends GraphQLObject, _Input extends {
|
|
|
118
173
|
artifact,
|
|
119
174
|
variables,
|
|
120
175
|
session,
|
|
176
|
+
setFetching,
|
|
121
177
|
cached,
|
|
122
178
|
fetch,
|
|
123
179
|
metadata,
|
|
@@ -126,6 +182,7 @@ export async function executeQuery<_Data extends GraphQLObject, _Input extends {
|
|
|
126
182
|
artifact: QueryArtifact | MutationArtifact
|
|
127
183
|
variables: _Input
|
|
128
184
|
session: any
|
|
185
|
+
setFetching: (fetching: boolean) => void
|
|
129
186
|
cached: boolean
|
|
130
187
|
config: ConfigFile
|
|
131
188
|
fetch?: typeof globalThis.fetch
|
|
@@ -139,6 +196,7 @@ export async function executeQuery<_Data extends GraphQLObject, _Input extends {
|
|
|
139
196
|
session,
|
|
140
197
|
},
|
|
141
198
|
artifact,
|
|
199
|
+
setFetching,
|
|
142
200
|
variables,
|
|
143
201
|
cached,
|
|
144
202
|
})
|
|
@@ -156,16 +214,18 @@ export async function executeQuery<_Data extends GraphQLObject, _Input extends {
|
|
|
156
214
|
|
|
157
215
|
export async function fetchQuery<_Data extends GraphQLObject, _Input extends {}>({
|
|
158
216
|
client,
|
|
217
|
+
context,
|
|
159
218
|
artifact,
|
|
160
219
|
variables,
|
|
220
|
+
setFetching,
|
|
161
221
|
cached = true,
|
|
162
222
|
policy,
|
|
163
|
-
context,
|
|
164
223
|
}: {
|
|
165
224
|
client: HoudiniClient
|
|
166
225
|
context: FetchContext
|
|
167
226
|
artifact: QueryArtifact | MutationArtifact
|
|
168
227
|
variables: _Input
|
|
228
|
+
setFetching: (fetching: boolean) => void
|
|
169
229
|
cached?: boolean
|
|
170
230
|
policy?: CachePolicy
|
|
171
231
|
}): Promise<FetchQueryResult<_Data>> {
|
|
@@ -225,6 +285,9 @@ export async function fetchQuery<_Data extends GraphQLObject, _Input extends {}>
|
|
|
225
285
|
cache._internal_unstable.collectGarbage()
|
|
226
286
|
}, 0)
|
|
227
287
|
|
|
288
|
+
// tell everyone that we are fetching if the function is defined
|
|
289
|
+
setFetching(true)
|
|
290
|
+
|
|
228
291
|
// the request must be resolved against the network
|
|
229
292
|
const result = await client.sendRequest<_Data>(context, {
|
|
230
293
|
text: artifact.raw,
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
/// This file contains a modified version, made by AlecAivazis, of the functions found here: https://github.com/jaydenseric/extract-files/blob/master/extractFiles.mjs
|
|
2
|
+
/// The associated license is at the end of the file (per the project's license agreement)
|
|
3
|
+
|
|
4
|
+
export function isExtractableFile(value: any): value is ExtractableFile {
|
|
5
|
+
return (
|
|
6
|
+
(typeof File !== 'undefined' && value instanceof File) ||
|
|
7
|
+
(typeof Blob !== 'undefined' && value instanceof Blob)
|
|
8
|
+
)
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
type ExtractableFile = File | Blob
|
|
12
|
+
|
|
13
|
+
/** @typedef {import("./isExtractableFile.mjs").default} isExtractableFile */
|
|
14
|
+
|
|
15
|
+
export function extractFiles(value: any) {
|
|
16
|
+
if (!arguments.length) throw new TypeError('Argument 1 `value` is required.')
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Deeply clonable value.
|
|
20
|
+
* @typedef {Array<unknown> | FileList | Record<PropertyKey, unknown>} Cloneable
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Clone of a {@link Cloneable deeply cloneable value}.
|
|
25
|
+
* @typedef {Exclude<Cloneable, FileList>} Clone
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Map of values recursed within the input value and their clones, for reusing
|
|
30
|
+
* clones of values that are referenced multiple times within the input value.
|
|
31
|
+
* @type {Map<Cloneable, Clone>}
|
|
32
|
+
*/
|
|
33
|
+
const clones = new Map()
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Extracted files and their object paths within the input value.
|
|
37
|
+
* @type {Extraction<Extractable>["files"]}
|
|
38
|
+
*/
|
|
39
|
+
const files = new Map()
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Recursively clones the value, extracting files.
|
|
43
|
+
*/
|
|
44
|
+
function recurse(value: any, path: string | string[], recursed: Set<any>) {
|
|
45
|
+
if (isExtractableFile(value)) {
|
|
46
|
+
const filePaths = files.get(value)
|
|
47
|
+
|
|
48
|
+
filePaths ? filePaths.push(path) : files.set(value, [path])
|
|
49
|
+
|
|
50
|
+
return null
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const valueIsList =
|
|
54
|
+
Array.isArray(value) || (typeof FileList !== 'undefined' && value instanceof FileList)
|
|
55
|
+
const valueIsPlainObject = isPlainObject(value)
|
|
56
|
+
|
|
57
|
+
if (valueIsList || valueIsPlainObject) {
|
|
58
|
+
let clone = clones.get(value)
|
|
59
|
+
|
|
60
|
+
const uncloned = !clone
|
|
61
|
+
|
|
62
|
+
if (uncloned) {
|
|
63
|
+
clone = valueIsList
|
|
64
|
+
? []
|
|
65
|
+
: // Replicate if the plain object is an `Object` instance.
|
|
66
|
+
value instanceof /** @type {any} */ Object
|
|
67
|
+
? {}
|
|
68
|
+
: Object.create(null)
|
|
69
|
+
|
|
70
|
+
clones.set(value, /** @type {Clone} */ clone)
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (!recursed.has(value)) {
|
|
74
|
+
const pathPrefix = path ? `${path}.` : ''
|
|
75
|
+
const recursedDeeper = new Set(recursed).add(value)
|
|
76
|
+
|
|
77
|
+
if (valueIsList) {
|
|
78
|
+
let index = 0
|
|
79
|
+
|
|
80
|
+
// @ts-ignore
|
|
81
|
+
for (const item of value) {
|
|
82
|
+
const itemClone = recurse(item, pathPrefix + index++, recursedDeeper)
|
|
83
|
+
|
|
84
|
+
if (uncloned) /** @type {Array<unknown>} */ clone.push(itemClone)
|
|
85
|
+
}
|
|
86
|
+
} else
|
|
87
|
+
for (const key in value) {
|
|
88
|
+
const propertyClone = recurse(value[key], pathPrefix + key, recursedDeeper)
|
|
89
|
+
|
|
90
|
+
if (uncloned)
|
|
91
|
+
/** @type {Record<PropertyKey, unknown>} */ clone[key] = propertyClone
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return clone
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
return value
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return {
|
|
102
|
+
clone: recurse(value, '', new Set()),
|
|
103
|
+
files,
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* An extraction result.
|
|
109
|
+
* @template [Extractable=unknown] Extractable file type.
|
|
110
|
+
* @typedef {object} Extraction
|
|
111
|
+
* @prop {unknown} clone Clone of the original value with extracted files
|
|
112
|
+
* recursively replaced with `null`.
|
|
113
|
+
* @prop {Map<Extractable, Array<ObjectPath>>} files Extracted files and their
|
|
114
|
+
* object paths within the original value.
|
|
115
|
+
*/
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* String notation for the path to a node in an object tree.
|
|
119
|
+
* @typedef {string} ObjectPath
|
|
120
|
+
* @see [`object-path` on npm](https://npm.im/object-path).
|
|
121
|
+
* @example
|
|
122
|
+
* An object path for object property `a`, array index `0`, object property `b`:
|
|
123
|
+
*
|
|
124
|
+
* ```
|
|
125
|
+
* a.0.b
|
|
126
|
+
* ```
|
|
127
|
+
*/
|
|
128
|
+
|
|
129
|
+
function isPlainObject(value: any) {
|
|
130
|
+
if (typeof value !== 'object' || value === null) {
|
|
131
|
+
return false
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const prototype = Object.getPrototypeOf(value)
|
|
135
|
+
return (
|
|
136
|
+
(prototype === null ||
|
|
137
|
+
prototype === Object.prototype ||
|
|
138
|
+
Object.getPrototypeOf(prototype) === null) &&
|
|
139
|
+
!(Symbol.toStringTag in value) &&
|
|
140
|
+
!(Symbol.iterator in value)
|
|
141
|
+
)
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// MIT License
|
|
145
|
+
// Copyright Jayden Seric
|
|
146
|
+
|
|
147
|
+
// Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
|
148
|
+
|
|
149
|
+
// The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
|
150
|
+
|
|
151
|
+
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|