return model instance without tuple when case statement is used in SQLAlchemy + FastAPI - sqlalchemy

Let's say I have an API, by calling it we get a list of posts, for each post I want to send a value whether that post is editable or not by the logged-in user in the response. so for that I'm using Case statement from SQLAlchemy and based on the logged-in user ID I'm returning true or false
The code looks like below
is_editable_expr = case(
[
(Post.user_id == current_user.id, True),
],
else_=False,
).label("is_editable")
data = db_session.query(Post, is_editable_expr).order_by(Post.created_at.desc()).join(User).all()
I'm using FastAPI and when it tries to serialize the data it fails because the value returned by this is ORM looks like
[(<Post title=Todo title description=A short description about your todo>, False), ...]
here the post model instance is inside the tuple and is_editable is directly accessible. the Post pydantic model looks like this
class Post(BaseModel):
id: int
title: str,
description: str,
user: User
is_active: bool
is_editable: bool
class Config:
orm_mode = True
since the orm instance itself is inside tuple while serializing it's failing and cannot access title/descriptions etc. I want my response to be a list of Post and it should look like this
[
{
"title":"title name",
"description":"some long description",
"is_editable":true
},
...
]
Can anyone please advice or suggest how can I make it work. Thanks in advance.

That is happening because of your query definition: db_session.query(Post, is_editable_expr)
So basically the second item in the tuple is your is_editable expression
I would rather avoid doing it in the database and would do a simple loop to do it on the server:
data = (db_session.query(Post)
.order_by(Post.created_at.desc())
.join(User)
.all())
for post in data:
post.is_editable = post.user_id == current_user.id

Related

Create or update one to many relationship in Prisma

