graphiti-api / spraypaint.js Goto Github PK
View Code? Open in Web Editor NEWGraphiti Client / Javascript ORM / JSONAPI
License: MIT License
Graphiti Client / Javascript ORM / JSONAPI
License: MIT License
Using a simple relationship between a Company and Country, and when the Country is already persisted:
@Model()
export class Country extends ApplicationRecord {
static jsonapiType = "countries"
@Attr() name: string
}
@Model()
export class Company extends ApplicationRecord {
static jsonapiType = "companies"
@Attr() name: string
@BelongsTo() country: Country
}
var country = new Country({ id: '123', name: 'USA' });
country.isPersisted = true;
Creation via direct assignment appears to work normally (it establishes a relationship):
var company = new Company({ name: 'ACME' });
company.country = country;
company.save({ with: 'country' });
{"data":{"type":"companies","attributes":{"name":"ACME"},"relationships":{"country":{"data":{"type":"countries","id":"123","method":"update"}}}},"included":[{"type":"countries","id":"123"}]}
While creation via the constructor does not (it does not establish the relationship):
var company = new Company({ name: 'ACME', country: country });
company.save({ with: 'country' });
{"data":{"type":"companies","attributes":{"name":"ACME"}}}
An identifiable difference between the two approaches (just before the save is performed) is that via the constructor company._originalRelationships
is populated with information related to the country, while during direct assignment it is empty.
I'd like to be able to use the model.isDirty()
method within VueJS computed values for immediate DOM updates when a model becomes dirty. I believe that this currently does not work because of the way VueJS observes referenced objects within a computed method for property access on first run and subsequently assigns getter/setter methods with which it can determine that a change has been made and therefore trigger a regeneration of the computed value.
My first thought was to touch/read all model attribute methods within isDirty()
, but because of the way it (rightfully) short-circuits checking all objects when the first dirty object is found, it wouldn't trigger reactivity in all places. Perhaps a method on the models that could be called in tandem within the computed value?
Or perhaps a better implementation if my need is valid for this project?
Here is a sample of what I'm currently thinking of doing.
HTML:
div.alert(v-if="assetChanged") You have unsaved data!
VueJS component:
computed:
assetChanged: ->
if @.asset
relationships = ['relation1', 'relation2': 'child-relation']
@.asset.establishReactivity(relationships)
@.asset.isDirty(relationships)
Hi, when I try to update a model, it will send all object attributes to server which is not really ideal since sometimes objects are huge and donot meant to be updated.
Found this line of code here:
spraypaint.js/src/util/dirty-check.ts
Line 63 in af9493e
Update to JSON.stringify(prior) !== JSON.stringify(current)
would solve the problem.
Also, this similar issue here:
Line 475 in af9493e
Inside self[k] !== attrs[k] && !changes[k]
, if an attribute is an object, self[k] !== attrs[k]
might always return true and !changes[k]
will always return false. So, model.ts#L480 will never happen.
It would be really nice if I could do this:
Resource.where({ createdAt: { lte: new Date() } }).all()
But in order for spraypaint to recognise the filter, I need to stringify it first, otherwise it silently ignores it.
Resource.where({ createdAt: { lte: new Date().toUTCString() } }).all()
Hi,
I got here by asking for support on the jsorm slack, and was directed to an example application which uses this library.
My concern is that I am using jsorm for its compatibility with jsonapi, coupled with a django backend.
I was hoping it could be made clear either in this repository or in the documentation whether spraypaint will maintain jsonapi compatibility, or if there is a chance graphiti may diverge from jsonapi and take spraypaint with it.
E.g. a Company
that has many Employee
company.employees = [];
company.save({ with: 'employees' });
This is the payload which doesn't include the relationships
field:
{ "data": { "type": "companies" } }
It seems to be because of this line explicitly setting it to null
https://github.com/graphiti-api/spraypaint.js/blob/master/src/util/write-payload.ts#L115
I just wonder this is intended? (JSON:API spec allows replacement of the entire array: https://jsonapi.org/format/#crud-updating-resource-relationships)
I believe the code here: https://github.com/graphiti-api/spraypaint.js/blob/master/src/util/validation-error-builder.ts#L71 is lacking an additional presence check of the r.temp_id
and.or meta["temp-id"]
as are done in many other places in the code.
For a relationship where there are no temporary ids on any elements, this causes the first element to be returned always.
Also, it seems there is a type issue with id
s. For all the records I fetch via spraypaint the object's id
is a string. However, the id
coming back in the validation errors is an integer. And thus the ===
won’t match.
I have made a branch of employee-directory
which demonstrates this issue: https://github.com/gstark/employee_directory/tree/demonstrate-validation-error-builder-bug
To run:
bundle && rails db:setup && rails server
In a separate shell:
cd js
yarn install
node demo.js
Hello there!
Due to this code the reset function doesn't reset the state to the original attributes. Switching it to this._attributes = cloneDeep(this._originalAttributes)
should fix it.
I have 2 resources in different namespaces with the same base url. When I try and set the jsonapiType
for both of these to the same string - my_type
in my models, I receive:
Uncaught Error: Type "<my_type>" already registered on base class function Subclass() {
return _super !== null && _super.apply(this, arguments) || this;
I had a little look and I think it's down to JsonapiTypeRegistry
class.
Will be happy to submit a PR.
I really like the idea of saving a resource as easily as resource.save()
.
Even cooler would be if the saving (or loading) status would be available on the resource, too. This would allow to just bind resource.isSaving
e.g. to a loading indicator without having to create additional variables.
When defining a model, it appears that only attributes are getting converted to camelCase but relationship keys have to be defined in snake case or else I get the error Unknown type "appointment_requests"
I have an Appointment model that is currently setup like this:
const Appointment = ApplicationRecord.extend({
static: {
jsonapiType: "appointments",
},
attrs: {
appointmentRequests: hasMany(),
},
});
It has a has many relationship to the AppointmentRequests model that looks like this:
const AppointmentRequests = ApplicationRecord.extend({
static: {
jsonapiType: "appointmentRequests",
},
attrs: {
appointment: belongsTo(),
startTime: attr(),
},
});
However, I currently have to change appointmentRequests
to appointment_requests
in both models in order to be able to access the related appointment requests from an appointment record. It seems like I should be able to define the relationships in camelCase since other attributes get converted from snake_case to camelCase correctly. Has anyone else seen or run into this issue before?
Hey everyone,
(see solution at end of post, or read the entire thread)
I'm starting a new project in VueJS, coming from an Ember project, and both will be using the same JSON:API backend.
I looked around for libraries to Help with the JSON:API in the Vue project and landed on Spraypaint after a bit of search, and it seems a perfect match, but I'm having an issue that I can't simple figure out.
This is my base model:
import { Model, SpraypaintBase } from 'spraypaint';
import envConfig from '@/configEnv.js';
@Model()
export class ApplicationRecord extends SpraypaintBase {
static baseUrl = envConfig.url;
static apiNamespace = '/api';
}
And this is my model
import { Model, Attr } from 'spraypaint';
import { ApplicationRecord } from './BaseAssemblyModel';
@Model()
export class Client extends ApplicationRecord {
static jsonapiType = 'project-clients';
@Attr() name: string | null;
}
I can query just fine, but I have an issue with saving (in this case, updating an existing model, but the same happens when creating a new one)
When I ask to await client.save()
the json that is sent to the backend doesn't include the name
property defined in the model.
If I inspect the data however, it's there:
However, the serialized JSON doesn't have it. And apparently, from what I can investigate... it's because of this:
The attributes
property returns empty all the time.
The typedAttributes
always returns empty :(. Further digging, and It seems the model._attributes
is never set, I've put debugger statements, live debugging, but don't find a place where _attributes
is set and it's always empty.
It seems such a basic thing, that I'm not sure what I'm doing wrong... any ideas? Thanks :)
Solution: The compilerOptions
of tsconfig.json
needed to be changed, we had target: esnext
and needed to change to target:es2017
. Thanks @bilouw for the help
It would be really helpful to allow overwriting or mock the default http client. This is especially useful when the existing app is based on axios with many predefined axios interceptors and also when using server side rendering.
The lack of typecasting on attributes is preventing the ability to override getters and setters for attributes when using typescript.
Hi, I'm looking to follow this JSON:API recommendation for batch processing:
https://jsonapi.org/recommendations/#asynchronous-processing
My use case is that I have a delete that is super slow (many minutes). I would like to put that into the background job, then return the job details in the 202 response like that document says.
I look into our source code and see this
Line 907 in de34db3
I realize that there is a difficulty with implementing this feature: the model returned by the response is usually completely different from the model that is deleted. I'm not sure how to look up the appropriate model based on the type in the response.
Hi,
I have the following error on an empty Nuxt project based on typescript (npx create-nuxt-app . followed by https://typescript.nuxtjs.org/guide/setup.html)
ERROR INTERNAL ERROR(undefined,undefined) Maximum call stack size exceeded nuxt:typescript 14:25:58
stack trace:
RangeError: Maximum call stack size exceeded
at isRelatedTo (/workspaces/app/node_modules/typescript/lib/typescript.js:43632:33)
at someTypeRelatedToType (/workspaces/app/node_modules/typescript/lib/typescript.js:44028:35)
at isRelatedTo (/workspaces/app/node_modules/typescript/lib/typescript.js:43752:34)
at typeRelatedToEachType (/workspaces/app/node_modules/typescript/lib/typescript.js:44013:35)
at isRelatedTo (/workspaces/app/node_modules/typescript/lib/typescript.js:43730:34)
at typeRelatedToSomeType (/workspaces/app/node_modules/typescript/lib/typescript.js:43913:35)
at isRelatedTo (/workspaces/app/node_modules/typescript/lib/typescript.js:43719:34)
at eachTypeRelatedToType (/workspaces/app/node_modules/typescript/lib/typescript.js:44040:35)
at isRelatedTo (/workspaces/app/node_modules/typescript/lib/typescript.js:43715:25)
at isPropertySymbolTypeRelated (/workspaces/app/node_modules/typescript/lib/typescript.js:44717:28)
This error occurs as soon as I use the decorator @Model()
example: at the moment, I only define the ApplicationRecord
like this:
import { SpraypaintBase, Model } from 'spraypaint';
@Model()
export default class ApplicationRecord extends SpraypaintBase {
static baseUrl = 'http://localhost:3000';
public static apiNamespace = '/api/v1';
}
If I remove the decorator, no error is raised.
for info:
node@d92271203242:/workspaces/app$ yarn dev
yarn run v1.19.1
$ nuxt -H 0.0.0.0 -p 3001
╭───────────────────────────────────────────╮
│ │
│ Nuxt.js v2.10.2 │
│ Running in development mode (spa) │
│ │
│ Listening on: http://localhost:3001/ │
│ │
╰───────────────────────────────────────────╯
ℹ Preparing project for development 14:33:39
ℹ Initial build may take a while 14:33:39
✔ Builder initialized 14:33:40
✔ Nuxt files generated 14:33:40
ℹ Starting type checking service... nuxt:typescript 14:33:56
ℹ Using 1 worker with 2048MB memory limit nuxt:typescript 14:33:56
✔ Client
Compiled successfully in 20.55s
ERROR INTERNAL ERROR(undefined,undefined) Maximum call stack size exceeded nuxt:typescript 14:34:17
stack trace:
RangeError: Maximum call stack size exceeded
at getPropertyOfObjectType (/workspaces/app/node_modules/typescript/lib/typescript.js:39024:31)
at isKnownProperty (/workspaces/app/node_modules/typescript/lib/typescript.js:50819:21)
at isKnownProperty (/workspaces/app/node_modules/typescript/lib/typescript.js:50828:25)
at hasCommonProperties (/workspaces/app/node_modules/typescript/lib/typescript.js:45147:21)
at isRelatedTo (/workspaces/app/node_modules/typescript/lib/typescript.js:43692:58)
at typeRelatedToSomeType (/workspaces/app/node_modules/typescript/lib/typescript.js:43913:35)
at isRelatedTo (/workspaces/app/node_modules/typescript/lib/typescript.js:43719:34)
at eachTypeRelatedToType (/workspaces/app/node_modules/typescript/lib/typescript.js:44040:35)
at isRelatedTo (/workspaces/app/node_modules/typescript/lib/typescript.js:43715:25)
at isPropertySymbolTypeRelated (/workspaces/app/node_modules/typescript/lib/typescript.js:44717:28)
ℹ Version: typescript 3.6.4 nuxt:typescript 14:34:17
ℹ Time: 15674ms nuxt:typescript 14:34:17
ℹ Waiting for file changes 14:34:17
ℹ Memory usage: 510 MB (RSS: 621 MB) 14:34:17
Hi, in my own backend that implements JSON:API, I use the links as indicated the documentation of JSON:API like this https://jsonapi.org/format/#document-links . I wonder if you guys plan to support it? I'm thinking of something like the @Attr
you already did:
@Model()
class Person extends ApplicationRecord {
static jsonapiType = "people"
@Link() self: string
}
If you guys think it's good idea to support it then I can attempt a PR.
I have a join table model C that joins models A and B (i.e. on the backend C has two columns a_id
and b_id
and on the frontend C has two BelongTo's for each of a and b).
If I do the following, saving c
does not work.
a = (await A.find(1)).data
b = new B({field: 'value'})
c = new C({a, b})
c.save(with: ["a", "b"])
This fails with the message "A must exist".
The solution is to do c.save(with: ["a.id", "b"])
.
IMO this isn't exactly intuitive. I believe it's checking for the dirtiness of a
and b
to determine if they should be sent up to the server for saving. In this case a
isn't dirty and b
is dirty (it's new) so only b gets sent up. IMO it should be checking for the dirtiness of c.a
and c.b
aka the dirtiness of the pointers rather than the things the pointers point to.
Thoughts?
I was trying to use the library without modules, by directly loading from cdn, and SpraypaintBase
was undefined. As far as I understand, there issue is here:
Line 2 in cae16da
process
is not defined. I removed the resulting lines from the generated js file, and now it seems to work ok.Is there a way to generate json payload from a model's instance? Right now I have this in my code which seems to work, but curious if an alternative exists
import { WritePayload } from 'spraypaint/lib-esm/util/write-payload';
@Model()
class Model extends SpraypaintBase {
toJsonPayload = () => {
return new WritePayload(this).asJSON();
};
}
Hi,
We are using Spraypaint with another JSONAPI compliant backend. The backend returns validation errors as 400's, while Spraypaint explicitly looks for 422's. The result is an "invalid json" exception being thrown, when in fact it's not. This also means I am cut off short when trying to get access to the errors payload.
Can you make the 422 validation status code configurable? or at least refactor the code to spit out the payload so I can handle it myself?
Thanks
The newest version of typescript that was recently released is not compatible with spraypaint. We've temporarily locked to compatible versions in our tests, but we should figure out how to fix this issue before too long. Failing build is below:
https://travis-ci.org/graphiti-api/spraypaint.js/jobs/541274953
How to handle polymorphic fields?
I suggest adding a new association that takes a list of resource classes, and figure out the proper class to be used based on the type
.
Input will be tricky though.
const bazScope = Baz.selectExtra(['baz_optional_field']);
const returnScope = Foo.selectExtra(['foo_optional_field']).includes(['foo_sideload']).
merge({bars: Bar.merge({ baz_sideload: bazScope }).includes(['bar_sideload']});
parent.save({ returnScope });
This will call a PATCH URL like /parents?extra_fields[foos]=foo_optional_field&extra_fields[bazes]=baz_optional_field&include=foo_sideload
As you can see the extra_fields all gets constructed correctly, but the include only has the Foo sideloads and not the nested Bar sideloads.
Given that the URL as passed in as a string to beforeFilters
, it's unable to be modified.
The use case for us is that we have an endpoint that we need to use HTTP POSTs for reads due to very long filter strings. beforeFilters
allows the HTTP method to be changed to POST, and allows for translating the query string to a POST body, but we're unable to strip the query string from the URL.
I'd imagine the fix for this would be nesting the url
and options
filter args under an object, which would be a breaking change. I'd be happy to write a PR if it's decided that this would be an acceptable feature.
Another approach might be to check to see if the method has changed from GET to POST/PUT/PATCH and do that translation and query string removal automatically.
I have been using vue, typescript, jsorm, nodejs,jsonapi, nest.js for the application,
need to issue Patch request to people/:id with some meta data,
but i am not able to find any document to do it, like do I use something like this,
@Model({ jsonapiType: 'people/:id', }) export default class ValidateEmail extends AppAuthRecord { @Attr() id!: string; }
suppose if the user id is 5, which we are fetching from the url of frontend, then how could i append to the new http request and make it a fetch request.
Hi,
When attempting to persist relationships, the addition of the HTTP verb in the method
field of the relationship's data
payload may not be strictly JSONAPI-compliant.
As per the specification:
A resource object MUST contain at least the following top-level members:
id
type
Exception: The id member is not required when the resource object originates at the client and represents a new resource to be created on the server.In addition, a resource object MAY contain any of these top-level members:
attributes: an attributes object representing some of the resource’s data.
relationships: a relationships object describing relationships between the resource and other JSON:API resources.
links: a links object containing links related to the resource.
meta: a meta object containing non-standard meta-information about a resource that can not be represented as an attribute or relationship.
This additional field provokes errors in some server-side implementations, as a non-valid data
representation.
Would there be a way to deal with sideposting in another fashion? Or allow the developer to choose whether or not to enable this "Graphiti-only" behaviour?
We are seeing a failed validation that populates the errors
attribute returned with the new entity, but the hasError
attribute is still false.
It looks like spraypaint checks for greater than 1 error:
Line 690 in cae16da
Hello you wonderful people,
I found an error in the validation-error-builder in Line 74. The error can be summarised as such:
r.temp_id === meta["temp-id"]
undefined === undefined
.I'll submit a PR with a test case right away. :)
I'm really enjoying using Spraypaint (currently in React, and potentially next in React Native).
I am getting an uncaught RangeError: Maximum call stack size exceeded
when model instances are trying to unlisten. I have model syncing turned on:
ApplicationRecord.sync = true
I have two models, Course and Group. A course belongs to a group, and a group has many courses.
const Course = ApplicationRecord.extend({
static: {
jsonapiType: 'courses',
},
attrs: {
name: attr(),
group: belongsTo(),
}
})
const Group = ApplicationRecord.extend({
static: {
jsonapiType: 'groups',
},
attrs: {
name: attr(),
courses: hasMany(),
}
})
I am loading both from the API:
Group.includes({ courses: 'group' }).find(123)
I get the RangeError: Maximum call stack size exceeded
when the .unlisten
method is called on all instances. Those models are recursively checking their relationships and are getting stuck in a loop when traversing from Group -> Course -> Group -> Course -> etc.
It hits the exception on model.ts line 548:
Object.keys(this.relationships).forEach(k => {
let related = this.relationships[k]
if (related) {
if (Array.isArray(related)) { // <-- Exception happens here
related.forEach(r => r.unlisten())
} else {
related.unlisten()
}
}
})
Let me know if this is a known issue or you need any other documentation! Thanks.
I'm Running:
Spraypaint v0.10.23
React v16.13.1
Node v14.4.0
Hi,
It seems like the docs cover how to associate an object with a relation, however the relation seems to be stripped from the payload.
const properties = formData.properties.map((id) => new Property({ id }));
const propertyList = new PropertyList({ name: formData.name, properties: [properties]});
return propertyList.save({ with: 'properties.id' });
Is there something I am missing?
@Model()
class Property extends ApplicationRecord {
static jsonapiType = 'properties';
@Attr() name = null;
@Attr() deletedAt = null;
@HasOne() address = null;
}
export default Property;
import { Model, Attr, HasMany } from 'jsorm';
import ApplicationRecord from './ApplicationRecord';
@Model()
class PropertyList extends ApplicationRecord {
static jsonapiType = 'property_lists';
@Attr() name = null;
@HasMany() users = null;
@HasMany() properties = null;
}
export default PropertyList;
Resulting request body for the server:
data: {type: "property_lists", attributes: {name: "123123"}}
Thank you for your time!
Forgive me if this is an operator error, but I think we're seeing Spraypaint being unable to cope with graphiti produced validation errors on (deeply) nested relationships.
We have a Delivery 1 <> n ContentsVersions 1 <> n Contents
relationship which is modelled server-side roughly like so:
# Models:
class Delivery < ApplicationRecord
has_many :contents_versions
end
class ContentsVersions < ApplicationRecord
belongs_to :delivery
has_many :contents
validates :total_price, numericality: { greater_than: 0 }
end
class Contents < ApplicationRecord
belongs_to :contents_version
end
# Resources:
class DeliveryResource < ApplicationResource
attribute :courier_name, :string
has_many :contents_versions
end
class ContentsVersionResource < ApplicationResource
attribute :total_price, :big_decimal
belongs_to :delivery
has_many :contents
end
class ContentResource < ApplicationResource
attribute :product_id, :string,
attribute :qty, :integer
belongs_to :contents_version
end
Our Spraypaint models correspondingly look something like this:
const Delivery = ApplicationRecord.extend({
attrs: {
contents_versions: hasMany(),
courier_name: attr(),
},
static: {
jsonapiType: 'deliveries',
},
});
const DeliveriesContentsVersion = ApplicationRecord.extend({
attrs: {
contents: hasMany(),
delivery: belongsTo(),
delivery_id: attr(),
},
static: {
jsonapiType: 'contents_versions',
},
});
const DeliveriesContent = ApplicationRecord.extend({
attrs: {
contents_version: belongsTo(),
contents_version_id: attr(),
product_id: attr(),
qty: attr(),
},
static: {
jsonapiType: 'contents',
},
});
When we try to create a Delivery
side-posting a ContentsVersion
with a Content
, but the ContentsVersion
fails to validate server-side, we get this error:
TypeError: "relatedObject is undefined"
_processRelationship validation-error-builder.js:56
apply validation-error-builder.js:20
apply validation-error-builder.js:16
apply validation-error-builder.js:8
_handleResponse model.js:764
The error gets raised when trying to parse the error related to the Contents#content_version
presence validation. Please find a full copy of our version of the _processRelationship
function further below.
Our request payload looks like this:
{
"data": {
"attributes": {
"courier_name": "DPD"
},
"relationships": {
"contents_versions": {
"data": [
{
"temp-id": "temp-id-12",
"method": "create",
"type": "contents_versions"
}
]
}
},
"type": "deliveries"
},
"included": [
{
"temp-id": "temp-id-12",
"attributes": {
"total_price": "0.0"
},
"relationships": {
"contents": {
"data": [
{
"temp-id": "temp-id-13",
"method": "create",
"type": "contents"
}
]
}
},
"type": "contents_versions"
},
{
"temp-id": "temp-id-13",
"attributes": {
"product_id": "1",
"qty": 1
},
"type": "contents"
}
]
}
Note that this payload format seems to be correct. When we send data that passes the validation, all resources get created correctly. With invalid data, however, the servers responds as follows:
{
"errors": [
{
"status": "422",
"code": "unprocessable_entity",
"source": {
"pointer": "/data/attributes/total_price"
},
"meta": {
"relationship": {
"attribute": "total_price",
"temp-id": "temp-id-12",
"name": "contents_versions",
"code": "zero_price",
"type": "contents_versions",
"message": "must be larger than 0"
}
},
"title": "Validation Error",
"detail": "Total price must be larger than 0"
},
{
"status": "422",
"code": "unprocessable_entity",
"source": {
"pointer": "/data/relationships/contents_version"
},
"meta": {
"relationship": {
"attribute": "contents_version",
"temp-id": "temp-id-13",
"name": "contents",
"code": "blank",
"type": "contents",
"message": "must exist"
}
},
"title": "Validation Error",
"detail": "Contents version must exist"
}
]
}
From this error response, Spraypaint does not seem able to figure out that the second object relates to the deeply nested Delivery => ContentsVersion => Content.
ValidationErrorBuilder.prototype._processRelationship = function (model, meta, err) {
var relatedObject = model[model.klass.deserializeKey(meta.name)];
if (Array.isArray(relatedObject)) {
relatedObject = relatedObject.find(function (r) {
return r.id === meta.id || r.temp_id === meta["temp-id"];
});
}
if (meta.relationship) {
this._processRelationship(relatedObject, meta.relationship, err);
}
else {
var relatedAccumulator_1 = {};
this._processResource(relatedAccumulator_1, meta, err);
// make sure to assign a new error object, instead of mutating
// the existing one, otherwise js frameworks with object tracking
// won't be able to keep up. Validate vue.js when changing this code:
var newErrs_1 = {};
Object.keys(relatedObject.errors).forEach(function (key) {
newErrs_1[key] = relatedObject.errors[key];
});
Object.keys(relatedAccumulator_1).forEach(function (key) {
var error = relatedAccumulator_1[key];
if (error !== undefined) {
newErrs_1[key] = error;
}
});
relatedObject.errors = newErrs_1;
}
};
Annotated with payloads:
I'm confused because the docs have Typescript and ES5 syntax, but I generally write ES6.
I can't figure out how to instantiate my models with ES6. I'd really rather not write TS.
This works:
const ApplicationRecord = SpraypaintBase.extend(
{
static: {
baseUrl: window.location.origin,
apiNamespace: "/api"
}
}
)
const Tag = ApplicationRecord.extend(
{
static: {
jsonapiType: "tags"
},
attrs: {
name: attr(),
},
methods: {
sayHi: function() {
return `Hi I'm a ${this.name} tag`
}
}
}
)
But I'd prefer to write something like this, thought I'm not sure how to define attributes etc... Please help...
export default class ApplicationRecord extends SpraypaintBase {
static baseUrl = window.location.origin
static apiNamespace = "/api"
}
export default class Tag extends ApplicationRecord {
static jsonapiType = "tags"
//attributes ... etc
sayHi() {
return `Hi I'm a ${this.name} tag`
}
}
Spraypaint maps errors in a JSON:API response to model attributes by looking at the attribute
metadata.
{
"errors": [
{
"code": "bad_request",
"status": "400",
"title": "Request Error",
"detail": "...",
"meta": {
"attribute": "name",
"message": "..."
},
"source": {
"pointer": "/data/attributes/name"
}
}
]
}
Spraypaint models currently only support one error per attribute, so if a response includes multiple errors for a single attribute only the last one will be exposed in the errors
object.
https://github.com/graphiti-api/spraypaint.js/blob/master/src/util/validation-error-builder.ts#L56
To allow for multiple errors per attribute we could do something like below, however it would be a breaking change:
diff --git a/src/util/validation-error-builder.ts b/src/util/validation-error-builder.ts
index 2e4fac5..24db9bd 100644
--- a/src/util/validation-error-builder.ts
+++ b/src/util/validation-error-builder.ts
@@ -53,14 +53,15 @@ export class ValidationErrorBuilder<T extends SpraypaintBase> {
error: JsonapiError
) {
let attribute = this.model.klass.deserializeKey(meta.attribute)
- errorsAccumulator[attribute] = {
+ errorsAccumulator[attribute] = errorsAccumulator[attribute] || [];
+ errorsAccumulator[attribute].push({
title: error.title as string,
code: error.code as string,
attribute: meta.attribute,
message: meta.message,
fullMessage: attribute === "base" ? meta.message : error.detail,
rawPayload: error
- }
+ })
}
private _processRelationship<R extends SpraypaintBase>(
With some edge-cases, I need to do some manual requests, since I can't use Spraypaint for arbitrary endpoints.
Those requests return a Graphiti Response (JSON:API Spec) with associations. How can I transform this request into a Model Instance?
Best regards
When, say, filtering, we send what the server understands - this means (by default) underscores and not camelCase. But we should support sending camelCase and converting to underscores (and other casings as well - we already have primitives for this)
If I define a hasOne relationship like:
const Employee = ApplicationRecord.extend({
static: {
jsonapiType: 'employees',
},
attrs: {
name: attr(),
manager: hasOne(),
},
});
I want to be able to fetch an employee, and the later, optionally fetch the manger, e.g.
var {e} = await Employee.first()
var {m} = await e.manager()
console.log(m.name)
Spraypaint doesn't seem to support following links that JSON:API returns in the original request. The ability to load sub-records incrementally is one of the great promises of JSON:API, but it seems that in the current implementation I need to fetch all the data in my original request, using include('manager')
.
Am I missing how to do incremental loading?
Using Angular 8.2.14
and spraypaint 0.10.14
, when compiling with AOT enabled, this error is shown:
ERROR in spraypaint/lib-esm/decorators.ts(14,15): Error during template compile of 'Model'
Only initialized variables and constants can be referenced in decorators because the value of this variable is needed by the template compiler.
I'm not really sure how to proceed with this issue.
Hi There,
'created_at' appears in the api and I can see the raw json but comes through as 'undefined' in the spraypaint model in my angular frontend. Note: we are using the Mongoid adapter which I will include below as well.
How can I debug the spraypaint code to see why the attributes are not coming through to the spraypaint model?
Please advise
# order.rb Model:
class Order
include Mongoid::Document
include Mongoid::Timestamps
field :created_at, type: DateTime
end
# order_resource.rb Resources:
class OrderResource < ApplicationResource
self.adapter = Graphiti::Adapters::Mongoid
attribute :created_at, :datetime
end
// Spraypaint angular frontend model:
// OrderResource.ts
@Model()
export class OrderResource extends ApplicationRecord {
static jsonapiType = "orders"
@Attr() created_at: string
}
# mongoid.rb Adapter
module Graphiti
module Adapters
class Mongoid < ::Graphiti::Adapters::Abstract
def base_scope(model)
model.all
end
def paginate(scope, current_page, per_page)
scope.page(current_page).per(per_page)
end
def resolve(scope)
scope.to_a
end
def order(scope, attribute, direction)
scope.order(attribute => direction)
end
end
end
end
Hi,
Let's say i want to create a Post and associate it to an already created Comment. (in my example, Comment belongs_to Post and Post has_one Comment)
This is what i'm doing:
let post = new Post({ comment: new Comment})
post.comment.id = commentIdToAssociate
post.save({ with: 'comment.id' })
This code will create a new comment associated to the post with method: "create".
I don't want to create this comment, i want to associate it to my post (method: "update")
I want this kind of body:
"relationships": {
"comment": {
"data": {
"id": "64520319-03be-4c29-b86b-199c1038ba23",
"type": " comments",
"method": "update"
}
},
}
How can i do that ?
Thanks.
Hello,
Suppose I use Spraypaint in a React app that is SSR'd. In responding to a request, the frontend server fetches data & instantiates models using Spraypaint/Graphiti layer and, after generating markup, sends markup and data to the client.
The data is a JSON representation of up-to-date models. How can I instantiate those models on the client without making a network request?
I believe I should use middleware to do this, and some methods from the library that are exposed to us.
This looked relevant: #79
Big apology if this is obvious, I've always struggled with Spraypaint because I come from a node background and not a RoR background.
It looks like Spraypaint can't handle 204 - "No content" codes when POSTing or PATCHing.
In those cases, if the server returns 204 and an empty body Spraypaint tries to parse the body (which is not there) and throws an error, while it should just return the success of the operation.
For reference JSON:API considers 204 to be a valid status both for POST (see here and for PATCH (see here)
Note: If a 204 response is received the client should consider the resource object sent in the request to be accepted by the server, as if the server had returned it back in a 201 response.
Say I have a Project
model defined, which in the ProjectResource defined on the backend has a relationship to Board
defined as such:
polymorphic_has_many :boards, as: :boardable
The BoardResource includes
attribute :boardable_id, :uuid
attribute :boardable_type, :string
polymorphic_belongs_to :boardable do
group_by(:boardable_type) do
on(:Client)
on(:Company)
on(:Project)
end
end
Using Spraypaint on the Frontend, I have the Project
and Board
models defined as such:
export const Project = ApplicationRecord.extend({
static: {
jsonapiType: "projects"
},
attrs: {
...
boards: hasMany()
}
});
export const Board = ApplicationRecord.extend({
static: {
jsonapiType: "boards"
},
attrs: {
name: attr(),
boardable_id: attr(),
boardable_type: attr()
}
})
When I query for a project using Project.find(projectId)
I'm getting the following error:
Error: "Unknown type "boards""
I've tried including boardable: belongsTo()
in the Board model on the frontend, but that doesn't help.
Can someone point me in the right direction?
Thanks in advance!
Hey, I've been looking into graphitti and saw that you use classes to define relationships in the JS Client.
I'm currently using https://github.com/ghidoz/angular2-jsonapi with the "jsonapi suite" and they use a similar approach for this. But while using it I've had to upgrade to a new angular version, which also upgraded my "target" in tsconfig
to es2015
.
Now the issue is that it seems that when you change the target, you can no longer define classes that depend on each other in this way, see: ghidoz/angular2-jsonapi#236 (comment). Meaning if you define Post
which hasMany Comments
, but Comment
also belongsTo Post
it would throw a Uncaught ReferenceError: Cannot access '<insert class herr>' before initialization
error.
Would this be an issue with spraypaint.js
as well?
Is it possible to specify a different authorization header per request?
I have a server side api (NestJS) that makes calls to another jsonapi service and I was hoping to use Spraypaint to simplify those requests.
The current implementation seems to assume this is running in a browser and uses static methods to set the authorization header.
Everything works great calling external api except the auth methods are static.
This works but seems very unsafe
Widget.setJWT(token);
const widgets = Widget.per(10).all();
Widget.setJWT("");
Is there another way to handle this scenario with Spraypaint?
Hi There
We're using spraypaint together with typescript and stumbled across an error when an attribute has the name kind
.
this code here
import { Attr, Model, BelongsTo, SpraypaintBase } from "spraypaint";
@Model()
class Person extends SpraypaintBase {
@Attr() name: string;
}
@Model()
class Dog extends SpraypaintBase {
@Attr() name: string;
@Attr() kind: string; // if renamed, things are working properly
@BelongsTo() person: Person; // if removed, things are working properly
}
leads to the following error
TypeError: context.attributes is undefined
getter attribute.ts:82
get attribute.ts:93
isModernDecoratorDescriptor decorators.ts:55
fn decorators.ts:318
__decorate index.ts:5
<anonymous> index.ts:34
<anonymous> index.ts:42
<anonymous> index.ts:43
We figured out that the error only occurs if there is some kind of relationship on the model with the attribute.
See minimal demo here: https://stackblitz.com/edit/typescript-xyevlu?file=index.ts
Does someone know whats the problem here?
Thanks in advance.
Hi, thanks for your great work with Spraypaint & Graphiti !
I can't use the state sync. When I add ApplicationRecord.sync = true
I got EventBus.dispatch is not a function
It seems that we should call EventBus.default.dispatch
to make it work
I tried with v0.10.8 and 0.10.9
I was wondering how to implement fetching the currently active user.
Normally, I would just call /api/v1/user
. How can I achieve this behaviour with spraypaint?
Besides, a related question is concerning actions (which are indicated in the route). For example I would like a post by performing a get request to /api/v1/posts/1/like
. Is there a possibility to add something to the route in a find request?
Thanks in advance!
How can use this library to bulk create?
There is nested updating, but I didn't see anything about bulk create (with nested attributes).
A declarative, efficient, and flexible JavaScript library for building user interfaces.
🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. 📊📈🎉
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google ❤️ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.