Update Models and Schema

This commit is contained in:
2022-07-12 06:32:03 -07:00
parent 57c9c70cbb
commit f7e93e7098
7 changed files with 317 additions and 63 deletions

View File

@@ -0,0 +1,108 @@
"""empty message
Revision ID: 938218f911e8
Revises:
Create Date: 2022-07-12 06:31:18.835124
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '938218f911e8'
down_revision = None
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.create_table('access_keys',
sa.Column('key', sa.String(length=64), nullable=False),
sa.Column('clientId', sa.String(length=64), nullable=False),
sa.Column('clientIP', sa.String(length=46), nullable=False),
sa.Column('userAgent', sa.String(length=255), nullable=False),
sa.Column('suspended', sa.Boolean(), nullable=False),
sa.Column('revoked', sa.Boolean(), nullable=False),
sa.Column('createdAt', sa.DateTime(), nullable=True),
sa.Column('suspendedAt', sa.DateTime(), nullable=True),
sa.Column('revokedAt', sa.DateTime(), nullable=True),
sa.Column('restoredAt', sa.DateTime(), nullable=True),
sa.PrimaryKeyConstraint('key')
)
op.create_table('access_tokens',
sa.Column('token', sa.String(length=64), nullable=False),
sa.Column('clientIP', sa.String(length=46), nullable=False),
sa.Column('userAgent', sa.String(length=255), nullable=False),
sa.Column('expired', sa.Boolean(), nullable=False),
sa.Column('revoked', sa.Boolean(), nullable=False),
sa.Column('issuedAt', sa.DateTime(), nullable=True),
sa.Column('expiresAt', sa.DateTime(), nullable=True),
sa.Column('revokedAt', sa.DateTime(), nullable=True),
sa.PrimaryKeyConstraint('token')
)
op.create_table('lists',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('accessKey', sa.String(length=64), nullable=True),
sa.Column('name', sa.String(length=128), nullable=False),
sa.Column('notes', sa.String(), nullable=True),
sa.Column('createdAt', sa.DateTime(), nullable=True),
sa.Column('modifiedAt', sa.DateTime(), nullable=True),
sa.PrimaryKeyConstraint('id')
)
op.create_table('products',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('accessKey', sa.String(length=64), nullable=True),
sa.Column('name', sa.String(length=128), nullable=False),
sa.Column('category', sa.String(length=128), nullable=False),
sa.Column('notes', sa.String(), nullable=True),
sa.Column('createdAt', sa.DateTime(), nullable=True),
sa.Column('modifiedAt', sa.DateTime(), nullable=True),
sa.PrimaryKeyConstraint('id')
)
op.create_index(op.f('ix_products_category'), 'products', ['category'], unique=False)
op.create_table('sigl_config',
sa.Column('key', sa.String(), nullable=False),
sa.Column('value', sa.String(length=128), nullable=True),
sa.PrimaryKeyConstraint('key')
)
op.create_table('list_entries',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('list_id', sa.Integer(), nullable=False),
sa.Column('product_id', sa.Integer(), nullable=False),
sa.Column('quantity', sa.String(length=128), nullable=True),
sa.Column('crossedOff', sa.Boolean(), nullable=True),
sa.Column('deleted', sa.Boolean(), nullable=True),
sa.Column('notes', sa.String(), nullable=True),
sa.Column('createdAt', sa.DateTime(), nullable=True),
sa.Column('modifiedAt', sa.DateTime(), nullable=True),
sa.ForeignKeyConstraint(['list_id'], ['lists.id'], ),
sa.ForeignKeyConstraint(['product_id'], ['products.id'], ),
sa.PrimaryKeyConstraint('id')
)
op.create_table('product_locations',
sa.Column('product_id', sa.Integer(), nullable=False),
sa.Column('store', sa.String(length=128), nullable=False),
sa.Column('aisle', sa.String(length=64), nullable=False),
sa.Column('bin', sa.String(length=64), nullable=True),
sa.Column('notes', sa.String(), nullable=True),
sa.Column('createdAt', sa.DateTime(), nullable=True),
sa.Column('modifiedAt', sa.DateTime(), nullable=True),
sa.ForeignKeyConstraint(['product_id'], ['products.id'], ),
sa.PrimaryKeyConstraint('product_id', 'store')
)
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_table('product_locations')
op.drop_table('list_entries')
op.drop_table('sigl_config')
op.drop_index(op.f('ix_products_category'), table_name='products')
op.drop_table('products')
op.drop_table('lists')
op.drop_table('access_tokens')
op.drop_table('access_keys')
# ### end Alembic commands ###

View File

@@ -1,56 +0,0 @@
"""empty message
Revision ID: b3639db87e06
Revises:
Create Date: 2022-07-11 17:09:20.502213
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = 'b3639db87e06'
down_revision = None
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.create_table('products',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('name', sa.String(length=128), nullable=False),
sa.Column('category', sa.String(length=128), nullable=False),
sa.Column('notes', sa.String(), nullable=True),
sa.Column('created_at', sa.DateTime(), nullable=True),
sa.Column('modified_at', sa.DateTime(), nullable=True),
sa.PrimaryKeyConstraint('id')
)
op.create_index(op.f('ix_products_category'), 'products', ['category'], unique=False)
op.create_table('sigl_config',
sa.Column('key', sa.String(), nullable=False),
sa.Column('value', sa.String(length=128), nullable=True),
sa.PrimaryKeyConstraint('key')
)
op.create_table('product_locations',
sa.Column('product_id', sa.Integer(), nullable=False),
sa.Column('store', sa.String(length=128), nullable=False),
sa.Column('aisle', sa.String(length=64), nullable=False),
sa.Column('bin', sa.String(length=64), nullable=True),
sa.Column('notes', sa.String(), nullable=True),
sa.Column('created_at', sa.DateTime(), nullable=True),
sa.Column('modified_at', sa.DateTime(), nullable=True),
sa.ForeignKeyConstraint(['product_id'], ['products.id'], ),
sa.PrimaryKeyConstraint('product_id', 'store')
)
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_table('product_locations')
op.drop_table('sigl_config')
op.drop_index(op.f('ix_products_category'), table_name='products')
op.drop_table('products')
# ### end Alembic commands ###

