diff --git a/data/chinook-spec.yaml b/data/chinook-spec.yaml index 0afe7b8..0377593 100644 --- a/data/chinook-spec.yaml +++ b/data/chinook-spec.yaml @@ -1,6 +1,169 @@ +components: + schemas: + Album: &id001 + properties: + AlbumId: + type: integer + ArtistId: + type: integer + Title: + type: string + type: object + Artist: &id002 + properties: + ArtistId: + type: integer + Name: + type: string + type: object + Customer: &id003 + properties: + Address: + type: string + City: + type: string + Company: + type: string + Country: + type: string + CustomerId: + type: integer + Email: + type: string + Fax: + type: string + FirstName: + type: string + LastName: + type: string + Phone: + type: string + PostalCode: + type: string + State: + type: string + SupportRepId: + type: integer + type: object + Employee: &id004 + properties: + Address: + type: string + BirthDate: + type: string + City: + type: string + Country: + type: string + Email: + type: string + EmployeeId: + type: integer + Fax: + type: string + FirstName: + type: string + HireDate: + type: string + LastName: + type: string + Phone: + type: string + PostalCode: + type: string + ReportsTo: + type: integer + State: + type: string + Title: + type: string + type: object + Genre: &id005 + properties: + GenreId: + type: integer + Name: + type: string + type: object + Invoice: &id006 + properties: + BillingAddress: + type: string + BillingCity: + type: string + BillingCountry: + type: string + BillingPostalCode: + type: string + BillingState: + type: string + CustomerId: + type: integer + InvoiceDate: + type: string + InvoiceId: + type: integer + Total: + type: string + type: object + InvoiceLine: &id007 + properties: + InvoiceId: + type: integer + InvoiceLineId: + type: integer + Quantity: + type: integer + TrackId: + type: integer + UnitPrice: + type: string + type: object + MediaType: &id008 + properties: + MediaTypeId: + type: integer + Name: + type: string + type: object + Playlist: &id009 + properties: + Name: + type: string + PlaylistId: + type: integer + type: object + PlaylistTrack: &id010 + properties: + PlaylistId: + type: integer + TrackId: + type: integer + type: object + Track: &id011 + properties: + AlbumId: + type: integer + Bytes: + type: integer + Composer: + type: string + GenreId: + type: integer + MediaTypeId: + type: integer + Milliseconds: + type: integer + Name: + type: string + TrackId: + type: integer + UnitPrice: + type: string + type: object info: title: SQLite2REST - version: 1.0.0 + version: 0.4.0 openapi: 3.0.0 paths: /Album: @@ -22,15 +185,34 @@ paths: type: integer responses: '200': + content: + application/json: + schema: + items: *id001 + type: array description: OK summary: Retrieve all records from the Album table post: + requestBody: + content: + application/json: + schema: *id001 responses: '200': + content: + application/json: + schema: *id001 description: OK summary: Create a new record in the Album table /Album/: delete: + parameters: + - description: The ID of the record to delete + in: path + name: id + required: true + schema: + type: integer responses: '200': description: OK @@ -45,11 +227,28 @@ paths: type: integer responses: '200': + content: + application/json: + schema: *id001 description: OK summary: Retrieve all records from the Album table put: + parameters: + - description: The ID of the record to update + in: path + name: id + required: true + schema: + type: integer + requestBody: + content: + application/json: + schema: *id001 responses: '200': + content: + application/json: + schema: *id001 description: OK summary: Update a record in the Album table /Artist: @@ -71,15 +270,34 @@ paths: type: integer responses: '200': + content: + application/json: + schema: + items: *id002 + type: array description: OK summary: Retrieve all records from the Artist table post: + requestBody: + content: + application/json: + schema: *id002 responses: '200': + content: + application/json: + schema: *id002 description: OK summary: Create a new record in the Artist table /Artist/: delete: + parameters: + - description: The ID of the record to delete + in: path + name: id + required: true + schema: + type: integer responses: '200': description: OK @@ -94,11 +312,28 @@ paths: type: integer responses: '200': + content: + application/json: + schema: *id002 description: OK summary: Retrieve all records from the Artist table put: + parameters: + - description: The ID of the record to update + in: path + name: id + required: true + schema: + type: integer + requestBody: + content: + application/json: + schema: *id002 responses: '200': + content: + application/json: + schema: *id002 description: OK summary: Update a record in the Artist table /Customer: @@ -120,15 +355,34 @@ paths: type: integer responses: '200': + content: + application/json: + schema: + items: *id003 + type: array description: OK summary: Retrieve all records from the Customer table post: + requestBody: + content: + application/json: + schema: *id003 responses: '200': + content: + application/json: + schema: *id003 description: OK summary: Create a new record in the Customer table /Customer/: delete: + parameters: + - description: The ID of the record to delete + in: path + name: id + required: true + schema: + type: integer responses: '200': description: OK @@ -143,11 +397,28 @@ paths: type: integer responses: '200': + content: + application/json: + schema: *id003 description: OK summary: Retrieve all records from the Customer table put: + parameters: + - description: The ID of the record to update + in: path + name: id + required: true + schema: + type: integer + requestBody: + content: + application/json: + schema: *id003 responses: '200': + content: + application/json: + schema: *id003 description: OK summary: Update a record in the Customer table /Employee: @@ -169,15 +440,34 @@ paths: type: integer responses: '200': + content: + application/json: + schema: + items: *id004 + type: array description: OK summary: Retrieve all records from the Employee table post: + requestBody: + content: + application/json: + schema: *id004 responses: '200': + content: + application/json: + schema: *id004 description: OK summary: Create a new record in the Employee table /Employee/: delete: + parameters: + - description: The ID of the record to delete + in: path + name: id + required: true + schema: + type: integer responses: '200': description: OK @@ -192,11 +482,28 @@ paths: type: integer responses: '200': + content: + application/json: + schema: *id004 description: OK summary: Retrieve all records from the Employee table put: + parameters: + - description: The ID of the record to update + in: path + name: id + required: true + schema: + type: integer + requestBody: + content: + application/json: + schema: *id004 responses: '200': + content: + application/json: + schema: *id004 description: OK summary: Update a record in the Employee table /Genre: @@ -218,15 +525,34 @@ paths: type: integer responses: '200': + content: + application/json: + schema: + items: *id005 + type: array description: OK summary: Retrieve all records from the Genre table post: + requestBody: + content: + application/json: + schema: *id005 responses: '200': + content: + application/json: + schema: *id005 description: OK summary: Create a new record in the Genre table /Genre/: delete: + parameters: + - description: The ID of the record to delete + in: path + name: id + required: true + schema: + type: integer responses: '200': description: OK @@ -241,11 +567,28 @@ paths: type: integer responses: '200': + content: + application/json: + schema: *id005 description: OK summary: Retrieve all records from the Genre table put: + parameters: + - description: The ID of the record to update + in: path + name: id + required: true + schema: + type: integer + requestBody: + content: + application/json: + schema: *id005 responses: '200': + content: + application/json: + schema: *id005 description: OK summary: Update a record in the Genre table /Invoice: @@ -267,15 +610,34 @@ paths: type: integer responses: '200': + content: + application/json: + schema: + items: *id006 + type: array description: OK summary: Retrieve all records from the Invoice table post: + requestBody: + content: + application/json: + schema: *id006 responses: '200': + content: + application/json: + schema: *id006 description: OK summary: Create a new record in the Invoice table /Invoice/: delete: + parameters: + - description: The ID of the record to delete + in: path + name: id + required: true + schema: + type: integer responses: '200': description: OK @@ -290,11 +652,28 @@ paths: type: integer responses: '200': + content: + application/json: + schema: *id006 description: OK summary: Retrieve all records from the Invoice table put: + parameters: + - description: The ID of the record to update + in: path + name: id + required: true + schema: + type: integer + requestBody: + content: + application/json: + schema: *id006 responses: '200': + content: + application/json: + schema: *id006 description: OK summary: Update a record in the Invoice table /InvoiceLine: @@ -316,15 +695,34 @@ paths: type: integer responses: '200': + content: + application/json: + schema: + items: *id007 + type: array description: OK summary: Retrieve all records from the InvoiceLine table post: + requestBody: + content: + application/json: + schema: *id007 responses: '200': + content: + application/json: + schema: *id007 description: OK summary: Create a new record in the InvoiceLine table /InvoiceLine/: delete: + parameters: + - description: The ID of the record to delete + in: path + name: id + required: true + schema: + type: integer responses: '200': description: OK @@ -339,11 +737,28 @@ paths: type: integer responses: '200': + content: + application/json: + schema: *id007 description: OK summary: Retrieve all records from the InvoiceLine table put: + parameters: + - description: The ID of the record to update + in: path + name: id + required: true + schema: + type: integer + requestBody: + content: + application/json: + schema: *id007 responses: '200': + content: + application/json: + schema: *id007 description: OK summary: Update a record in the InvoiceLine table /MediaType: @@ -365,15 +780,34 @@ paths: type: integer responses: '200': + content: + application/json: + schema: + items: *id008 + type: array description: OK summary: Retrieve all records from the MediaType table post: + requestBody: + content: + application/json: + schema: *id008 responses: '200': + content: + application/json: + schema: *id008 description: OK summary: Create a new record in the MediaType table /MediaType/: delete: + parameters: + - description: The ID of the record to delete + in: path + name: id + required: true + schema: + type: integer responses: '200': description: OK @@ -388,11 +822,28 @@ paths: type: integer responses: '200': + content: + application/json: + schema: *id008 description: OK summary: Retrieve all records from the MediaType table put: + parameters: + - description: The ID of the record to update + in: path + name: id + required: true + schema: + type: integer + requestBody: + content: + application/json: + schema: *id008 responses: '200': + content: + application/json: + schema: *id008 description: OK summary: Update a record in the MediaType table /Playlist: @@ -414,15 +865,34 @@ paths: type: integer responses: '200': + content: + application/json: + schema: + items: *id009 + type: array description: OK summary: Retrieve all records from the Playlist table post: + requestBody: + content: + application/json: + schema: *id009 responses: '200': + content: + application/json: + schema: *id009 description: OK summary: Create a new record in the Playlist table /Playlist/: delete: + parameters: + - description: The ID of the record to delete + in: path + name: id + required: true + schema: + type: integer responses: '200': description: OK @@ -437,11 +907,28 @@ paths: type: integer responses: '200': + content: + application/json: + schema: *id009 description: OK summary: Retrieve all records from the Playlist table put: + parameters: + - description: The ID of the record to update + in: path + name: id + required: true + schema: + type: integer + requestBody: + content: + application/json: + schema: *id009 responses: '200': + content: + application/json: + schema: *id009 description: OK summary: Update a record in the Playlist table /PlaylistTrack: @@ -463,15 +950,34 @@ paths: type: integer responses: '200': + content: + application/json: + schema: + items: *id010 + type: array description: OK summary: Retrieve all records from the PlaylistTrack table post: + requestBody: + content: + application/json: + schema: *id010 responses: '200': + content: + application/json: + schema: *id010 description: OK summary: Create a new record in the PlaylistTrack table /PlaylistTrack/: delete: + parameters: + - description: The ID of the record to delete + in: path + name: id + required: true + schema: + type: integer responses: '200': description: OK @@ -486,11 +992,28 @@ paths: type: integer responses: '200': + content: + application/json: + schema: *id010 description: OK summary: Retrieve all records from the PlaylistTrack table put: + parameters: + - description: The ID of the record to update + in: path + name: id + required: true + schema: + type: integer + requestBody: + content: + application/json: + schema: *id010 responses: '200': + content: + application/json: + schema: *id010 description: OK summary: Update a record in the PlaylistTrack table /Track: @@ -512,15 +1035,34 @@ paths: type: integer responses: '200': + content: + application/json: + schema: + items: *id011 + type: array description: OK summary: Retrieve all records from the Track table post: + requestBody: + content: + application/json: + schema: *id011 responses: '200': + content: + application/json: + schema: *id011 description: OK summary: Create a new record in the Track table /Track/: delete: + parameters: + - description: The ID of the record to delete + in: path + name: id + required: true + schema: + type: integer responses: '200': description: OK @@ -535,11 +1077,28 @@ paths: type: integer responses: '200': + content: + application/json: + schema: *id011 description: OK summary: Retrieve all records from the Track table put: + parameters: + - description: The ID of the record to update + in: path + name: id + required: true + schema: + type: integer + requestBody: + content: + application/json: + schema: *id011 responses: '200': + content: + application/json: + schema: *id011 description: OK summary: Update a record in the Track table diff --git a/sqlite2rest/__init__.py b/sqlite2rest/__init__.py index 97aaf81..623ae26 100644 --- a/sqlite2rest/__init__.py +++ b/sqlite2rest/__init__.py @@ -1,3 +1,3 @@ -__version__ = "0.4.0" +__version__ = "1.5.0" from .app import create_app from .database import Database diff --git a/sqlite2rest/database.py b/sqlite2rest/database.py index 0000956..2e1e040 100644 --- a/sqlite2rest/database.py +++ b/sqlite2rest/database.py @@ -33,6 +33,10 @@ def get_record(self, table_name, key): col_names = [column[0] for column in self.cursor.description] return dict(zip(col_names, row)) + def get_table_schema(self, table_name): + self.cursor.execute(f"PRAGMA table_info({table_name});") + columns = self.cursor.fetchall() + return {column[1]: column[2] for column in columns} # Return a dictionary mapping column names to SQLite types def create_record(self, table_name, data): columns = ', '.join(data.keys()) diff --git a/sqlite2rest/openapi.py b/sqlite2rest/openapi.py index eeb866d..958dea4 100644 --- a/sqlite2rest/openapi.py +++ b/sqlite2rest/openapi.py @@ -37,16 +37,11 @@ def add_paging_parameters(operation_obj): } ] -def add_operation_to_path(path_item, method, rule_str, primary_key_type): +def add_operation_to_path(path_item, method, rule_str, primary_key_type, schema): operation = get_operation_summary(method) table_name = rule_str.split('/')[1] operation_obj = { "summary": f"{operation} the {table_name} table", - "responses": { - "200": { - "description": "OK" - } - } } if method == 'GET': if '' in rule_str: @@ -61,10 +56,70 @@ def add_operation_to_path(path_item, method, rule_str, primary_key_type): } } ] + add_response_to_operation(operation_obj, schema) else: add_paging_parameters(operation_obj) + add_response_to_operation(operation_obj, {"type": "array", "items": schema}) + elif method == 'POST': + operation_obj["requestBody"] = { + "content": { + "application/json": { + "schema": schema + } + } + } + add_response_to_operation(operation_obj, schema) + elif method == 'PUT': + operation_obj["parameters"] = [ + { + "name": "id", + "in": "path", + "description": "The ID of the record to update", + "required": True, + "schema": { + "type": primary_key_type, + } + } + ] + operation_obj["requestBody"] = { + "content": { + "application/json": { + "schema": schema + } + } + } + add_response_to_operation(operation_obj, schema) + elif method == 'DELETE': + operation_obj["parameters"] = [ + { + "name": "id", + "in": "path", + "description": "The ID of the record to delete", + "required": True, + "schema": { + "type": primary_key_type, + } + } + ] + operation_obj["responses"] = { + "200": { + "description": "OK" + } + } path_item[method.lower()] = operation_obj +def add_response_to_operation(operation_obj, schema): + operation_obj["responses"] = { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": schema + } + } + } + } + def sqlite_type_to_openapi_type(sqlite_type): """ Convert SQLite data types to OpenAPI data types. @@ -74,14 +129,16 @@ def sqlite_type_to_openapi_type(sqlite_type): return "integer" elif sqlite_type in ["REAL", "DOUBLE", "DOUBLE PRECISION", "FLOAT"]: return "number" - elif sqlite_type in ["TEXT", "CHARACTER", "VARCHAR", "VARYING CHARACTER", "NCHAR", "NATIVE CHARACTER", "NVARCHAR", "CLOB"]: + elif sqlite_type in ["TEXT", "CHARACTER", "VARCHAR", "VARYING CHARACTER", "NCHAR", "NATIVE CHARACTER", "CLOB"]: + return "string" + elif sqlite_type.startswith("NVARCHAR"): return "string" elif sqlite_type in ["BLOB"]: - return "string", "byte" + return "byte" elif sqlite_type in ["BOOLEAN"]: return "boolean" elif sqlite_type in ["DATE", "DATETIME"]: - return "string", "date-time" + return "string" else: return "string" @@ -93,27 +150,38 @@ def generate_openapi_spec(db): "title": "SQLite2REST", "version": __version__ }, - "paths": {} + "paths": {}, + "components": { + "schemas": {} + } } - # Generate paths for each route - for rule in current_app.url_map.iter_rules(): - # Skip the static routes - if rule.endpoint in ('static', 'openapi'): - continue + # Generate schemas for each table + spec["components"] = {"schemas": {}} + for table_name in db.get_tables(): + schema = db.get_table_schema(table_name) + spec["components"]["schemas"][table_name] = { + "type": "object", + "properties": {column: {"type": sqlite_type_to_openapi_type(sqlite_type)} for column, sqlite_type in schema.items()} + } - # Initialize the path item object - if str(rule) not in spec["paths"]: - spec["paths"][str(rule)] = {} + # Generate paths for each table + for table_name in db.get_tables(): + # Get the primary key and schema for the table + _, primary_key_type = db.get_primary_key(table_name) + primary_key_type = sqlite_type_to_openapi_type(primary_key_type) + schema = spec["components"]["schemas"][table_name] - path_item = spec["paths"][str(rule)] + # Add the GET (all records), POST, PUT, and DELETE operations for the table + path_item = spec["paths"].setdefault(f'/{table_name}', {}) + add_operation_to_path(path_item, 'GET', f'/{table_name}', primary_key_type, schema) + add_operation_to_path(path_item, 'POST', f'/{table_name}', primary_key_type, schema) - # Add an operation object for each method - for method in rule.methods: - if method in ['GET', 'POST', 'PUT', 'DELETE']: - table_name = str(rule).split('/')[1] - _, primary_key_type = db.get_primary_key(table_name) - add_operation_to_path(path_item, method, str(rule), sqlite_type_to_openapi_type(primary_key_type)) + # Add the GET (single record), PUT, and DELETE operations for a record in the table + path_item = spec["paths"].setdefault(f'/{table_name}/', {}) + add_operation_to_path(path_item, 'GET', f'/{table_name}/', primary_key_type, schema) + add_operation_to_path(path_item, 'PUT', f'/{table_name}/', primary_key_type, schema) + add_operation_to_path(path_item, 'DELETE', f'/{table_name}/', primary_key_type, schema) # Validate the spec validate_spec(spec)