-
Notifications
You must be signed in to change notification settings - Fork 5
/
LOBackendObjectStore.j
273 lines (237 loc) · 10.8 KB
/
LOBackendObjectStore.j
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
/*
* LOBackendObjectStore.j
*
* Created by Martin Carlberg on Januaray 31, 2016.
* Copyright 2016, All rights reserved.
*/
@import "LOAdvanceJSONObjectStore.j"
/*!
This object store should be used if the you want to connect to the node Backend
written in Objective-J
*/
@implementation LOBackendObjectStore : LOAdvanceJSONObjectStore {
CPString baseURL @accessors;
CPArray blocksToRunWhenModelIsReceived;
}
- (id)init {
self = [super init];
if (self) {
if (window && window.location) {
var host = window.location.host;
var protocol = window.location.protocol;
if (host != nil && protocol != nil) {
baseURL = protocol + @"//" + host + "/backend";
}
}
}
return self;
}
- (void)awakeFromCib {
[super awakeFromCib];
if (!model) {
[CPManagedObjectModel modelWithContentsOfURL:baseURL + @"/retrievemodel" completionHandler:function(receivedModel) {
if (model) {
console.error("Model is already set on object store when receiving model from backend.");
} else {
model = receivedModel;
if (blocksToRunWhenModelIsReceived) {
[blocksToRunWhenModelIsReceived enumerateObjectsUsingBlock:function(aBlock) {
aBlock();
}];
blocksToRunWhenModelIsReceived = nil;
}
}
}];
}
}
- (void)retrieveModelWithCompletionHandler:(Function/*(CPManagedObjectModel)*/)completionBlock {
[CPManagedObjectModel modelWithContentsOfURL:baseURL + @"/retrievemodel" completionHandler:function(receivedModel) {
model = receivedModel;
if (completionBlock) completionBlock(receivedModel);
}];
}
// This is used to get things to run after the model is loaded
- (void)addBlockToRunWhenModelIsReceived:(Function)aBlock {
if (!blocksToRunWhenModelIsReceived) {
blocksToRunWhenModelIsReceived = [];
}
[blocksToRunWhenModelIsReceived addObject:aBlock];
}
- (CPString)urlForSaveChangesWithData:(id)data {
if (!baseURL) throw new Error(_cmd + @" Has no baseURL to use");
return baseURL + @"/modify";
}
- (CPURLRequest)urlForRequestObjectsWithFetchSpecification:(LOFetchSpecification)fetchSpecification {
if (!baseURL) throw new Error(_cmd + @" Has no baseURL to use");
var resolvedEntityName = [fetchSpecification alias] || [fetchSpecification entityName];
var url = baseURL;
if ([fetchSpecification method]) {
url = url + @"/" + [fetchSpecification method];
} else {
url = url + @"/fetch";
}
url += @"/" + resolvedEntityName;
if ([fetchSpecification operator]) {
url = url + @"/" + [fetchSpecification operator];
}
var advancedQualifierString = nil;
var qualifier = [fetchSpecification qualifier];
if (qualifier) {
var qualifierString = [self buildRequestPathForQualifier:qualifier];
if (qualifierString) {
url = url + @"/" + qualifierString;
} else {
qualifierString = [LOAdvanceJSONObjectStore UTF16ToUTF8:JSON.stringify([qualifier LOJSONFormat])];
advancedQualifierString = [[CPData dataWithRawString:qualifierString] base64];
url = url + @"/X-LO-Advanced-Qualifier=" + md5lib.md5(qualifierString);
}
}
var request = [CPURLRequest requestWithURL:url];
[request setHTTPMethod:@"GET"];
if (advancedQualifierString) {
[request setValue:advancedQualifierString forHTTPHeaderField:@"X-LO-Advanced-Qualifier"];
}
return request;
}
- (CPString)buildRequestPathForQualifier:(CPPredicate)aQualifier {
if (!aQualifier) return nil;
var qualiferAndItems = [aQualifier];
if ([aQualifier isKindOfClass:[CPCompoundPredicate class]]) {
if ([aQualifier compoundPredicateType] != CPAndPredicateType) return nil;
qualiferAndItems = [aQualifier subpredicates];
}
var qualiferAndItemSize = [qualiferAndItems count];
for (var i = 0; i < qualiferAndItemSize; i++) {
var eachQualifier = [qualiferAndItems objectAtIndex:i];
if (![eachQualifier isKindOfClass:[CPComparisonPredicate class]]) return nil;
if ([eachQualifier predicateOperatorType] != CPEqualToPredicateOperatorType) return nil;
if ([[eachQualifier leftExpression] expressionType] != CPKeyPathExpressionType) return nil;
if ([[eachQualifier rightExpression] expressionType] != CPConstantValueExpressionType) return nil;
if (([[eachQualifier rightExpression] expressionType] === CPConstantValueExpressionType) &&
(![[eachQualifier rightExpression] constantValue])) return nil;
if (([[eachQualifier rightExpression] expressionType] === CPConstantValueExpressionType) &&
((![[[eachQualifier rightExpression] constantValue] isKindOfClass:[CPString class]]) &&
(![[[eachQualifier rightExpression] constantValue] isKindOfClass:[CPNumber class]]))
) return nil;
}
// We've now ensured that each predicate is a simple 'keyPath equals constant value' predicate
var parts = [];
for (var i = 0; i < qualiferAndItemSize; i++) {
var eachQualifier = [qualiferAndItems objectAtIndex:i];
var left = [[eachQualifier leftExpression] description];
var right = [[[eachQualifier rightExpression] constantValue] description];
// todo: percent encode whitespace
[parts addObject:[self escapeStringForQualifier:left] + @"=" + [self escapeStringForQualifier:right]];
}
if ([parts count] == 0) return nil;
return parts.join(@"/");
}
- (CPString)escapeStringForQualifier:(CPString)aString {
var result = [aString stringByReplacingOccurrencesOfString:@"=" withString:@"%3D"];
result = [result stringByReplacingOccurrencesOfString:@"/" withString:@"%2F"];
return result;
}
/*!
* Returns the type for the raw row.
*/
- (CPString)typeForRawRow:(id)row objectContext:(LOObjectContext)objectContext fetchSpecification:(LOFetchSpecification)fetchSpecification {
var type = row._type;
// If we the row does not have the type use the entityName
return type != nil ? type : fetchSpecification.entityName;
}
/*!
* Returns the primary key attribute for the raw row.
TODO: Move this up to super class and get information from model. Maybe this method can be removed?
*/
- (CPString)primaryKeyAttributeForType:(CPString)aType objectContext:(LOObjectContext)objectContext {
return @"primaryKey";
}
/*!
* Returns true if the attribute is a foreign key for the raw row.
TODO: Move this up to super class and get information from model. Maybe this method can be removed?
*/
- (BOOL)isForeignKeyAttribute:(CPString)attribute forType:(CPString)aType objectContext:(LOObjectContext)objectContext {
return [attribute hasSuffix:@"ForeignKey"];
}
/*!
* Returns to one relationship attribute that correspond to the foreign key attribute for the raw row
TODO: Move this up to super class and get information from model. Maybe this method can be removed?
*/
- (CPString)toOneRelationshipAttributeForForeignKeyAttribute:(CPString)attribute forType:(CPString)aType objectContext:(LOObjectContext)objectContext {
return [attribute substringToIndex:[attribute length] - 10]; // Remove "ForeignKey" at end of attribute
}
/*!
* Returns foreign key attribute that correspond to the to one relationship attribute for the type
TODO: Move this up to super class and get information from model. Maybe this method can be removed?
*/
- (CPString)foreignKeyAttributeForToOneRelationshipAttribute:(CPString)attribute forType:(CPString)aType objectContext:(LOObjectContext)objectContext {
return attribute + @"ForeignKey";
}
/*!
* Returns the primary key for the raw row with type aType.
TODO: Move this up to super class and get information from model. Maybe this method can be removed?
*/
- (CPString)primaryKeyForRawRow:(id)row forType:(CPString)aType objectContext:(LOObjectContext)objectContext {
var primaryKeyAttribute = [self primaryKeyAttributeForType:aType objectContext:objectContext];
return row[primaryKeyAttribute];
}
/*!
* Returns LOError for data if the backend has returned a error.
*/
- (id)dataForResponse:(CPHTTPURLResponse)response andData:(CPString)data fromURL:(CPString)urlString connection:(CPURLConnection)connection error:(LOErrorRef)error {
var statusCode = [response statusCode];
if (statusCode === 200) return [data length] > 0 ? [data objectFromJSON] : data;
if (statusCode === 537) { // Error from backend. Should maybe include more codes?
var jSON = [data objectFromJSON];
if (jSON.error) {
if (jSON.error.code && jSON.error.domain) {
@deref(error) = [LOError errorWithDomain:jSON.error.domain code:jSON.error.code userInfo:nil];
} else {
@deref(error) = [LOError errorWithDomain:nil code:0 userInfo:[CPDictionary dictionaryWithObject:jSON.error forKey:@"errorText"]];
}
} else if (jSON.exception) {
@deref(error) = [LOError errorWithDomain:jSON.exception.domain code:jSON.exception.code userInfo:[CPDictionary dictionaryWithJSObject:jSON.exception]];
}
} else {
@deref(error) = [LOError errorWithDomain:@"org.carlberg.LOF" code:statusCode userInfo:[CPDictionary dictionaryWithObject:urlString forKey:@"url"]];
}
return nil;
}
- (CPString)typeOfObject:(id)theObject {
return theObject._loObjectType;
}
- (void)setType:(CPString)aType onObject:(id)theObject {
theObject._loObjectType = aType;
}
// TODO: Move this up to super class where we use the model.
- (id)newObjectForType:(CPString)aType objectContext:(LOObjectContext)objectContext {
var entity = [self entityForName:aType];
if (entity) {
var className = [entity externalName];
var aClass = objj_getClass(className);
if (aClass) {
var obj = [[aClass alloc] init];
[self setType:aType onObject:obj];
return obj;
} else
CPLog.error(@"[" + [self className] + @" " + _cmd + @"]: Class '" + className + "' can't be found for entity named '" + aType + "'");
} else {
CPLog.error(@"[" + [self className] + @" " + _cmd + @"]: Entity can't be found for entity named '" + aType + "'");
}
return nil;
}
/*!
* Returns the primary key value for an object.
TODO: Move this up to super class and get information from model. Maybe this method can be removed?
*/
- (CPString)primaryKeyForObject:(id)theObject {
return [theObject valueForKey:@"primaryKey"];
}
/*!
* Sets the primary key value for an object.
TODO: Move this up to super class and get information from model. Maybe this method can be removed?
*/
- (void)setPrimaryKey:(CPString)thePrimaryKey forObject:(id)theObject {
[theObject setValue:thePrimaryKey forKey:@"primaryKey"];
}
@end