View File

@@ -15,6 +15,91 @@ sigl_config = db.Table(
db.Column('value', db.String(128)), db.Column('value', db.String(128)),
) )
#: Access Key Table
access_keys = db.Table(
'access_keys',
# Primary Key
db.Column('key', db.String(64), primary_key=True),
# Client Attributes
db.Column('clientId', db.String(64), nullable=False),
db.Column('clientIP', db.String(46), nullable=False),
db.Column('userAgent', db.String(255), nullable=False),
# Key Validity
db.Column('suspended', db.Boolean, nullable=False, default=False),
db.Column('revoked', db.Boolean, nullable=False, default=False),
# Timestamps
db.Column('createdAt', db.DateTime(), default=None),
db.Column('suspendedAt', db.DateTime(), default=None),
db.Column('revokedAt', db.DateTime(), default=None),
db.Column('restoredAt', db.DateTime(), default=None),
)
#: Access Token Table
access_tokens = db.Table(
'access_tokens',
# Primary Key
db.Column('token', db.String(64), primary_key=True),
# Client Attributes
db.Column('clientIP', db.String(46), nullable=False),
db.Column('userAgent', db.String(255), nullable=False),
# Key Validity
db.Column('expired', db.Boolean, nullable=False, default=False),
db.Column('revoked', db.Boolean, nullable=False, default=False),
# Timestamps
db.Column('issuedAt', db.DateTime(), default=None),
db.Column('expiresAt', db.DateTime(), default=None),
db.Column('revokedAt', db.DateTime(), default=None),
)
#: Shopping List Table
lists = db.Table(
'lists',
# Primary Key
db.Column('id', db.Integer, primary_key=True),
# Access Key
db.Column('accessKey', db.String(64), default=None),
# List Attributes
db.Column('name', db.String(128), nullable=False),
# Mixin Columns
db.Column('notes', db.String(), default=None),
db.Column('createdAt', db.DateTime(), default=None),
db.Column('modifiedAt', db.DateTime(), default=None),
)
#: List Entry Table
list_entries = db.Table(
'list_entries',
# Primary Key
db.Column('id', db.Integer, primary_key=True),
# Shopping List and Product Link
db.Column('list_id', db.ForeignKey('lists.id'), nullable=False),
db.Column('product_id', db.ForeignKey('products.id'), nullable=False),
# Entry Attributes
db.Column('quantity', db.String(128), default=None),
db.Column('crossedOff', db.Boolean, default=False),
db.Column('deleted', db.Boolean, default=False),
# Mixin Columns
db.Column('notes', db.String(), default=None),
db.Column('createdAt', db.DateTime(), default=None),
db.Column('modifiedAt', db.DateTime(), default=None),
)
#: Product Table #: Product Table
products = db.Table( products = db.Table(
'products', 'products',
@@ -22,14 +107,17 @@ products = db.Table(
# Primary Key # Primary Key
db.Column('id', db.Integer, primary_key=True), db.Column('id', db.Integer, primary_key=True),
# Access Key
db.Column('accessKey', db.String(64), default=None),
# Product Attributes # Product Attributes
db.Column('name', db.String(128), nullable=False), db.Column('name', db.String(128), nullable=False),
db.Column('category', db.String(128), nullable=False, index=True), db.Column('category', db.String(128), nullable=False, index=True),
# Mixin Columns # Mixin Columns
db.Column('notes', db.String(), default=None), db.Column('notes', db.String(), default=None),
db.Column('created_at', db.DateTime(), default=None), db.Column('createdAt', db.DateTime(), default=None),
db.Column('modified_at', db.DateTime(), default=None), db.Column('modifiedAt', db.DateTime(), default=None),
) )
#: Product Location Table #: Product Location Table
@@ -51,6 +139,6 @@ product_locations = db.Table(
# Mixin Columns # Mixin Columns
db.Column('notes', db.String(), default=None), db.Column('notes', db.String(), default=None),
db.Column('created_at', db.DateTime(), default=None), db.Column('createdAt', db.DateTime(), default=None),
db.Column('modified_at', db.DateTime(), default=None), db.Column('modifiedAt', db.DateTime(), default=None),
) )