I'm trying to update a one to many relationship in Prisma. My schema looks like this
model A_User {
id Int #id
username String
age Int
bio String #db.VarChar(1000)
createdOn DateTime #default(now())
features A_Features[]
}
model A_Features {
id Int #id #default(autoincrement())
description String
A_User A_User? #relation(fields: [a_UserId], references: [id])
a_UserId Int?
}
I'm trying to add a couple of new features to user with id: 1, or update them if they are already there.
I'm trying doing something like
const post = await prisma.a_User.update({
where: { id: 1},
data: {
features: {
upsert: [
{ description: 'first feature'},
{ description: 'second feature'}
]
}
}
})
The compiler isn't happy, it tells me
Type '{ features: { upsert: { description: string; }[]; }; }' is not assignable to type '(Without<A_UserUpdateInput, A_UserUncheckedUpdateInput> & A_UserUncheckedUpdateInput) | (Without<...> & A_UserUpdateInput)'.
Object literal may only specify known properties, and 'features' does not exist in type '(Without<A_UserUpdateInput, A_UserUncheckedUpdateInput> & A_UserUncheckedUpdateInput) | (Without<...> & A_UserUpdateInput)'.ts(2322)
index.d.ts(1572, 5): The expected type comes from property 'data' which is declared here on type '{ select?: A_UserSelect; include?: A_UserInclude; data: (Without<A_UserUpdateInput, A_UserUncheckedUpdateInput> & A_UserUncheckedUpdateInput) | (Without<...> & A_UserUpdateInput); where: A_UserWhereUniqueInput; }'
(property) features: {
upsert: {
description: string;
}[];
}
I can't work out how to do it nor I can find clear help in the documentation. Any idea on how to implement it or where I can find some examples?
I'm providing my solution based on the clarifications you provided in the comments. First I would make the following changes to your Schema.
Changing the schema
model A_User {
id Int #id
username String
age Int
bio String #db.VarChar(1000)
createdOn DateTime #default(now())
features A_Features[]
}
model A_Features {
id Int #id #default(autoincrement())
description String #unique
users A_User[]
}
Notably, the relationship between A_User and A_Features is now many-to-many. So a single A_Features record can be connected to many A_User records (as well as the opposite).
Additionally, A_Features.description is now unique, so it's possible to uniquely search for a certain feature using just it's description.
You can read the Prisma Guide on Relations to learn more about many-to-many relations.
Writing the update query
Again, based on the clarification you provided in the comments, the update operation will do the following:
Overwrite existing features in a A_User record. So any previous features will be disconnected and replaced with the newly provided ones. Note that the previous features will not be deleted from A_Features table, but they will simply be disconnected from the A_User.features relation.
Create the newly provided features that do not yet exist in the A_Features table, and Connect the provided features that already exist in the A_Features table.
You can perform this operation using two separate update queries. The first update will Disconnect all previously connected features for the provided A_User. The second query will Connect or Create the newly provided features in the A_Features table. Finally, you can use the transactions API to ensure that both operations happen in order and together. The transactions API will ensure that if there is an error in any one of the two updates, then both will fail and be rolled back by the database.
//inside async function
const disconnectPreviouslyConnectedFeatures = prisma.a_User.update({
where: {id: 1},
data: {
features: {
set: [] // disconnecting all previous features
}
}
})
const connectOrCreateNewFeatures = prisma.a_User.update({
where: {id: 1},
data: {
features: {
// connect or create the new features
connectOrCreate: [
{
where: {
description: "'first feature'"
}, create: {
description: "'first feature'"
}
},
{
where: {
description: "second feature"
}, create: {
description: "second feature"
}
}
]
}
}
})
// transaction to ensure either BOTH operations happen or NONE of them happen.
await prisma.$transaction([disconnectPreviouslyConnectedFeatures, connectOrCreateNewFeatures ])
If you want a better idea of how connect, disconnect and connectOrCreate works, read the Nested Writes section of the Prisma Relation queries article in the docs.
The TypeScript definitions of prisma.a_User.update can tell you exactly what options it takes. That will tell you why the 'features' does not exist in type error is occurring. I imagine the object you're passing to data takes a different set of options than you are specifying; if you can inspect the TypeScript types, Prisma will tell you exactly what options are available.
If you're trying to add new features, and update specific ones, you would need to specify how Prisma can find an old feature (if it exists) to update that one. Upsert won't work in the way that you're currently using it; you need to provide some kind of identifier to the upsert call in order to figure out if the feature you're adding already exists.
https://www.prisma.io/docs/reference/api-reference/prisma-client-reference/#upsert
You need at least create (what data to pass if the feature does NOT exist), update (what data to pass if the feature DOES exist), and where (how Prisma can find the feature that you want to update or create.)
You also need to call upsert multiple times; one for each feature you're looking to update or create. You can batch the calls together with Promise.all in that case.
const upsertFeature1Promise = prisma.a_User.update({
data: {
// upsert call goes here, with "create", "update", and "where"
}
});
const upsertFeature2Promise = prisma.a_User.update({
data: {
// upsert call goes here, with "create", "update", and "where"
}
});
const [results1, results2] = await Promise.all([
upsertFeaturePromise1,
upsertFeaturePromise2
]);

How to modify a many-to-many collection using django rest framework

