def list_collection_join(self, path, keys): """ Called from API to obtain list of collection items details. """ # Verify that collection parent exists. self.model.path_row(path[:-1], keys) # Find endpoint for the collection itself. endpoint = schema.resolve_path(path) if endpoint.parent.table is None: # Top-level collections do not have any parents. rows = self.model[endpoint.table.name].list() else: # Filter using the endpoint filter and parent relationship. pname = endpoint.parent.table.name filter = dict(endpoint.filter) filter.update({pname: keys[pname]}) rows = self.model[endpoint.table.name].list(**filter) join = schema.tables[endpoint.table.name].join if not join: return {} else: items = {} # Return rows keyed by primary key of joined table. to_expand = path[-1] if to_expand in join: for key in join: if key not in keys: for row in rows: items[row.get(key)] = self.model[to_expand][row.get(key)].to_dict() # Add data of the joining object items[row.get(key)]['joined'] = {row.pkey: row.to_dict()} return items
def list_collection(self, path, keys): """ Called from API to obtain list of collection items. """ # Verify that collection parent exists. self.model.path_row(path[:-1], keys) # Find endpoint for the collection itself. endpoint = schema.resolve_path(path) if endpoint.parent.table is None: # Top-level collections do not have any parents. # Filter using the endpoint filter filter = dict(endpoint.filter) rows = self.model[endpoint.table.name].list(**filter) else: # Filter using the endpoint filter and parent relationship. pname = endpoint.parent.table.name filter = dict(endpoint.filter) filter.update({pname: keys[pname]}) rows = self.model[endpoint.table.name].list(**filter) if isinstance(schema.tables[endpoint.table.name].pkey, basestring): # Return rows keyed by the primary key. return {row.pkey: row.to_dict() for row in rows} else: # Return rows keyed by the missing portion of composite # primary key. for key in schema.tables[endpoint.table.name].pkey: if key not in keys: return {row.get(key): row.to_dict() for row in rows}
def make_handlers(path): endpoint = schema.resolve_path(path) def common_patch(creds, prefix): """ PATCH handler for both collection and entity endpoints. """ # Read patch from the client. patch = loads(flask.request.data) # And make sure it really is a patch and not something # totally weird that would blow up later. validate_json_patch(patch) # Alas, we need to poke into the patch a bit before we allow # it to execute. Namely, we need to rebase paths and consult # access control rules. for op in patch: if 'path' in op: # Determine whether this operation is a mutation. # We have different access rules for reading and writing. write = ('test' != op['op']) # We default to empty dictionary when no value is given. # This is however just for access control checks. value = op.get('value', {}) # Adjust the path to include endpoint prefix. op['path'] = prefix + normalize_path(op['path']) # Validate the value at the path. validate_dbdict_fragment(creds, value, op['path'], write) if 'from' in op: # Adjust source path the same way as above. op['from'] = prefix + normalize_path(op['from']) # And verify that we can access the source data. # XXX: This could be very broken as we should validate # access to all child entities and not just the root. validate_dbdict_fragment(creds, {}, op['from'], False) # Remove None keys from values of non-merge operations. if 'value' in op and 'x-merge' != op['op']: op['value'] = remove_nulls(op['value']) # Run the patch and hope for the best? return {'uuids': apply_patch(patch)} @app.require_credentials(manager) @convert_errors def collection_handler(credentials={}, **keys): jpath = endpoint.to_jpath(keys)[:-2] if 'GET' == flask.request.method: # Make sure all access control restrictions are applied. validate_dbdict_fragment(credentials, {}, jpath, False) try: # Let the manager deal with model access and data # retrieval. XXX: Access control is broken there, BTW. data = call_sync(manager.list_collection, path, keys) except KeyError: raise PathError('not found', jpath) # We promised not to return any nulls in the output. return remove_nulls(data) if 'POST' == flask.request.method: # Read data from client. data = loads(flask.request.data) # Make sure it's at least a litle sane. if not isinstance(data, Mapping): raise DataError('invalid entity', jpath) # Desired state must be present when POSTing. if 'desired' not in data: raise DataError('desired state missing', jpath + ['desired']) # When primary keys must be specified by the user, make sure # that they are present and do not invent them on our own. if endpoint.table.user_pkey: djpath = jpath + ['desired'] if isinstance(endpoint.table.pkey, basestring): if endpoint.table.pkey not in data['desired']: raise DataError('primary key missing', djpath + [endpoint.table.pkey]) else: for key in endpoint.table.pkey: if key not in data['desired']: raise DataError('primary key missing', djpath + [key]) if 'desired' in data: # Extract the primary key placeholder. pkey = data['desired'].get(endpoint.table.pkey, 'POST') else: # Or default to one named 'POST'. pkey = 'POST' # Get rid of keys with None values. data = remove_nulls(data) # Calculate hypothetical destination path. post_path = jpath + [pkey] # Make sure all access control restrictions are applied. validate_dbdict_fragment(credentials, data, post_path, True) # This is what the patch should look like: patch = [{'op': 'add', 'path': post_path, 'value': data}] # Apply the patch and return resulting set of UUIDs. return {'uuids': apply_patch(patch)} if 'PATCH' == flask.request.method: return common_patch(credentials, jpath) @app.require_credentials(manager) @convert_errors def entity_handler(credentials={}, **keys): jpath = endpoint.to_jpath(keys)[:-1] if 'GET' == flask.request.method: # Make sure that we can access the entity. validate_dbdict_fragment(credentials, {}, jpath, False) try: # Get data from manager who can access the model. data = call_sync(manager.get_entity, path, keys) except KeyError: raise PathError('not found', jpath) # Strip keys with None values. return remove_nulls(data) if 'DELETE' == flask.request.method: # Make sure that we can write to that entity. validate_dbdict_fragment(credentials, {}, jpath, True) # Construct patch that would remove it. patch = [{'op': 'remove', 'path': jpath}] # Execute as usual. return {'uuids': apply_patch(patch)} if 'PATCH' == flask.request.method: return common_patch(credentials, jpath) # Rename handlers to satisfy Flask. collection_handler.__name__ = 'c_' + '_'.join(path) entity_handler.__name__ = 'e_' + '_'.join(path) if path[-1] not in endpoint.table.join: join_handler = None else: @app.require_credentials(manager) @convert_errors def join_handler(credentials={}, **keys): jpath = endpoint.to_jpath(keys)[:-2] if 'GET' == flask.request.method: # Make sure all access control restrictions are applied. validate_dbdict_fragment(credentials, {}, jpath, False) try: # Let the manager deal with model access and data # retrieval. XXX: Access control is broken there, BTW. data = call_sync(manager.list_collection_join, path, keys) except KeyError: raise PathError('not found', jpath) # We promised not to return any nulls in the output. return remove_nulls(data) join_handler.__name__ = 'j_' + '_'.join(path) return collection_handler, entity_handler, join_handler