@data-eden/athena
Advanced tools
Comparing version 0.13.2 to 0.14.0
@@ -1,7 +0,44 @@ | ||
import { describe, test, expect, beforeEach } from 'vitest'; | ||
import { AthenaClient } from '../src/client.js'; | ||
import { | ||
describe, | ||
test, | ||
expect, | ||
beforeEach, | ||
beforeAll, | ||
afterAll, | ||
afterEach, | ||
} from 'vitest'; | ||
import http from 'http'; | ||
import { readFileSync } from 'fs'; | ||
import { resolve } from 'path'; | ||
import { createServer } from '@data-eden/shared-test-utilities'; | ||
import { createSignal } from '@signalis/core'; | ||
import { gql } from '@data-eden/codegen/gql'; | ||
import { Mocker } from '@data-eden/mocker'; | ||
import { type PeopleQuery } from './__generated/client.test.graphql'; | ||
import { AthenaClient } from '../src/client.js'; | ||
import type { ReactiveSignal } from '../src/types.js'; | ||
const schema = readFileSync( | ||
resolve( | ||
__dirname, | ||
'..', | ||
'..', | ||
'..', | ||
'internal-packages/react-graphql-test-app/src/graphql/schema.graphql' | ||
), | ||
'utf8' | ||
); | ||
const peopleQuery = gql<PeopleQuery>` | ||
query people { | ||
people { | ||
id | ||
name | ||
} | ||
} | ||
`; | ||
function adapter<T>(v: T): ReactiveSignal<T> { | ||
@@ -16,30 +53,22 @@ return createSignal(v, false); | ||
function hashCode(str: string) { | ||
let hash = 0; | ||
for (let i = 0, len = str.length; i < len; i++) { | ||
let chr = str.charCodeAt(i); | ||
// eslint-disable-next-line no-bitwise | ||
hash = (hash << 5) - hash + chr; | ||
// eslint-disable-next-line no-bitwise | ||
hash |= 0; // Convert to 32bit integer | ||
} | ||
return hash.toString(); | ||
} | ||
const mocker = new Mocker({ | ||
schema, | ||
}); | ||
describe('client', () => { | ||
let client: AthenaClient; | ||
describe('processEntities', () => { | ||
let client: AthenaClient; | ||
beforeEach((test) => { | ||
client = new AthenaClient({ | ||
url: '/example', | ||
adapter: adapter, | ||
getCacheKey: (v: Entity, parent: Entity) => { | ||
return `${v.__typename}:${( | ||
v?.id ?? hashCode(JSON.stringify({ ...v, parent: parent?.id })) | ||
).replace(/:/g, '&')}`; | ||
}, | ||
beforeEach((test) => { | ||
client = new AthenaClient({ | ||
url: '/example', | ||
adapter: adapter, | ||
getCacheKey: (v: Entity, parent: Entity) => { | ||
if (v.id) { | ||
return `${v.__typename}:${v?.id}`; | ||
} | ||
}, | ||
}); | ||
}); | ||
}); | ||
describe('processEntities', () => { | ||
test('parses a single entity', async () => { | ||
@@ -322,3 +351,168 @@ const document = { | ||
}); | ||
test('should be able to handle entities with no cacheable nested values', async () => { | ||
const document = { | ||
paginatedCommentsPage: { | ||
threadUrn: 'urn:li:activity:7070125034782027776', | ||
id: 'urn:li:activity:7070125034782027776', | ||
comments: [ | ||
{ | ||
socialMetadata: { | ||
reactionSummaries: [ | ||
{ | ||
count: 1, | ||
type: 'APPRECIATION', | ||
__typename: 'ReactionSummary', | ||
}, | ||
], | ||
id: 'urn:li:comment:(activity:7070125034782027776,7071631601935249410)', | ||
viewerReaction: { | ||
type: 'APPRECIATION', | ||
}, | ||
commentSummary: { | ||
count: 0, | ||
}, | ||
__typename: 'SocialMetadata', | ||
}, | ||
createdAt: 1686008358464, | ||
id: 'urn:li:comment:(urn:li:activity:7070125034782027776,7071631601935249410)', | ||
author: { | ||
firstName: 'Bob', | ||
lastName: 'Bobberson', | ||
profilePicture: { | ||
url: 'https://media.licdn.com/dms/image/C5603AQGjVp-oZT1bnw/profile-displayphoto-shrink_400_400/0/1561741260600?e=1692835200&v=beta&t=kUlBHG3Fe5z4ao2vgCyP8DVR6nERCy4fTXxAnCxo9F8', | ||
}, | ||
id: 'urn:li:member:655184127', | ||
profileCanonicalUrl: { | ||
url: 'https://www.linkedin.com/in/bob-bobberson', | ||
}, | ||
__typename: 'Profile', | ||
memberUrn: 'urn:li:member:655184127', | ||
networkDistance: { | ||
distance: 'SELF', | ||
}, | ||
headline: 'Code Janitor at LinkedIn', | ||
}, | ||
__typename: 'Comment', | ||
media: [], | ||
message: [ | ||
{ | ||
__typename: 'AttributedTextSegment', | ||
text: "hello again'", | ||
}, | ||
], | ||
}, | ||
], | ||
__typename: 'PaginatedCommentsPage', | ||
}, | ||
}; | ||
const result = await client.processEntities(document); | ||
expect( | ||
result.paginatedCommentsPage.comments[0].socialMetadata | ||
.reactionSummaries[0].type | ||
).toEqual('APPRECIATION'); | ||
expect( | ||
result.paginatedCommentsPage.comments[0].socialMetadata | ||
.reactionSummaries[0].count | ||
).toEqual(1); | ||
expect( | ||
result.paginatedCommentsPage.comments[0].socialMetadata.viewerReaction | ||
.type | ||
).toEqual('APPRECIATION'); | ||
await client.processEntities({ | ||
toggleSocialReaction: { | ||
socialMetadata: { | ||
reactionSummaries: [ | ||
{ | ||
count: 1, | ||
type: 'INTEREST', | ||
__typename: 'ReactionSummary', | ||
}, | ||
], | ||
id: 'urn:li:comment:(activity:7070125034782027776,7071631601935249410)', | ||
viewerReaction: { | ||
type: 'INTEREST', | ||
__typename: 'Reaction', | ||
}, | ||
commentSummary: { | ||
count: 0, | ||
__typename: 'CommentSummary', | ||
}, | ||
__typename: 'SocialMetadata', | ||
}, | ||
__typename: 'ToggleSocialReactionResult', | ||
responseCode: 'OK_200', | ||
}, | ||
}); | ||
expect( | ||
result.paginatedCommentsPage.comments[0].socialMetadata | ||
.reactionSummaries[0].type | ||
).toEqual('INTEREST'); | ||
expect( | ||
result.paginatedCommentsPage.comments[0].socialMetadata.viewerReaction | ||
.type | ||
).toEqual('INTEREST'); | ||
}); | ||
}); | ||
describe('query', async () => { | ||
const server = await createServer(); | ||
beforeAll(async () => await server.listen()); | ||
afterEach(() => server.reset()); | ||
afterAll(() => server.close()); | ||
test('should be able to query a basic endpoint', async () => { | ||
const client = new AthenaClient({ | ||
url: server.buildUrl('/graphql'), | ||
adapter: adapter, | ||
getCacheKey: (v: Entity, parent: Entity) => { | ||
if (v.id) { | ||
return `${v.__typename}:${v?.id}`; | ||
} | ||
}, | ||
}); | ||
server.post( | ||
'/graphql', | ||
async ( | ||
request: http.IncomingMessage, | ||
response: http.ServerResponse | ||
) => { | ||
response.writeHead(200, { 'Content-Type': 'application/json' }); | ||
response.end( | ||
JSON.stringify({ | ||
data: await mocker.mock(peopleQuery, { | ||
people: [ | ||
{ | ||
id: 12, | ||
name: 'Bob', | ||
}, | ||
], | ||
}), | ||
}) | ||
); | ||
} | ||
); | ||
const { data, error } = await client.query(peopleQuery, {}); | ||
expect(error).toBeUndefined(); | ||
expect(data).toMatchInlineSnapshot(` | ||
{ | ||
"people": [ | ||
{ | ||
"__typename": "Person", | ||
"id": 12, | ||
"name": "Bob", | ||
}, | ||
], | ||
} | ||
`); | ||
}); | ||
}); | ||
}); |
@@ -21,3 +21,3 @@ import type { CacheKey, PropertyPath } from './client.js'; | ||
links: Map<string, Link>; | ||
records: Map<string, Record<string, Scalar>>; | ||
records: Map<string, Record<string, Scalar> | WithSignal<Entity>>; | ||
signals: Map<string, WeakRef<WithSignal<Entity>>>; | ||
@@ -24,0 +24,0 @@ private registry; |
@@ -50,3 +50,3 @@ import type { buildCache } from '@data-eden/cache'; | ||
} | ||
export type IdFetcher<T = any> = (v: T, parent: T) => string; | ||
export type IdFetcher<T = any> = (v: T, parent: T) => string | undefined; | ||
//# sourceMappingURL=types.d.ts.map |
{ | ||
"name": "@data-eden/athena", | ||
"version": "0.13.2", | ||
"version": "0.14.0", | ||
"repository": { | ||
@@ -27,3 +27,3 @@ "type": "git", | ||
"dependencies": { | ||
"@data-eden/cache": "^0.13.2", | ||
"@data-eden/cache": "^0.14.0", | ||
"date-fns": "^2.30.0", | ||
@@ -33,2 +33,5 @@ "lodash-es": "^4.17.21" | ||
"devDependencies": { | ||
"@data-eden/codegen": "0.14.0", | ||
"@data-eden/mocker": "0.14.0", | ||
"@data-eden/shared-test-utilities": "0.14.0", | ||
"@graphql-typed-document-node/core": "^3.2.0", | ||
@@ -35,0 +38,0 @@ "@signalis/core": "^0.1.0", |
@@ -202,2 +202,7 @@ import type { Cache } from '@data-eden/cache'; | ||
if (!key) { | ||
// if we don't have a key it is not cacheable and must be cached based on the parent | ||
continue; | ||
} | ||
// replace the entity object with the key we're using to store it in the cache so that we can | ||
@@ -204,0 +209,0 @@ // later replace the key with the reactive entity |
@@ -53,3 +53,3 @@ import { addMilliseconds, getTime } from 'date-fns'; | ||
links = new Map<string, Link>(); | ||
records = new Map<string, Record<string, Scalar>>(); | ||
records = new Map<string, Record<string, Scalar> | WithSignal<Entity>>(); | ||
signals = new Map<string, WeakRef<WithSignal<Entity>>>(); | ||
@@ -145,7 +145,19 @@ private registry: FinalizationRegistry<string>; | ||
const arrayLink: Array<string> = []; | ||
const recordArray: any[] = []; | ||
value.forEach((link) => { | ||
if (isLinkNode(link)) { | ||
arrayLink.push(link.__link); | ||
} else { | ||
// we don't have a link and we need to attach this to the value itself as the reactivity is owned by the parent | ||
recordArray.push(link); | ||
} | ||
}); | ||
// We only want to update the array value on the entity if the record has an array value or if the record has a length | ||
// This is to ensure we update the record with an empty array if had an original value | ||
if ( | ||
recordArray.length > 0 || | ||
(record[entityKey] && Array.isArray(record[entityKey])) | ||
) { | ||
record[entityKey] = recordArray; | ||
} | ||
links[entityKey] = arrayLink; | ||
@@ -236,3 +248,7 @@ } else if (isLinkNode(value)) { | ||
const toRemove = parentArray.filter((v) => { | ||
return !value.includes(this.getCacheKey(v, parent)); | ||
const key = this.getCacheKey(v, parent); | ||
// if there is no key we can't consistency manage it. | ||
// it will get overriden when new values are returned on the parent object | ||
return key && !value.includes(key); | ||
}); | ||
@@ -299,2 +315,6 @@ | ||
// in order to get consistency updates on non managed field updates we set the signal if we have it | ||
// so we can update it in storeEntity | ||
this.records.set(entityKey, root); | ||
return root; | ||
@@ -301,0 +321,0 @@ } |
@@ -69,2 +69,2 @@ import type { buildCache } from '@data-eden/cache'; | ||
export type IdFetcher<T = any> = (v: T, parent: T) => string; | ||
export type IdFetcher<T = any> = (v: T, parent: T) => string | undefined; |
import { defineConfig } from 'vitest/config'; | ||
import { resolve } from 'node:path'; | ||
import { rollupPlugin } from '@data-eden/codegen'; | ||
export default defineConfig({ | ||
plugins: [ | ||
rollupPlugin({ | ||
baseDir: resolve(__dirname, '__tests__'), | ||
schemaPath: resolve( | ||
__dirname, | ||
'..', | ||
'..', | ||
'internal-packages', | ||
'react-graphql-test-app', | ||
'src', | ||
'graphql', | ||
'schema.graphql' | ||
), | ||
documents: ['**/*.test.ts'], | ||
extension: '.graphql.ts', | ||
disableSchemaTypesGeneration: false, | ||
production: false, | ||
}), | ||
], | ||
test: { | ||
testTimeout: 10_000, | ||
include: ['__tests__/**/*.test.ts'], | ||
}, | ||
@@ -11,4 +32,5 @@ resolve: { | ||
'@data-eden/athena': resolve(__dirname, './src'), | ||
'@data-eden/mocker': resolve(__dirname, '../mocker/src'), | ||
}, | ||
}, | ||
}); |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
1021769
6272
8
+ Added@data-eden/cache@0.14.0(transitive)
- Removed@data-eden/cache@0.13.2(transitive)
Updated@data-eden/cache@^0.14.0