I am trying to create an endpoint where, having a User entity, I can add / remove existing Group entities to user.groups many-to-many field. But when I try to do it, django-rest-framework tries to create new group objects instead of finding existing ones.
I have defined two serializers where UserSerializer has a nested GroupSerializer:
class GroupSerializer(serializers.ModelSerializer):
class Meta:
model = Group
fields = ['id', 'name']
class UserSerializer(serializers.ModelSerializer):
class Meta:
model = User
fields = ['id', 'username', 'email', 'groups']
groups = GroupSerializer(many=True)
def update(self, instance, validated_data):
data = validated_data.copy()
groups = data.pop('groups', [])
for key, val in data.items():
setattr(instance, key, val)
instance.groups.clear()
for group in groups:
instance.groups.add(group)
return instance
def create(self, validated_data):
data = validated_data.copy()
groups = data.pop('groups', [])
instance = self.Meta.model.objects.create(**data)
for group in groups:
instance.groups.add(group)
return instance
When I send a JSON through a PUT REST call (from django-rest-framework web interface):
{
"id": 6,
"username": "user5#example.com",
"email": "user5#example.com",
"groups": [
{
"id": 1,
"name": "AAA"
}
]
}
I expect serializer to find the Group with given id and add it to User. But instead, it tries to create a new user group and fails with duplicate key error:
{
"groups": [
{
"name": [
"group with this name already exists."
]
}
]
}
I searched over the internet and debugged myself and found no solution to this use case.
The create and update methods inside UserSerializerclass are never reached.
Edit: as asked, here are my views and urls:
class UserViewSet(viewsets.ModelViewSet):
queryset = User.objects.all().order_by('-date_joined')
serializer_class = UserSerializer
class GroupViewSet(viewsets.ModelViewSet):
queryset = Group.objects.all()
serializer_class = GroupSerializer
Urls:
router = routers.DefaultRouter()
router.register(r'users', views.UserViewSet)
router.register(r'groups', views.GroupViewSet)
urlpatterns = [
path('', include(router.urls)),
path('api-auth/', include('rest_framework.urls', namespace='rest_framework'))
]
This seems to be the validation error due to a nested serializer model that contains unique constraint, see this post. According to the article, DRF did not handle this condition since it's hard to realize if the serializer is a nested serializer within another one. And that's why the create() and update() never been reached since the validation is done before calling them.
The way to work around this is to remove the uniqueness validator manually in GroupSerializer as follow:
class GroupSerializer(serializers.ModelSerializer):
class Meta:
model = Group
fields = ['id', 'name']
extra_kwargs = {
'name': {'validators': []},
}
BTW, there are some points that can be improved or should be corrected in your update() and create() code. Firstly, you didn't do instance.save() so the instance won't be update after the whole process done. Second, the groups are just a list of dictionary, and you should not add object that way. The following are the modification based on your OP code:
def update(self, instance, validated_data):
data = validated_data.copy()
groups = data.pop('groups', [])
for key, val in data.items():
setattr(instance, key, val)
instance.save() # This will indeed update DB values
group_ids = [g['id'] for g in groups]
instance.groups.clear()
instance.groups.add(*group_ids) # Add all groups once. Also you can replace these two lines with
# instance.groups.set(group_ids)
return instance
Though am late, here is how I did it, adding to Tsang-Yi Shen answer. However this worked for me because I was using django-role-permissions https://django-role-permissions.readthedocs.io/en/stable/setup.html
For the group serializer am only interested in the name.
from rolepermissions.roles import assign_role
class GroupModelSerializer(serializers.ModelSerializer):
name = serializers.SerializerMethodField()
class Meta:
model = Group
fields = ['name']
extra_kwargs = {
'name': {'validators': []},
}
def get_name(self, obj):
"""
This method modifies the way the name field is returned in a get request.
"""
return [group.name for group in obj.objects.all()]
In the UserSerializer, I modify the groups field to be returned as a list of groups a user belongs to, and received as a list instead of an OrderedDict in the JSON payload for creating a User, which can look something like this:
{
"email": "testuser#example.com",
"username": "testuser",
"name": "Test User",
"password": "password123",
"groups": ["doctor", "nurse", ]
}
class UserSerializer(serializers.ModelSerializer):
password = serializers.CharField(write_only=True)
groups = ListField(required=False, default=[], write_only=True)
user_groups = serializers.SerializerMethodField(read_only=True)
class Meta:
model = User
fields = ["id", "email", "username", "name", "password", "groups", "user_groups"]
depth = 2
def get_user_groups(self, obj):
"""
This method modifies the way the `groups` field is returned in a get request.
"""
return [group.name for group in obj.groups.all()]
def create(self, validated_data):
password = validated_data.pop('password')
gropus_to_add_user = validated_data.pop("groups")
user = User(**validated_data)
user.set_password(password)
user.save()
for group_name in groups_to_add_user:
# The assign_role function adds the user to a group.
# In this case group and roles may mean the same thing.
assign_role(user, group_name)
return user
You will have something like this when creating a user, here am using Swagger:

yii2 with dhtmlx scheduler and select populated from the server