View File

@@ -0,0 +1,67 @@
"""Sigl Access Key Model.
Simple Grocery List (Sigl) | sigl.app
Copyright (c) 2022 Asymworks, LLC. All Rights Reserved.
"""
from dataclasses import dataclass
from datetime import datetime
__all__ = ('AccessKey', 'AccessToken')
@dataclass
class AccessKey:
"""Sigl Access Key Class.
The Access Key represents a client or group of clients authorized to
interact with their specific Sigl shopping lists and products. Access Keys
are generally not revoked or deleted and remain for the lifetime of the
server.
When an Access Key is created, client details including the IP address and
User Agent string are logged for future auditing purposes. The IP address
may also be used for throttling.
"""
key: str = None
clientId: str = None
clientIP: str = None
userAgent: str = None
# Key Suspension/Revocation
suspended: bool = False
revoked: bool = False
# Timestamps
createdAt: datetime = datetime.utcnow()
suspendedAt: datetime = None
revokedAt: datetime = None
restoredAt: datetime = None
@dataclass
class AccessToken:
"""Sigl Access Token Class.
The Access Token represents authorization for a client (identified by an
Access Key) to interact with the Sigl server. The token string, issue time,
and expiry time must match the client-provided JWT for access to be granted.
Access Tokens are short-lived tokens expected to expire or be revoked, and
new tokens requested by the client.
When an Access Key is created, client details including the IP address and
User Agent string are logged for future auditing purposes.
"""
token: str = None
accessKey: str = None
clientIP: str = None
userAgent: str = None
# Key Suspension/Revocation
expired: bool = False
revoked: bool = False
# Timestamps
issuedAt: datetime = datetime.utcnow()
expiresAt: datetime = None
revokedAt: datetime = None

View File

@@ -0,0 +1,44 @@
"""Sigl Shopping List Domain Model.
Simple Grocery List (Sigl) | sigl.app
Copyright (c) 2022 Asymworks, LLC. All Rights Reserved.
"""
from dataclasses import dataclass
from typing import List
from .mixins import NotesMixin, TimestampMixin
from .product import Product
__all__ = ('ShoppingList', 'ListEntry')
@dataclass
class ListEntry(NotesMixin, TimestampMixin):
"""Information about a Product on a Shopping List.
This class contains information about a Product that is on a shopping
list, including the quantity to be purchased, notes about the entry, and
whether the Product has been crossed off the list or deleted.
"""
quantity: str = None
crossedOff: bool = False
deleted: bool = False
# Relationship Fields
shoppingList: 'ShoppingList' = None
product: Product = None
@dataclass
class ShoppingList(NotesMixin, TimestampMixin):
"""Top-Level Shopping List.
Contains a collection of `ListEntry` items which are intended to be
purchased.
"""
accessKey: str = None
name: str = None
# Populated by ORM
# entries: List[ListEntry] = None

View File

@@ -34,8 +34,8 @@ class TimestampMixin:
Adds a ``modified_at`` column to the database object with type Adds a ``modified_at`` column to the database object with type
:py:class:`datetime.datetime` :py:class:`datetime.datetime`
''' '''
created_at: datetime = datetime.utcnow() createdAt: datetime = datetime.utcnow()
modified_at: datetime = None modifiedAt: datetime = None
def set_modified_at(self): def set_modified_at(self):
self.modified_at = datetime.utcnow() self.modifiedAt = datetime.utcnow()

View File

@@ -23,6 +23,9 @@ class Product(NotesMixin, TimestampMixin):
name: str = None name: str = None
category: str = None category: str = None
# Access Control
accessKey: str = None
# Populated by ORM # Populated by ORM
# locations: List['ProductLocation'] = [] # locations: List['ProductLocation'] = []