I am trying to populate the project select dropdown with data from the server.
I am using yii2.
My controller data action:
public function actionData()
{
$list = new OptionsConnector(null, "PHPYii");
$list->render_table("project", "id", "id(value),name(label)");
$connector = new JSONSchedulerConnector(null, "PHPYii");
$connector->set_options("project", $list);
$connector->configure(
new Booking(), "id", "start, end, activity, user, subsubproject, status, comment"
);
$connector->render();
}
I get an error message:
Exception 'Error' with message 'Call to a member function find() on
string'
And I think this line is the cause: $connector->set_options("project", $list);
What should I change?
UPDATE:
So I am here now:
public function actionData() {
$list = new JSONOptionsConnector(null, "PHPYii");
$list->enable_log("text1.log");
$list->configure(
new Subsubproject(),
"-","id, name"
);
$list->render();
$connector = new JSONSchedulerConnector(null, "PHPYii");
$connector->enable_log("text2.log");
$connector->set_options("subsubprojects", $list);
$connector->configure(
new Booking(),
"id", "start, end, activity, user, subsubproject, status,comment"
);
$connector->render();
}
and I get this:
0: Object { key: undefined, id: 1, name: "Thing1", … }
​1: Object { key: undefined, id: 2, name: "Thing2", … }
​2: Object { key: undefined, id: 3, name: "Thing3", … }
I don't have keys... How can I get some? :)
1) You don't need to call the render method of JSONOptionsConnector directly. Calling it ends processing of the request if I'm not mistaken, so the SchedulerConnector takes no effect
Try commenting out $list->render(); line.
2) The response format seems a bit off. This may be a bug of PHPYii wrapper of dhtmlx connector, I'm not sure
According to source codes the client-side needs value and label properties from options, and while handler returns id and name.
You can try something following:
public function actionData() {
$list = new JSONOptionsConnector(null, "PHPYii");
$list->enable_log("text1.log");
$list->configure(
new Subsubproject(),
"id","id(value), name(label)"
// or
// "id(value)","id(value), name(label)"
);
$connector->enable_log("text2.log");
$connector->set_options("subsubprojects", $list);
$connector->configure(
new Booking(),
"id", "start, end, activity, user, subsubproject, status,comment"
);
$connector->render();
}
This should produce a json response containing a list of booking and subprojects.
However, I can't test this code so something may still be wrong.
You can try it and see whether the result JSON looks right.
If it doesn't get you any closer, I honestly would produce json manually rather than using a connector with PHPYii wrapper. That way you'll have direct control over what is returned from your controller and won't have another black box there.
You'll need to return a json of the following structure from your action:
https://docs.dhtmlx.com/scheduler/data_formats.html#jsonwithcollections
so you'll have something like this in your action:
return $this->asJson([
"data"=> $preparedEventsArray
"collections" => [
"subprojects"=> $preparedSubprojects
]
]);
where $preparedEventsArray is an array of event objects as shown in docs, and $subprojects - your value/label objects
Note that names of properties in the data collection - "id","start_date","end_date","text" - are mandatory, you'll have to map your data model to this structure,
e.g.
start -> start_date
end -> end_date
activity -> text
all other properties can have their names unchanged.
The official docs don't have a sample code for Yii2, unfortunately.
There are common docs for server formats
https://docs.dhtmlx.com/scheduler/server_integration.html
And tutorials for PHP Slim framework and Laravel, which is not exactly what you need, but the closest thing the current documentation has.

Ember Data findAll() not populating models?

new to ember js, and working on an app using ember-data. If I test with same data using FixtureAdapter, everything populates in the html template ok. When I switch to RESTAdapter, the data looks like it's coming back ok, but the models are not being populated in the template? Any ideas? Here's the code:
App.Store = DS.Store.extend({
revision:12,
//adapter: 'DS.FixtureAdapter'
adapter: DS.RESTAdapter.extend({
url:'http://bbx-dev.footballamerica.com/builderapirequest/bat'
})
});
App.Brand = DS.Model.extend({
name: DS.attr('string'),
numStyles: DS.attr('string'),
vendorId: DS.attr('string')
});
App.BrandsRoute = Ember.Route.extend({
setupController:function(controller){
},
model:function(){
return App.Brand.find();
}
});
And here is the data coming back, but not being inserted into the template!
returnValue: [{numStyles:1, name:Easton, vendorId:6043}, {numStyles:1, name:Louisville Slugger, vendorId:6075},…]
0: {numStyles:1, name:Easton, vendorId:6043}
1: {numStyles:1, name:Louisville Slugger, vendorId:6075}
2: {numStyles:1, name:Rawlings, vendorId:6109}
3: {numStyles:7, name:BWP Bats , vendorId:6496}
4: {numStyles:1, name:DeMarini, vendorId:W002}
status: "ok"
And here is the template:
{{#each brand in model.returnValue }}
<div class="brand-node"{{action select brand}}>
<h2>{{brand.name}}</h2>
<p>{{brand.numStyles}} Styles</p>
</div>
{{/each}}
Any help would be greatly appreciated! I'm not getting any errors, and the data seems to be coming back ok, just not getting into the template. Not sure if the returned dataset needs "id" param?
I am also using the Store congfig to alter the find() from plural to singular:
DS.RESTAdapter.configure("plurals", {
brand: "brand"
});
The way the API was written, its expecting "brand" and not "brands"... maybe its something to do with this??
Thanks in advance.
You have stated:
Not sure if the returned dataset needs "id" param?
Yes you are guessing right, you data coming back from the backend need's an id field set. And if the id field name is different then id you should also define this in ember like so:
App.Store = DS.Store.extend({
revision:12,
//adapter: 'DS.FixtureAdapter'
adapter: DS.RESTAdapter.extend({
url:'http://bbx-dev.footballamerica.com/builderapirequest/bat'
}),
serializer: DS.RESTSerializer.extend({
primaryKey: function (type) {
return '_my_super_custom_ID'; // Only needed if your id field name is different than 'id'
}
})
});
I suppose your Fixtures have an id defined thus it works, right?
Note: you don't need to define the id field at all explicitly, ember add's automatically the id field to a model, so your model is correct.
Here a website that is still a WIP but can be good reference for this conventions
and as stated there:
The document MUST contain an id key.
And this is how your JSON should look like for a collection of records:
{
"brands": [{
"id": "1",
"numStyles": "1",
"name": "Easton",
"vendorId" :"6043"
}, {
"id": "2",
"numStyles": "4",
"name": "Siemens",
"vendorId": "6123"
}/** and so on**/]
}
Note: as you have shown you JSON root is called returnValue this should be called brand or brands if you are not adapting the plurals. See here for reference for the JSON root I'm talking about.
Hope it helps

Neo4j::Rails::Model to_json - node id is missing

I have a Jruby on Rails application with Neo4j.rb and a model, let's say Auth, defined like this:
class Auth < Neo4j::Rails::Model
property :uid, :type => String, :index => :exact
property :provider, :type => String, :index => :exact
property :email, :type => String, :index => :exact
end
And this code:
a = Auth.find :uid => 324, :provider => 'twitter'
# a now represents a node
a.to_json
# outputs: {"auth":{"uid": "324", "provider": "twitter", "email": "email#example.com"}}
Notice that the ID of the node is missing from the JSON representation. I have a RESTful API within my application and I need the id to perform DELETE and UPDATE actions.
I tried this to see if it works:
a.to_json :only => [:id]
But it returns an empty JSON {}.
Is there any way I can get the ID of the node in the JSON representation without rewriting the whole to_json method?
Update The same problems applies also to the to_xml method.
Thank you!
I am answering my own question. I still think that there is a better way to do this, but, for now, I am using the following hack:
In /config/initializers/neo4j_json_hack.rb I put the following code:
class Neo4j::Rails::Model
def as_json(options={})
repr = super options
repr.merge! '_nodeId' => self.id if self.persisted?
end
end
And now every JSON representations of my persisted Neo4j::Rails::Model objects have a _nodeId parameter.
The ID is typically not included because it shouldn't be exposed outside the Neo4j database. Neo4j doesn't guarantee that the ID will be identical from instance to instance, and it wouldn't surprise me if the ID changed in a distributed, enterprise installation of Neo4j.
You should create your own ID (GUID?), save it as a property on the node, index it, and use that to reference your nodes. Don't expose the Neo4j ID to your users or application, and definitely don't rely on it beyond a single request (e.g. don't save a reference to it in another database or use it to test for equality).