diff --git a/http-api/app/app.py b/http-api/app/app.py index 9dbd9e7..ee8d048 100644 --- a/http-api/app/app.py +++ b/http-api/app/app.py @@ -60,12 +60,14 @@ app.include_router(enrollments.cancel, prefix='/enrollments') app.include_router(enrollments.deduplication_window, prefix='/enrollments') app.include_router(orders.router, prefix='/orders') app.include_router(users.router, prefix='/users') +app.include_router(users.add, prefix='/users') app.include_router(users.logs, prefix='/users') app.include_router(users.emails, prefix='/users') app.include_router(users.orgs, prefix='/users') app.include_router(billing.router, prefix='/billing') app.include_router(orgs.policies, prefix='/orgs') app.include_router(orgs.address, prefix='/orgs') +app.include_router(orgs.admins, prefix='/orgs') app.include_router(orgs.custom_pricing, prefix='/orgs') app.include_router(webhooks.router, prefix='/webhooks') app.include_router(settings.router, prefix='/settings') diff --git a/http-api/app/routes/orgs/__init__.py b/http-api/app/routes/orgs/__init__.py index 57e41e3..5690a7f 100644 --- a/http-api/app/routes/orgs/__init__.py +++ b/http-api/app/routes/orgs/__init__.py @@ -1,5 +1,6 @@ from .address import router as address +from .admins import router as admins from .custom_pricing import router as custom_pricing from .policies import router as policies -__all__ = ['policies', 'address', 'custom_pricing'] +__all__ = ['policies', 'address', 'custom_pricing', 'admins'] diff --git a/http-api/app/routes/orgs/admins.py b/http-api/app/routes/orgs/admins.py new file mode 100644 index 0000000..00b2525 --- /dev/null +++ b/http-api/app/routes/orgs/admins.py @@ -0,0 +1,78 @@ +from http import HTTPStatus +from typing import Annotated + +from aws_lambda_powertools.event_handler.api_gateway import Router +from aws_lambda_powertools.event_handler.openapi.params import Body +from layercake.dateutils import now +from layercake.dynamodb import DynamoDBPersistenceLayer, KeyPair +from layercake.extra_types import NameStr +from pydantic import BaseModel, EmailStr + +from api_gateway import JSONResponse +from boto3clients import dynamodb_client +from config import USER_TABLE + +router = Router() +user_layer = DynamoDBPersistenceLayer(USER_TABLE, dynamodb_client) + + +@router.get('//admins', compress=True) +def admins(id: str): + return user_layer.collection.query( + KeyPair(id, 'admins'), + limit=100, + ) + + +class Admin(BaseModel): + id: str + name: NameStr + email: EmailStr + + +@router.post('//admins', compress=True) +def grant(id: str, admin: Admin): + now_ = now() + + with user_layer.transact_writer() as transact: + transact.condition( + key=KeyPair(f'orgmembers#{id}', admin.id), + cond_expr='attribute_exists(sk)', + ) + # Grant admin privileges + transact.put( + item={ + 'id': admin.id, + 'sk': f'acls#{id}', + 'roles': ['ADMIN'], + 'created_ad': now_, + }, + cond_expr='attribute_not_exists(sk)', + ) + # Add user to admin list + transact.put( + item={ + 'id': id, + 'sk': f'admins#{admin.id}', + 'name': admin.name, + 'email': admin.email, + 'created_ad': now_, + }, + cond_expr='attribute_not_exists(sk)', + ) + + return JSONResponse(status_code=HTTPStatus.CREATED) + + +@router.delete('//admins', compress=True) +def revoke( + id: str, + user_id: Annotated[str, Body(embed=True)], +): + with user_layer.transact_writer() as transact: + # Revoke admin privileges + transact.delete(key=KeyPair(user_id, f'acls#{id}')) + # Remove user from admin list + transact.delete(key=KeyPair(id, f'admins#{user_id}')) + + return JSONResponse(status_code=HTTPStatus.OK) diff --git a/http-api/app/routes/orgs/custom_pricing.py b/http-api/app/routes/orgs/custom_pricing.py index e206dee..d31efbd 100644 --- a/http-api/app/routes/orgs/custom_pricing.py +++ b/http-api/app/routes/orgs/custom_pricing.py @@ -24,13 +24,11 @@ course_layer = DynamoDBPersistenceLayer(COURSE_TABLE, dynamodb_client) @router.get('//custompricing', compress=True) def get_custom_pricing(id: str): - result = course_layer.collection.query( + return course_layer.collection.query( PartitionKey(f'CUSTOM_PRICING#ORG#{id}'), limit=100, ) - return result - class CustomPricing(BaseModel): course_id: UUID4 @@ -38,7 +36,7 @@ class CustomPricing(BaseModel): @router.post('//custompricing', compress=True) -def post_custom_pricing(id: str, custom_princing: CustomPricing): +def add_custom_pricing(id: str, custom_princing: CustomPricing): now_ = now() with course_layer.transact_writer() as transact: @@ -61,18 +59,18 @@ def post_custom_pricing(id: str, custom_princing: CustomPricing): return JSONResponse(status_code=HTTPStatus.CREATED) -class DeleteCustomPricing(BaseModel): +class RemoveCustomPricing(BaseModel): course_id: UUID4 @router.delete('//custompricing', compress=True) -def delete_custom_pricing(id: str, custom_princing: DeleteCustomPricing): - pair = KeyPair( +def remove_custom_pricing(id: str, custom_princing: RemoveCustomPricing): + key = KeyPair( f'CUSTOM_PRICING#ORG#{id}', f'COURSE#{custom_princing.course_id}', ) - if course_layer.delete_item(pair): + if course_layer.delete_item(key): return JSONResponse(status_code=HTTPStatus.OK) diff --git a/http-api/app/routes/users/__init__.py b/http-api/app/routes/users/__init__.py index b8d11fc..b00d658 100644 --- a/http-api/app/routes/users/__init__.py +++ b/http-api/app/routes/users/__init__.py @@ -23,14 +23,14 @@ from api_gateway import JSONResponse from boto3clients import dynamodb_client, idp_client from config import MEILISEARCH_API_KEY, MEILISEARCH_HOST, USER_POOOL_ID, USER_TABLE from middlewares import AuditLogMiddleware, Tenant, TenantMiddleware -from models import User from rules.user import update_user +from .add import router as add from .emails import router as emails from .logs import router as logs from .orgs import router as orgs -__all__ = ['logs', 'emails', 'orgs'] +__all__ = ['add', 'logs', 'emails', 'orgs'] class BadRequestError(MissingError, PowertoolsBadRequestError): @@ -82,17 +82,6 @@ def get_users(): ) -@router.post( - '/', - compress=True, - tags=['User'], - summary='Create user', - middlewares=[AuditLogMiddleware('USER_ADD', user_collect)], -) -def post_user(payload: User): - return JSONResponse(status_code=HTTPStatus.CREATED) - - class UserData(BaseModel): name: NameStr cpf: CpfStr @@ -129,7 +118,7 @@ def put_user(id: str, payload: UserData): @router.get('/', compress=True, tags=['User'], summary='Get user') def get_user(id: str): return user_collect.get_items( - TransactKey(id) + SortKey('0') + SortKey('rate_limit#user_update') + TransactKey(id) + SortKey('0') + SortKey('RATE_LIMIT#USER_UPDATE') ) diff --git a/http-api/app/routes/users/add.py b/http-api/app/routes/users/add.py new file mode 100644 index 0000000..2208f6d --- /dev/null +++ b/http-api/app/routes/users/add.py @@ -0,0 +1,163 @@ +from http import HTTPStatus +from uuid import uuid4 + +from aws_lambda_powertools import Logger +from aws_lambda_powertools.event_handler.api_gateway import Router +from aws_lambda_powertools.event_handler.exceptions import ( + BadRequestError, + NotFoundError, +) +from layercake.dateutils import now +from layercake.dynamodb import DynamoDBPersistenceLayer, KeyPair, SortKey +from layercake.extra_types import CnpjStr, CpfStr, NameStr +from pydantic import ( + UUID4, + BaseModel, + ConfigDict, + EmailStr, + Field, +) + +from api_gateway import JSONResponse +from boto3clients import dynamodb_client +from config import USER_TABLE + +router = Router() +logger = Logger(__name__) +layer = DynamoDBPersistenceLayer(USER_TABLE, dynamodb_client) + + +class Org(BaseModel): + id: str + name: str + cnpj: CnpjStr + + +class User(BaseModel): + model_config = ConfigDict(arbitrary_types_allowed=True) + + id: UUID4 = Field(default_factory=uuid4) + name: NameStr + email: EmailStr + email_verified: bool = False + cpf: CpfStr + org: Org = Field(..., exclude=True) + + +class UserNotFoundError(NotFoundError): + def __init__(self, *_): + super().__init__('User not found') + + +class CPFConflictError(BadRequestError): + def __init__(self, *_): + super().__init__('CPF already exists') + + +class EmailConflictError(BadRequestError): + def __init__(self, *_): + super().__init__('Email already exists') + + +@router.post('/', compress=True) +def add(user: User): + now_ = now() + user_id = user.id + org = user.org + + try: + with layer.transact_writer() as transact: + transact.put( + item={ + 'id': 'cpf', + 'sk': user.cpf, + 'user_id': user.id, + 'created_at': now_, + }, + cond_expr='attribute_not_exists(sk)', + exc_cls=CPFConflictError, + ) + transact.put( + item={ + 'id': 'email', + 'sk': user.email, + 'user_id': user.id, + 'created_at': now_, + }, + cond_expr='attribute_not_exists(sk)', + exc_cls=EmailConflictError, + ) + transact.put( + item={ + 'sk': '0', + 'tenant_id': {org.id}, + # Post-migration: uncomment the following line + # 'createDate': now_, + 'createDate': now_, + } + | user.model_dump() + ) + transact.put( + item={ + 'id': user.id, + 'sk': f'orgs#{user.org.id}', + 'name': org.name, + 'cnpj': org.cnpj, + 'created_at': now_, + } + ) + transact.put( + item={ + 'id': f'orgmembers#{org.id}', + 'sk': user.id, + 'created_at': now_, + } + ) + + except (CPFConflictError, EmailConflictError): + user_id = layer.collection.get_items( + KeyPair( + pk='cpf', + sk=SortKey(user.cpf, path_spec='user_id'), + rename_key='id', + ) + + KeyPair( + pk='email', + sk=SortKey(user.email, path_spec='user_id'), + rename_key='id', + ), + flatten_top=False, + ).get('id') + + if not user_id: + raise UserNotFoundError() + + with layer.transact_writer() as transact: + transact.update( + key=KeyPair(user_id, '0'), + update_expr='ADD tenant_id :org_id', + expr_attr_values={ + ':org_id': {org.id}, + }, + ) + transact.put( + item={ + 'id': user_id, + 'sk': f'orgs#{user.org.id}', + 'name': org.name, + 'cnpj': org.cnpj, + 'created_at': now_, + } + ) + transact.put( + item={ + 'id': f'orgmembers#{org.id}', + 'sk': user_id, + 'created_at': now_, + } + ) + + return JSONResponse( + status_code=HTTPStatus.CREATED, + body={'id': user_id}, + ) diff --git a/http-api/app/routes/users/emails.py b/http-api/app/routes/users/emails.py index 0a4a1e7..0cdefb0 100644 --- a/http-api/app/routes/users/emails.py +++ b/http-api/app/routes/users/emails.py @@ -1,13 +1,11 @@ from http import HTTPStatus +from typing import Annotated from aws_lambda_powertools.event_handler.api_gateway import Router -from aws_lambda_powertools.event_handler.exceptions import ( - BadRequestError as PowertoolsBadRequestError, -) +from aws_lambda_powertools.event_handler.openapi.params import Body from layercake.dynamodb import ( DynamoDBPersistenceLayer, KeyPair, - MissingError, PrefixKey, ) from pydantic import BaseModel, EmailStr @@ -15,23 +13,13 @@ from pydantic import BaseModel, EmailStr from api_gateway import JSONResponse from boto3clients import dynamodb_client from config import USER_TABLE -from middlewares import AuditLogMiddleware -from rules.user import add_email, del_email, set_email_as_primary - - -class BadRequestError(MissingError, PowertoolsBadRequestError): ... - +from rules.user import add_email, remove_email, set_email_as_primary router = Router() user_layer = DynamoDBPersistenceLayer(USER_TABLE, dynamodb_client) -@router.get( - '//emails', - compress=True, - tags=['User'], - summary='Get user emails', -) +@router.get('//emails', compress=True) def get_emails(id: str): start_key = router.current_event.get_query_string_value('start_key', None) @@ -45,18 +33,12 @@ class Email(BaseModel): email: EmailStr -@router.post( - '//emails', - compress=True, - tags=['User'], - summary='Add user email', - middlewares=[AuditLogMiddleware('EMAIL_ADD', user_layer.collection, ('email',))], -) -def post_email(id: str, payload: Email): - add_email(id, payload.email, persistence_layer=user_layer) +@router.post('//emails', compress=True) +def add_email_(id: str, email: Annotated[str, Body(embed=True)]): + add_email(id, email, persistence_layer=user_layer) return JSONResponse( - body=payload, + body={'email': email}, status_code=HTTPStatus.CREATED, ) @@ -67,22 +49,7 @@ class EmailAsPrimary(BaseModel): email_verified: bool = False -@router.patch( - '//emails', - compress=True, - tags=['User'], - summary='Add user email as primary', - middlewares=[ - AuditLogMiddleware( - 'EMAIL_CHANGE', - user_layer.collection, - ( - 'new_email', - 'old_email', - ), - ) - ], -) +@router.patch('//emails', compress=True) def patch_email(id: str, payload: EmailAsPrimary): set_email_as_primary( id, @@ -98,15 +65,9 @@ def patch_email(id: str, payload: EmailAsPrimary): ) -@router.delete( - '//emails', - compress=True, - tags=['User'], - summary='Delete user email', - middlewares=[AuditLogMiddleware('EMAIL_DEL', user_layer.collection, ('email',))], -) +@router.delete('//emails', compress=True) def delete_email(id: str, payload: Email): - del_email( + remove_email( id, payload.email, persistence_layer=user_layer, diff --git a/http-api/app/routes/users/logs.py b/http-api/app/routes/users/logs.py index 3067f6b..03fe1c7 100644 --- a/http-api/app/routes/users/logs.py +++ b/http-api/app/routes/users/logs.py @@ -4,7 +4,6 @@ from aws_lambda_powertools.event_handler.exceptions import ( ) from layercake.dynamodb import ( ComposeKey, - DynamoDBCollection, DynamoDBPersistenceLayer, MissingError, PartitionKey, @@ -13,29 +12,19 @@ from layercake.dynamodb import ( from boto3clients import dynamodb_client from config import USER_TABLE -from .orgs import router as orgs - -__all__ = ['orgs'] - class BadRequestError(MissingError, PowertoolsBadRequestError): ... router = Router() user_layer = DynamoDBPersistenceLayer(USER_TABLE, dynamodb_client) -user_collect = DynamoDBCollection(user_layer, exc_cls=BadRequestError) -@router.get( - '//logs', - compress=True, - tags=['User'], - summary='Get user logs', -) +@router.get('//logs', compress=True, tags=['User']) def get_logs(id: str): - return user_collect.query( - # Post-migration: uncomment to enable PartitionKey with a composite key (id with `logs` prefix). - # PartitionKey(ComposeKey(id, 'logs')), + return user_layer.collection.query( + # Post-migration: uncomment the following line + # PartitionKey(ComposeKey(id, 'LOGS')), PartitionKey(ComposeKey(id, 'log', delimiter=':')), start_key=router.current_event.get_query_string_value('start_key', None), ) diff --git a/http-api/app/routes/users/orgs.py b/http-api/app/routes/users/orgs.py index b5dac5f..2e37aec 100644 --- a/http-api/app/routes/users/orgs.py +++ b/http-api/app/routes/users/orgs.py @@ -16,8 +16,7 @@ from pydantic import BaseModel from api_gateway import JSONResponse from boto3clients import dynamodb_client from config import USER_TABLE -from middlewares.audit_log_middleware import AuditLogMiddleware -from rules.user import del_org_member +from rules.user import remove_org_member class BadRequestError(MissingError, PowertoolsBadRequestError): ... @@ -30,13 +29,13 @@ user_layer = DynamoDBPersistenceLayer(USER_TABLE, dynamodb_client) @router.get( '//orgs', compress=True, - tags=['User'], - summary='Get user orgs', ) def get_orgs(id: str): + start_key = router.current_event.get_query_string_value('start_key', None) + return user_layer.collection.query( KeyPair(id, PrefixKey('orgs')), - start_key=router.current_event.get_query_string_value('start_key', None), + start_key=start_key, ) @@ -46,17 +45,7 @@ class Unassign(BaseModel): cnpj: CnpjStr -@router.delete( - '//orgs', - compress=True, - tags=['User'], - summary='Delete user org', - middlewares=[ - AuditLogMiddleware( - 'UNASSIGN_ORG', user_layer.collection, ('id', 'name', 'cnpj') - ) - ], -) +@router.delete('//orgs', compress=True) def delete_org(id: str, payload: Unassign): - del_org_member(id, org_id=payload.id, persistence_layer=user_layer) + remove_org_member(id, org_id=payload.id, persistence_layer=user_layer) return JSONResponse(status_code=HTTPStatus.OK, body=payload) diff --git a/http-api/app/routes/users/password.py b/http-api/app/routes/users/password.py new file mode 100644 index 0000000..e69de29 diff --git a/http-api/app/rules/user.py b/http-api/app/rules/user.py index 398b37a..68ead8c 100644 --- a/http-api/app/rules/user.py +++ b/http-api/app/rules/user.py @@ -15,14 +15,29 @@ from layercake.dynamodb import ( User = TypedDict('User', {'id': str, 'name': str, 'cpf': str}) +class CPFConflictError(BadRequestError): + def __init__(self, *_): + super().__init__('CPF already exists') + + +class RateLimitError(BadRequestError): + def __init__(self, *_): + super().__init__('Update limit reached') + + +class EmailConflictError(BadRequestError): + def __init__(self, *_): + super().__init__('Email already exists') + + def update_user( - data: User, + obj: User, /, *, persistence_layer: DynamoDBPersistenceLayer, ) -> bool: now_ = now() - user = SimpleNamespace(**data) + user = SimpleNamespace(**obj) # Get the user's CPF, if it exists. old_cpf = persistence_layer.collection.get_item( KeyPair( @@ -46,15 +61,11 @@ def update_user( cond_expr='attribute_exists(sk)', ) - class RateLimitError(BadRequestError): - def __init__(self, msg: str): - super().__init__('Update limit reached') - # Prevent the user from updating more than once every 24 hours transact.put( item={ 'id': user.id, - 'sk': 'rate_limit#user_update', + 'sk': 'RATE_LIMIT#USER_UPDATE', 'created_at': now_, 'ttl': ttl(start_dt=now_ + timedelta(hours=24)), }, @@ -62,10 +73,6 @@ def update_user( cond_expr='attribute_not_exists(sk)', ) - class CPFConflictError(BadRequestError): - def __init__(self, msg: str): - super().__init__('CPF already exists') - if user.cpf != old_cpf: transact.put( item={ @@ -114,10 +121,6 @@ def add_email( cond_expr='attribute_not_exists(sk)', ) - class EmailConflictError(BadRequestError): - def __init__(self, msg: str): - super().__init__('Email already exists') - # Prevent duplicate emails transact.put( item={ @@ -133,7 +136,7 @@ def add_email( return True -def del_email( +def remove_email( id: str, email: str, /, @@ -207,7 +210,7 @@ def set_email_as_primary( return True -def del_org_member( +def remove_org_member( id: str, *, org_id: str, diff --git a/http-api/cli/seeds.py b/http-api/cli/seeds.py index 100779b..a8a0be9 100644 --- a/http-api/cli/seeds.py +++ b/http-api/cli/seeds.py @@ -1,9 +1,9 @@ -from typing import Any, Generator +from typing import Generator import boto3 import jsonlines from aws_lambda_powertools.shared.json_encoder import Encoder -from layercake.dynamodb import deserialize +from layercake.dynamodb import deserialize, serialize from meilisearch import Client as Meilisearch from tqdm import tqdm @@ -11,6 +11,14 @@ dynamodb_client = boto3.client('dynamodb', endpoint_url='http://127.0.0.1:8000') meili_client = Meilisearch('http://127.0.0.1:7700') +JSONL_FILES = ( + # 'test-orders.jsonl', + 'test-users.jsonl', + # 'test-enrollments.jsonl', + # 'test-courses.jsonl', +) + + class JSONEncoder(Encoder): def default(self, obj): if isinstance(obj, set): @@ -18,19 +26,11 @@ class JSONEncoder(Encoder): return super(__class__, self).default(obj) -jsonl_files = ( - 'test-orders.jsonl', - 'test-users.jsonl', - 'test-enrollments.jsonl', - 'test-courses.jsonl', -) - - def put_item(item: dict, table_name: str, /, dynamodb_client) -> bool: try: dynamodb_client.put_item( TableName=table_name, - Item=item, + Item=serialize(item), ) except Exception: return False @@ -45,7 +45,7 @@ def scan_table(table_name: str, /, dynamodb_client, **kwargs) -> Generator: yield from () else: for item in r['Items']: - yield deserialize(item) + yield item if 'LastEvaluatedKey' in r: yield from scan_table( @@ -55,22 +55,9 @@ def scan_table(table_name: str, /, dynamodb_client, **kwargs) -> Generator: ) -def _serialize_to_basic_types(value: Any) -> Any: - if isinstance(value, dict): - return {k: _serialize_to_basic_types(v) for k, v in value.items()} - - if isinstance(value, set): - return list(value) - - if isinstance(value, list): - return [_serialize_to_basic_types(v) for v in value] - - return value - - if __name__ == '__main__': # Populate DynamoDB tables with data from JSONL files - for file in tqdm(jsonl_files, desc='Processing files'): + for file in tqdm(JSONL_FILES, desc='Processing files'): with open(f'seeds/{file}') as fp: table_name = file.removesuffix('.jsonl') reader = jsonlines.Reader(fp).iter(skip_invalid=True) @@ -79,7 +66,7 @@ if __name__ == '__main__': put_item(line, table_name, dynamodb_client) # type: ignore # Scan DynamoDB tables and index the data into Meilisearch - for file in tqdm(jsonl_files, desc='Scanning tables'): + for file in tqdm(JSONL_FILES, desc='Scanning tables'): table_name = file.removesuffix('.jsonl') for doc in tqdm( @@ -91,6 +78,7 @@ if __name__ == '__main__': ), desc=f'Indexing {table_name}', ): + doc = deserialize(doc) meili_client.index(table_name).add_documents([doc], serializer=JSONEncoder) meili_client.index('pytest').add_documents([doc], serializer=JSONEncoder) @@ -98,6 +86,6 @@ if __name__ == '__main__': index.update_settings( { 'sortableAttributes': ['create_date', 'createDate', 'created_at'], - 'filterableAttributes': ['tenant_id', 'status'], + 'filterableAttributes': ['tenant_id', 'status', 'cnpj'], } ) diff --git a/http-api/seeds/test-users.jsonl b/http-api/seeds/test-users.jsonl index 156582d..117bea1 100644 --- a/http-api/seeds/test-users.jsonl +++ b/http-api/seeds/test-users.jsonl @@ -1,76 +1,28 @@ -{"id": {"S": "apikey"}, "sk": {"S": "MzI1MDQ0NTctZjEzMy00YzAwLTkzNmItNmFhNzEyY2E5ZjQw"}, "tenant": {"M": {"id": {"S": "*"}, "name": {"S": "default"}}}, "user": {"M": {"id": {"S": "5OxmMjL-ujoR5IMGegQz"}, "name": {"S": "Sérgio R Siqueira"}, "email": {"S": "sergio@somosbeta.com.br"}}}} -{"updateDate": {"S": "2024-02-08T16:42:33.776409-03:00"}, "createDate": {"S": "2019-03-25T00:00:00-03:00"}, "email_verified": {"BOOL": true}, "cognito__sub": {"S": "58efed8d-d276-41a8-8502-4ab8b5a6415e"}, "cpf": {"S": "07879819908"}, "sk": {"S": "0"}, "email": {"S": "sergio@somosbeta.com.br"}, "id": {"S": "5OxmMjL-ujoR5IMGegQz"}, "name": {"S": "S\u00e9rgio Rafael de Siqueira"}, "lastLogin": {"S": "2024-02-08T20:53:45.818126-03:00"}, "tenant_id": {"L": [{"S": "cJtK9SsnJhKPyxESe7g3DG"}, {"S": "edp8njvgQuzNkLx2ySNfAD"}, {"S": "8TVSi5oACLxTiT8ycKPmaQ"}]}} -{"sk": {"S": "acl#admin"}, "id": {"S": "5OxmMjL-ujoR5IMGegQz"}, "create_date": {"S": "2022-06-13T15:00:24.309410-03:00"}} -{"email_verified": {"BOOL": true}, "update_date": {"S": "2024-02-08T16:42:33.776409-03:00"}, "create_date": {"S": "2024-01-19T22:53:43.135080-03:00"}, "deliverability": {"S": "DELIVERABLE"}, "sk": {"S": "emails#osergiosiqueira@gmail.com"}, "id": {"S": "5OxmMjL-ujoR5IMGegQz"}, "primaryEmail": {"BOOL": false}, "mx_record_exists": {"BOOL": true}} -{"email_verified": {"BOOL": true}, "update_date": {"S": "2024-02-08T16:42:33.776409-03:00"}, "create_date": {"S": "2019-03-25T00:00:00-03:00"}, "sk": {"S": "emails#sergio@somosbeta.com.br"}, "id": {"S": "5OxmMjL-ujoR5IMGegQz"}, "email_primary": {"BOOL": true}, "mx_record_exists": {"BOOL": true}, "update_date": {"S": "2023-11-09T12:13:04.308986-03:00"}} -{"email_verified": {"BOOL": false}, "update_date": {"S": "2023-12-29T02:18:27.225158-03:00"}, "create_date": {"S": "2022-09-01T12:23:15.431473-03:00"}, "sk": {"S": "emails#sergio@users.noreply.betaeducacao.com.br"}, "id": {"S": "5OxmMjL-ujoR5IMGegQz"}, "email_primary": {"BOOL": false}} -{"sk": {"S": "konviva"}, "createDate": {"S": "2023-07-22T21:46:02.527763-03:00"}, "id": {"S": "5OxmMjL-ujoR5IMGegQz"}, "konvivaId": {"N": "26943"}} -{"sk": {"S": "org#cJtK9SsnJhKPyxESe7g3DG"}, "createDate": {"S": "2023-12-24T20:50:27.656310-03:00"}, "id": {"S": "5OxmMjL-ujoR5IMGegQz"}, "name": {"S": "Beta Educa\u00e7\u00e3o"}} -{"sk": {"S": "sergio@somosbeta.com.br"}, "createDate": {"S": "2019-03-25T00:00:00-03:00"}, "userRefId": {"S": "5OxmMjL-ujoR5IMGegQz"}, "id": {"S": "email"}} -{"sk": {"S": "07879819908"}, "createDate": {"S": "2024-02-06T15:16:18.992509-03:00"}, "userRefId": {"S": "5OxmMjL-ujoR5IMGegQz"}, "id": {"S": "cpf"}} -{"updateDate": {"S": "2023-12-22T13:03:20.478342-03:00"}, "createDate": {"S": "2023-09-22T18:27:30.193484-03:00"}, "status": {"S": "CREATED"}, "sk": {"S": "0"}, "cnpj": {"S": "15608435000190"}, "email": {"S": "org+15608435000190@users.noreply.betaeducacao.com.br"}, "id": {"S": "cJtK9SsnJhKPyxESe7g3DG"}, "name": {"S": "Beta Educa\u00e7\u00e3o"}} -{"updateDate": {"S": "2023-12-22T13:03:20.478342-03:00"}, "createDate": {"S": "2023-09-22T18:27:30.193484-03:00"}, "status": {"S": "CREATED"}, "sk": {"S": "0"}, "cnpj": {"S": "13573332000107"}, "email": {"S": "org+13573332000107@users.noreply.betaeducacao.com.br"}, "id": {"S": "edp8njvgQuzNkLx2ySNfAD"}, "name": {"S": "KORD S.A"}} -{"id": {"S": "cJtK9SsnJhKPyxESe7g3DG"}, "sk": {"S": "tags#MqiRQWy9cSBEci93aKBvTd"}, "tag": {"S": "Test"}, "create_date": {"S": "2023-09-22T18:31:11.475449-03:00"}} -{"id": {"S": "cJtK9SsnJhKPyxESe7g3DG"}, "sk": {"S": "tags#NQ3YmmTesfoSyHCkPKGKEF"}, "tag": {"S": "F\u00e1brica"}, "create_date": {"S": "2023-09-22T18:31:11.475449-03:00"}} -{"id": {"S": "cJtK9SsnJhKPyxESe7g3DG"}, "sk": {"S": "tags#bg45io8igarjsA4BzPQyrz"}, "tag": {"S": "Escrit\u00f3rio"}, "create_date": {"S": "2023-09-22T18:31:11.475449-03:00"}} -{"id": {"S": "cJtK9SsnJhKPyxESe7g3DG"}, "sk": {"S": "payment_policy"}, "due_days": {"N": "90"}} -{"id": {"S": "cJtK9SsnJhKPyxESe7g3DG"}, "sk": {"S": "billing_policy"}, "billing_day": {"N": "2"}, "payment_method": {"S": "PIX"}} -{"sk": {"S": "admin#18606"}, "create_date": {"S": "2023-09-22T18:31:11.475449-03:00"}, "id": {"S": "cJtK9SsnJhKPyxESe7g3DG"}, "email": {"S": "mateushickmann@petrobras.com.br"}, "name": {"S": "MATEUS LATTIK HICKMANN"}} -{"sk": {"S": "admin#21425"}, "create_date": {"S": "2023-09-22T18:34:06.480230-03:00"}, "id": {"S": "cJtK9SsnJhKPyxESe7g3DG"}, "email": {"S": "sergio@inbep.com.br"}, "name": {"S": "S\u00e9rgio Siqueira"}} -{"sk": {"S": "admin#2338"}, "create_date": {"S": "2023-09-22T18:28:10.121068-03:00"}, "id": {"S": "cJtK9SsnJhKPyxESe7g3DG"}, "email": {"S": "empresa@inbep.com.br"}, "name": {"S": "Empresa Teste"}} -{"sk": {"S": "admin#26931"}, "create_date": {"S": "2023-09-22T18:39:04.522084-03:00"}, "id": {"S": "cJtK9SsnJhKPyxESe7g3DG"}, "email": {"S": "cida@qgog.com.br"}, "name": {"S": "Maria Aparecida do Nascimento"}} -{"sk": {"S": "admin#346628ce-a6c8-4fff-a6ee-5378675e220a"}, "create_date": {"S": "2023-10-01T14:25:16.662110-03:00"}, "id": {"S": "cJtK9SsnJhKPyxESe7g3DG"}, "email": {"S": "leandrodorea@kofre.com.br"}, "name": {"S": "Leandro Barbosa Dorea"}} -{"sk": {"S": "admin#5OxmMjL-ujoR5IMGegQz"}, "create_date": {"S": "2023-12-24T20:50:27.656310-03:00"}, "id": {"S": "cJtK9SsnJhKPyxESe7g3DG"}, "email": {"S": "sergio@somosbeta.com.br"}, "name": {"S": "S\u00e9rgio R Siqueira"}} -{"sk": {"S": "admin#9a41e867-55e1-4573-bd27-7b5d1d1bcfde"}, "create_date": {"S": "2023-09-22T18:31:28.540271-03:00"}, "id": {"S": "cJtK9SsnJhKPyxESe7g3DG"}, "email": {"S": "tiago@somosbeta.com.br"}, "name": {"S": "Tiago"}} -{"sk": {"S": "admin#DbMaGtQX4wXPDZyBjdZMzh"}, "create_date": {"S": "2023-09-22T18:34:46.696606-03:00"}, "id": {"S": "cJtK9SsnJhKPyxESe7g3DG"}, "email": {"S": "sergio+postman@somosbeta.com.br"}, "name": {"S": "S\u00e9rgio Rafael Siqueira"}} -{"sk": {"S": "admin#JMnHo46tR4aWVzH2dfus52"}, "create_date": {"S": "2023-09-22T18:39:58.373125-03:00"}, "id": {"S": "cJtK9SsnJhKPyxESe7g3DG"}, "email": {"S": "bruno@inbep.com.br"}, "name": {"S": "Bruno Lins de Albuquerque"}} -{"sk": {"S": "admin#TSxk7P89kRcNuLGy8A88ao"}, "create_date": {"S": "2023-09-22T18:28:02.875368-03:00"}, "id": {"S": "cJtK9SsnJhKPyxESe7g3DG"}, "email": {"S": "tiago@inbep.com.br"}, "name": {"S": "Tiago Maciel"}} -{"sk": {"S": "admin#YJkbDL7C2htnJVK8DXDiV2"}, "create_date": {"S": "2023-09-22T18:30:18.492717-03:00"}, "id": {"S": "cJtK9SsnJhKPyxESe7g3DG"}, "email": {"S": "acorianabuffet@gmail.com"}, "name": {"S": "Nadyjanara Leal Albino"}} -{"sk": {"S": "admin#hT2CD9lDeeqlOXLjR4mG"}, "create_date": {"S": "2023-09-22T18:27:30.344151-03:00"}, "id": {"S": "cJtK9SsnJhKPyxESe7g3DG"}, "email": {"S": "alessandraw@unc.br"}, "name": {"S": "Alessandra Wagner Jusviacky"}} -{"emailVerified": {"BOOL": true}, "createDate": {"S": "2023-09-22T18:27:30.193484-03:00"}, "sk": {"S": "emails#org+15608435000190@users.noreply.betaeducacao.com.br"}, "id": {"S": "cJtK9SsnJhKPyxESe7g3DG"}, "primaryEmail": {"BOOL": true}, "emailDeliverable": {"BOOL": true}} -{"expiry_date": {"S": "2024-07-08T17:38:29.159112-03:00"}, "user": {"M": {"name": {"S": "S\u00e9rgio R Siqueira"}, "email": {"S": "sergio@somosbeta.com.br"}}}, "ttl": {"N": "1720471109"}, "sk": {"S": "schedules#follow_up#5OxmMjL-ujoR5IMGegQz"}, "id": {"S": "cJtK9SsnJhKPyxESe7g3DG"}, "create_date": {"S": "2024-01-10T17:38:29.159148-03:00"}} -{"updateDate": {"S": "2022-08-09T09:44:39.386636-03:00"}, "createDate": {"S": "2022-06-13T10:20:00.528685-03:00"}, "status": {"S": "CONFIRMED"}, "cpf": {"S": "08679004901"}, "sk": {"S": "0"}, "email": {"S": "tiago@somosbeta.com.br"}, "id": {"S": "9a41e867-55e1-4573-bd27-7b5d1d1bcfde"}, "mobileNumber": {"S": ""}, "name": {"S": "Tiago Maciel"}, "lastLogin": {"S": "2024-02-07T11:14:34.516596-03:00"}, "tenant_id": {"L": [{"S": "cJtK9SsnJhKPyxESe7g3DG"}]}} -{"emailVerified": {"BOOL": true}, "updateDate": {"S": "2022-08-09T09:44:39.400384-03:00"}, "createDate": {"S": "2022-06-13T10:20:00.528685-03:00"}, "sk": {"S": "emails#tiago@somosbeta.com.br"}, "id": {"S": "9a41e867-55e1-4573-bd27-7b5d1d1bcfde"}, "primaryEmail": {"BOOL": true}, "emailDeliverable": {"BOOL": true}} -{"emailVerified": {"BOOL": true}, "updateDate": {"S": "2022-08-09T09:44:39.400384-03:00"}, "createDate": {"S": "2022-06-13T10:20:00.528685-03:00"}, "sk": {"S": "emails#tiago+1@somosbeta.com.br"}, "id": {"S": "9a41e867-55e1-4573-bd27-7b5d1d1bcfde"}, "primaryEmail": {"BOOL": false}, "emailDeliverable": {"BOOL": true}} -{"emailVerified": {"BOOL": true}, "updateDate": {"S": "2022-08-09T09:44:39.400384-03:00"}, "createDate": {"S": "2022-06-13T10:20:00.528685-03:00"}, "sk": {"S": "emails#tiago+2@somosbeta.com.br"}, "id": {"S": "9a41e867-55e1-4573-bd27-7b5d1d1bcfde"}, "primaryEmail": {"BOOL": false}, "emailDeliverable": {"BOOL": true}} -{"sk": {"S": "konviva"}, "createDate": {"S": "2023-07-22T21:46:04.494710-03:00"}, "id": {"S": "9a41e867-55e1-4573-bd27-7b5d1d1bcfde"}, "konvivaId": {"N": "26946"}} -{"sk": {"S": "org#VmkwfVGq5r7vEckEM8uiRf"}, "createDate": {"S": "2023-09-22T18:31:14.779525-03:00"}, "id": {"S": "9a41e867-55e1-4573-bd27-7b5d1d1bcfde"}, "name": {"S": "MCEND INSPE\u00c7\u00d5ES, CONSULTORIA E CONTROLE DE QUALIDADE"}} -{"sk": {"S": "org#cJtK9SsnJhKPyxESe7g3DG"}, "createDate": {"S": "2023-09-22T18:31:28.540230-03:00"}, "id": {"S": "9a41e867-55e1-4573-bd27-7b5d1d1bcfde"}, "name": {"S": "Funda\u00e7\u00e3o Universidade do Contestado - FUnC Campus Mafra"}} -{"action": {"S": "EMAIL_ADD"}, "data": {"M": {"email": {"S": "re+962834@users.noreply.betaeducacao.com.br"}}}, "ip": {"S": "189.73.20.218"}, "ttl": {"N": "1770420417"}, "sk": {"S": "2024-02-07T20:26:57.434320-03:00"}, "id": {"S": "log:5OxmMjL-ujoR5IMGegQz"}, "actor": {"M": {"name": {"S": "S\u00e9rgio Rafael Siqueira"}, "id": {"S": "5OxmMjL-ujoR5IMGegQz"}}}} -{"action": {"S": "EMAIL_ADD"}, "data": {"M": {"email": {"S": "test@xptoa.com"}}}, "ip": {"S": "189.73.20.218"}, "ttl": {"N": "1770420434"}, "sk": {"S": "2024-02-07T20:27:14.874383-03:00"}, "id": {"S": "log:5OxmMjL-ujoR5IMGegQz"}, "actor": {"M": {"name": {"S": "S\u00e9rgio Rafael Siqueira"}, "id": {"S": "5OxmMjL-ujoR5IMGegQz"}}}} -{"action": {"S": "EMAIL_DEL"}, "data": {"M": {"email": {"S": "test@xptoa.com"}}}, "ip": {"S": "189.73.20.218"}, "ttl": {"N": "1770420444"}, "sk": {"S": "2024-02-07T20:27:24.615391-03:00"}, "id": {"S": "log:5OxmMjL-ujoR5IMGegQz"}, "actor": {"M": {"name": {"S": "S\u00e9rgio Rafael Siqueira"}, "id": {"S": "5OxmMjL-ujoR5IMGegQz"}}}} -{"action": {"S": "user.enable_reverse_email"}, "data": {"M": {"email": {"S": "re+9628@users.noreply.betaeducacao.com.br"}}}, "ip": {"S": "189.73.20.218"}, "ttl": {"N": "3477771072"}, "sk": {"S": "2024-02-07T20:45:36.807607-03:00"}, "actorLocation": {"NULL": true}, "id": {"S": "log:5OxmMjL-ujoR5IMGegQz"}, "actor": {"M": {"name": {"S": "S\u00e9rgio Rafael Siqueira"}, "id": {"S": "5OxmMjL-ujoR5IMGegQz"}}}} -{"action": {"S": "EMAIL_ADD"}, "data": {"M": {"email": {"S": "sergio+12121@somosbeta.com.br"}}}, "ip": {"S": "189.73.20.218"}, "ttl": {"N": "1770492028"}, "sk": {"S": "2024-02-08T16:20:28.159065-03:00"}, "id": {"S": "log:5OxmMjL-ujoR5IMGegQz"}, "actor": {"M": {"name": {"S": "S\u00e9rgio Rafael Siqueira"}, "id": {"S": "5OxmMjL-ujoR5IMGegQz"}}}} -{"action": {"S": "PRIMARY_EMAIL_CHANGED"}, "data": {"M": {"old_email": {"S": "sergio@somosbeta.com.br"}, "new_email": {"S": "osergiosiqueira@gmail.com"}}}, "ip": {"S": "189.73.20.218"}, "ttl": {"N": "1770493307"}, "sk": {"S": "2024-02-08T16:41:47.678483-03:00"}, "id": {"S": "log:5OxmMjL-ujoR5IMGegQz"}, "actor": {"M": {"name": {"S": "S\u00e9rgio Rafael Siqueira"}, "id": {"S": "5OxmMjL-ujoR5IMGegQz"}}}} -{"ip": {"S": "189.73.20.218"}, "ttl": {"N": "1770493330"}, "sk": {"S": "2024-02-08T16:42:10.010857-03:00"}, "action": {"S": "LOGIN"}, "id": {"S": "log:5OxmMjL-ujoR5IMGegQz"}} -{"action": {"S": "PRIMARY_EMAIL_CHANGED"}, "data": {"M": {"old_email": {"S": "osergiosiqueira@gmail.com"}, "new_email": {"S": "sergio@somosbeta.com.br"}}}, "ip": {"S": "189.73.20.218"}, "ttl": {"N": "1770493353"}, "sk": {"S": "2024-02-08T16:42:33.812060-03:00"}, "id": {"S": "log:5OxmMjL-ujoR5IMGegQz"}, "actor": {"M": {"name": {"S": "S\u00e9rgio Rafael Siqueira"}, "id": {"S": "5OxmMjL-ujoR5IMGegQz"}}}} -{"ip": {"S": "189.73.20.218"}, "ttl": {"N": "1770493367"}, "sk": {"S": "2024-02-08T16:42:47.095878-03:00"}, "action": {"S": "LOGIN"}, "id": {"S": "log:5OxmMjL-ujoR5IMGegQz"}} -{"action": {"S": "EMAIL_DEL"}, "data": {"M": {"email": {"S": "sergio+12121@somosbeta.com.br"}}}, "ip": {"S": "189.73.20.218"}, "ttl": {"N": "1770493382"}, "sk": {"S": "2024-02-08T16:43:02.024191-03:00"}, "id": {"S": "log:5OxmMjL-ujoR5IMGegQz"}, "actor": {"M": {"name": {"S": "S\u00e9rgio Rafael Siqueira"}, "id": {"S": "5OxmMjL-ujoR5IMGegQz"}}}} -{"ip": {"S": "189.73.20.218"}, "ttl": {"N": "1770507839"}, "sk": {"S": "2024-02-08T20:43:59.040766-03:00"}, "action": {"S": "LOGIN"}, "id": {"S": "log:5OxmMjL-ujoR5IMGegQz"}} -{"ip": {"S": "189.73.20.218"}, "ttl": {"N": "1770508260"}, "sk": {"S": "2024-02-08T20:51:00.933637-03:00"}, "action": {"S": "LOGIN"}, "id": {"S": "log:5OxmMjL-ujoR5IMGegQz"}} -{"ttl": {"N": "1770508425"}, "sk": {"S": "2024-02-08T20:53:45.329416-03:00"}, "action": {"S": "LOGIN"}, "id": {"S": "log:5OxmMjL-ujoR5IMGegQz"}} -{"id": {"S": "DbMaGtQX4wXPDZyBjdZMzh"}, "sk": {"S": "0"}, "createDate": {"S": "2023-09-22T18:34:46.517274-03:00"}, "email": {"S": "sergio@users.noreply.betaeducacao.com.br"}, "name": {"S": "S\u00e9rgio Rafael Siqueira"}, "status": {"S": "CREATED"}, "updateDate": {"S": "2024-02-26T14:58:50.688790-03:00"}} -{"id": {"S": "DbMaGtQX4wXPDZyBjdZMzh"}, "sk": {"S": "emails#sergio@users.noreply.betaeducacao.com.br"}, "createDate": {"S": "2024-02-26T14:45:04.980636-03:00"}, "deliverability": {"S": "DELIVERABLE"}, "emailVerified": {"BOOL": false}, "primaryEmail": {"BOOL": true}, "updateDate": {"S": "2024-02-26T14:51:30.188894-03:00"}} -{"id": {"S": "email"}, "sk": {"S": "sergio@users.noreply.betaeducacao.com.br"}, "createDate": {"S": "2019-11-12T00:00:00-03:00"}, "userRefId": {"S": "DbMaGtQX4wXPDZyBjdZMzh"}} -{"id": {"S": "5ad1d654-efe5-4bcf-8016-332677c4ba61"}, "sk": {"S": "0"}, "createDate": {"S": "2023-09-22T18:34:46.517274-03:00"}, "email": {"S": "vera@somosbeta.com.br"}, "name": {"S": "Vera L\u00facia Machado"}, "status": {"S": "CREATED"}, "updateDate": {"S": "2024-02-26T14:58:50.688790-03:00"}} -{"id": {"S": "5ad1d654-efe5-4bcf-8016-332677c4ba61"}, "sk": {"S": "emails#vera@somosbeta.com.br"}, "createDate": {"S": "2024-02-26T14:45:04.980636-03:00"}, "deliverability": {"S": "DELIVERABLE"}, "emailVerified": {"BOOL": false}, "primaryEmail": {"BOOL": true}, "updateDate": {"S": "2024-02-26T14:51:30.188894-03:00"}} -{"id": {"S": "email"}, "sk": {"S": "vera@somosbeta.com.br"}, "createDate": {"S": "2019-11-12T00:00:00-03:00"}, "userRefId": {"S": "5ad1d654-efe5-4bcf-8016-332677c4ba61"}} -{"updateDate": {"S": "2024-02-08T16:42:33.776409-03:00"}, "createDate": {"S": "2019-03-25T00:00:00-03:00"}, "status": {"S": "CONFIRMED"}, "sk": {"S": "0"}, "email": {"S": "maite@somosbeta.com.br"}, "id": {"S": "ZoV7w6mZsdAABjXzeAodSQ"}, "name": {"S": "Mait\u00ea Laurenti Siqueira"}, "lastLogin": {"S": "2024-02-08T20:53:45.818126-03:00"}, "tenant_id": {"L": [{"S": "cJtK9SsnJhKPyxESe7g3DG"}]}} -{"id": {"S": "5OxmMjL-ujoR5IMGegQz"}, "sk": {"S": "orgs#cJtK9SsnJhKPyxESe7g3DG"}, "cnpj": {"S": "15608435000190"}, "create_date": {"S": "2023-12-24T20:50:27.656310-03:00"}, "name": {"S": "Beta Educa\u00e7\u00e3o"}} -{"id": {"S": "5OxmMjL-ujoR5IMGegQz"}, "sk": {"S": "orgs#edp8njvgQuzNkLx2ySNfAD"}, "cnpj": {"S": "13573332000107"}, "create_date": {"S": "2023-12-24T20:50:27.656310-03:00"}, "name": {"S": "KORD S.A"}} -{"id": {"S": "5OxmMjL-ujoR5IMGegQz"}, "sk": {"S": "acls#cJtK9SsnJhKPyxESe7g3DG"}, "create_date": {"S": "2022-06-13T15:00:24.309410-03:00"}, "roles": {"L": [{"S": "ADMIN"}]}} -{"id": {"S": "5OxmMjL-ujoR5IMGegQz"}, "sk": {"S": "acls#8TVSi5oACLxTiT8ycKPmaQ"}, "create_date": {"S": "2022-06-13T15:00:24.309410-03:00"}, "roles": {"L": [{"S": "USER"}]}} -{"id": {"S": "5OxmMjL-ujoR5IMGegQz"}, "sk": {"S": "acls#*"}, "create_date": {"S": "2022-06-13T15:00:24.309410-03:00"}, "roles": {"L": [{"S": "ADMIN"}]}} -{"id": {"S": "5OxmMjL-ujoR5IMGegQz"}, "sk": {"S": "orgs#8TVSi5oACLxTiT8ycKPmaQ"}, "cnpj": {"S": "07556927000666"}, "create_date": {"S": "2023-12-24T20:50:27.656310-03:00"}, "name": {"S": "Ponsse Latin America Ind\u00fastria de M\u00e1quinas Florestais LTDA"}} -{"id": {"S": "9a41e867-55e1-4573-bd27-7b5d1d1bcfde"}, "sk": {"S": "orgs#cJtK9SsnJhKPyxESe7g3DG"}, "cnpj": {"S": "15608435000190"}, "create_date": {"S": "2023-12-24T20:50:27.656310-03:00"}, "name": {"S": "Beta Educa\u00e7\u00e3o"}} -{"id": {"S": "ZoV7w6mZsdAABjXzeAodSQ"}, "sk": {"S": "orgs#cJtK9SsnJhKPyxESe7g3DG"}, "cnpj": {"S": "15608435000190"}, "create_date": {"S": "2023-12-24T20:50:27.656310-03:00"}, "name": {"S": "Beta Educa\u00e7\u00e3o"}} -{"id": {"S": "ZoV7w6mZsdAABjXzeAodSQ"}, "sk": {"S": "orgs#edp8njvgQuzNkLx2ySNfAD"}, "cnpj": {"S": "13573332000107"}, "create_date": {"S": "2023-12-24T20:50:27.656310-03:00"}, "name": {"S": "KORD S.A"}} -{"id": {"S": "webhooks#*"}, "sk": {"S": "0e7f4d2e62ec525fc94465a6dd7299d2"}, "event_type": {"S": "insert"}, "resource": {"S": "users"}, "url": {"S": "https://n8n.sergio.run/webhook/56bb43b8-533c-4e8b-bdaa-3f7c2b0e548f"}, "author": {"M": {"id": {"S": "5OxmMjL-ujoR5IMGegQz"}, "name": {"S": "S\u00e9rgio R Siqueira"}}}, "create_date": {"S": "2025-01-03T15:52:47.378885+00:00"}} -{"id": {"S": "webhook_requests#*#0e7f4d2e62ec525fc94465a6dd7299d2"}, "sk": {"S": "2025-01-03T15:47:42.039256-03:00"}, "request": {"M": {"id": {"S": "i8vZVsyir5LVVN9RDZ4eCN"}, "headers": {"M": {"Accept": {"S": "*/*"}, "Accept-Encoding": {"S": "gzip, deflate"}, "Connection": {"S": "keep-alive"}, "Content-Length": {"S": "108"}, "Content-Type": {"S": "application/json"}, "User-Agent": {"S": "eduseg/python-requests/2.32.3"}}}, "payload": {"M": {"id": {"S": "5OxmMjL-ujoR5IMGegQz"}, "email": {"S": "sergio@somosbeta.com.br"}, "name": {"S": "S\u00e9rgio Rafael de Siqueira"}}}}}, "response": {"M": {"body": {"S": "{\"message\":\"Workflow was started\"}"}, "elapsed_time": {"S": "0.14"}, "headers": {"M": {"alt-svc": {"S": "h3=\":443\"; ma=86400"}, "cf-cache-status": {"S": "DYNAMIC"}, "CF-RAY": {"S": "8fc528b57b62a3f0-GRU"}, "Connection": {"S": "keep-alive"}, "Content-Length": {"S": "34"}, "Content-Type": {"S": "application/json; charset=utf-8"}, "Date": {"S": "Fri, 03 Jan 2025 18:47:44 GMT"}, "etag": {"S": "W/\"22-6OS7cK0FzqnV2NeDHdOSGS1bVUs\""}, "NEL": {"S": "{\"success_fraction\":0,\"report_to\":\"cf-nel\",\"max_age\":604800}"}, "Report-To": {"S": "{\"endpoints\":[{\"url\":\"https:\\/\\/a.nel.cloudflare.com\\/report\\/v4?s=FMkgKCTOnFvNgtE30yiYnM4XtK8q99O62s7Ep57KDNc3YGiy3W1j%2BzfS0vqJNylSAn5viU3MMGJvTIwPsYQ6jQ298t1p0hYd1KPwURhxchME4hc%2BsO0NNAv%2FFEpXv1ZYWw%3D%3D\"}],\"group\":\"cf-nel\",\"max_age\":604800}"}, "Server": {"S": "cloudflare"}, "server-timing": {"S": "cfL4;desc=\"?proto=TCP&rtt=1016&min_rtt=998&rtt_var=411&sent=4&recv=7&lost=0&retrans=0&sent_bytes=2836&recv_bytes=999&delivery_rate=2522648&cwnd=251&unsent_bytes=0&cid=1b26053952909423&ts=93&x=0\""}, "Strict-Transport-Security": {"S": "max-age=0; includeSubDomains"}, "vary": {"S": "Accept-Encoding"}}}, "status_code": {"S": "200"}}}, "update_date": {"S": "2025-01-03T15:47:44.537597-03:00"}, "url": {"S": "https://n8n.sergio.run/webhook/56bb43b8-533c-4e8b-bdaa-3f7c2b0e548f"}} -{"id": {"S": "webhook_requests#*#0e7f4d2e62ec525fc94465a6dd7299d2"}, "sk": {"S": "2025-03-03T15:47:42.039256-03:00"}, "request": {"M": {"id": {"S": "cJtK9SsnJhKPyxESe7g3DG"}, "headers": {"M": {"Accept": {"S": "*/*"}, "Accept-Encoding": {"S": "gzip, deflate"}, "Connection": {"S": "keep-alive"}, "Content-Length": {"S": "108"}, "Content-Type": {"S": "application/json"}, "User-Agent": {"S": "eduseg/python-requests/2.32.3"}}}, "payload": {"M": {"id": {"S": "5OxmMjL-ujoR5IMGegQz"}, "email": {"S": "sergio@somosbeta.com.br"}, "name": {"S": "S\u00e9rgio Rafael de Siqueira"}}}}}, "response": {"M": {"body": {"S": "{\"message\":\"Workflow was started\"}"}, "elapsed_time": {"S": "0.14"}, "headers": {"M": {"alt-svc": {"S": "h3=\":443\"; ma=86400"}, "cf-cache-status": {"S": "DYNAMIC"}, "CF-RAY": {"S": "8fc528b57b62a3f0-GRU"}, "Connection": {"S": "keep-alive"}, "Content-Length": {"S": "34"}, "Content-Type": {"S": "application/json; charset=utf-8"}, "Date": {"S": "Fri, 03 Jan 2025 18:47:44 GMT"}, "etag": {"S": "W/\"22-6OS7cK0FzqnV2NeDHdOSGS1bVUs\""}, "NEL": {"S": "{\"success_fraction\":0,\"report_to\":\"cf-nel\",\"max_age\":604800}"}, "Report-To": {"S": "{\"endpoints\":[{\"url\":\"https:\\/\\/a.nel.cloudflare.com\\/report\\/v4?s=FMkgKCTOnFvNgtE30yiYnM4XtK8q99O62s7Ep57KDNc3YGiy3W1j%2BzfS0vqJNylSAn5viU3MMGJvTIwPsYQ6jQ298t1p0hYd1KPwURhxchME4hc%2BsO0NNAv%2FFEpXv1ZYWw%3D%3D\"}],\"group\":\"cf-nel\",\"max_age\":604800}"}, "Server": {"S": "cloudflare"}, "server-timing": {"S": "cfL4;desc=\"?proto=TCP&rtt=1016&min_rtt=998&rtt_var=411&sent=4&recv=7&lost=0&retrans=0&sent_bytes=2836&recv_bytes=999&delivery_rate=2522648&cwnd=251&unsent_bytes=0&cid=1b26053952909423&ts=93&x=0\""}, "Strict-Transport-Security": {"S": "max-age=0; includeSubDomains"}, "vary": {"S": "Accept-Encoding"}}}, "status_code": {"S": "200"}}}, "update_date": {"S": "2025-01-03T15:47:44.537597-03:00"}, "url": {"S": "https://n8n.sergio.run/webhook/56bb43b8-533c-4e8b-bdaa-3f7c2b0e548f"}} -{"id": {"S": "webhooks#*"}, "sk": {"S": "1e3759c9c4cfa7aaf86ac281bdb8fd6f"}, "author": {"M": {"id": {"S": "5OxmMjL-ujoR5IMGegQz"}, "name": {"S": "S\u00e9rgio Rafael de Siqueira"}}}, "create_date": {"S": "2025-01-06T15:00:39.363207-03:00"}, "event_type": {"S": "insert"}, "resource": {"S": "users"}, "url": {"S": "https://hook.us2.make.com/hgkc5oj5dbpfld5qvu1xmsu22ond93l9"}} -{"id": {"S": "log:5OxmMjL-ujoR5IMGegQz"},"sk": {"S": "2024-02-26T11:48:17.605911-03:00"},"action": {"S": "OPEN_EMAIL"},"data": {"M": {"ip_address": {"S": "66.249.83.73"},"message_id": {"S": "0103018de5de75cf-670bf01f-7ccf-4ce0-88b4-7c85cd0d06d0-000000"},"recipients": {"L": [{"S": "sergio@somosbeta.com.br"}]},"subject": {"S": "Re: (sem assunto)"},"timestamp": {"S": "2024-02-26T14:48:16.976Z"},"user_agent": {"S": "Mozilla/5.0 (Windows NT 5.1; rv:11.0) Gecko Firefox/11.0 (via ggpht.com GoogleImageProxy)"}}},"ttl": {"N": "1772030897"}} -{"id": {"S": "7zf52CWrTS3csRBFWU5rkq"},"sk": {"S": "0"},"cognito:sub": {"S": "58efed8d-d276-41a8-8502-4ab8b5a6415e"},"cpf": {"S": "04330965275"},"createDate": {"S": "2025-04-08T10:24:46.493980-03:00"},"email": {"S": "barbara.gomes@sinobras.com.br"},"email_verified": {"BOOL": true},"konviva:id": {"N": "199205"},"lastLogin": {"S": "2025-04-10T12:01:48.380215-03:00"},"name": {"S": "Barbara Kamyla Vasconcelos Gomes"},"tenant_id": {"SS": ["EkvQwpmmL6vzWtJunM5dCJ"]},"update_date": {"S": "2025-04-10T08:50:22.530758-03:00"}} -{"id": {"S": "7zf52CWrTS3csRBFWU5rkq"},"sk": {"S": "acls#EkvQwpmmL6vzWtJunM5dCJ"},"create_date": {"S": "2025-04-10T09:36:41.133157-03:00"},"roles": {"L": [{"S": "ADMIN"}]}} -{"id": {"S": "7zf52CWrTS3csRBFWU5rkq"},"sk": {"S": "orgs#EkvQwpmmL6vzWtJunM5dCJ"},"cnpj": {"S": "07933914000154"},"create_date": {"S": "2025-04-08T10:24:46.493980-03:00"},"name": {"S": "SIDERURGICA NORTE BRASIL S.A"}} -{"id": {"S": "edp8njvgQuzNkLx2ySNfAD"},"sk": {"S": "metadata#billing_policy"},"billing_day": {"N": "1"},"created_at": {"S": "2025-07-23T13:56:42.794693-03:00"},"payment_method": {"S": "MANUAL"}} \ No newline at end of file +// Post-migration: rename `create_date` to `created_at` + +// Users +{"id": "5OxmMjL-ujoR5IMGegQz", "sk": "0", "name": "Sérgio R Siqueira", "cpf": "07879819908", "email": "sergio@somosbeta.com.br", "email_verified": true, "emails": ["osergiosiqueira@gmail.com"], "lastLogin": "2025-08-14T14:36:38.758274-03:00", "tenant_id": ["cJtK9SsnJhKPyxESe7g3DG"], "cognito__sub": "58efed8d-d276-41a8-8502-4ab8b5a6415e"} +{"id": "5OxmMjL-ujoR5IMGegQz", "sk": "emails#sergio@somosbeta.com.br", "create_date": "2019-03-25T00:00:00-03:00", "email_primary": true, "email_verified": true, "mx_record_exists": true, "update_date": "2025-04-14T13:29:02.380381-03:00"} +{"id": "5OxmMjL-ujoR5IMGegQz", "sk": "emails#osergiosiqueira@gmail.com", "create_date": "2019-03-25T00:00:00-03:00", "email_primary": false, "email_verified": true, "mx_record_exists": true, "update_date": "2025-04-14T13:29:02.380381-03:00"} +{"id": "5OxmMjL-ujoR5IMGegQz", "sk": "orgs#cJtK9SsnJhKPyxESe7g3DG", "name": "EDUSEG", "cnpj": "15608435000190"} +{"id": "5OxmMjL-ujoR5IMGegQz", "sk": "acls#*", "roles": ["ADMIN"]} + +// CPFs +{"id": "cpf", "sk": "07879819908", "user_id": "5OxmMjL-ujoR5IMGegQz"} + +// Emails +{"id": "email", "sk": "sergio@somosbeta.com.br", "user_id": "5OxmMjL-ujoR5IMGegQz"} +{"id": "email", "sk": "osergiosiqueira@gmail.com", "user_id": "5OxmMjL-ujoR5IMGegQz"} + +// Orgs +{"id": "cJtK9SsnJhKPyxESe7g3DG", "sk": "0", "name": "EDUSEG", "cnpj": "15608435000190", "email": "org+15608435000190@users.noreply.betaeducacao.com.br"} +{"id": "edp8njvgQuzNkLx2ySNfAD", "sk": "0", "name": "KORD S.A", "email": "org+13573332000107@users.noreply.betaeducacao.com.br", "cnpj": "13573332000107"} + +// Org admins +{"id": "cJtK9SsnJhKPyxESe7g3DG", "sk": "admins#5OxmMjL-ujoR5IMGegQz", "name": "Sérgio Rafael Siqueira", "email": "sergio@somosbeta.com.br", "create_date": "2025-03-14T10:06:34.628078-03:00"} + +// Org members +{"id": "orgmembers#cJtK9SsnJhKPyxESe7g3DG", "sk": "5OxmMjL-ujoR5IMGegQz"} + +// CNPJs +{"id": "cnpj", "sk": "15608435000190", "user_id": "cJtK9SsnJhKPyxESe7g3DG"} \ No newline at end of file diff --git a/http-api/tests/conftest.py b/http-api/tests/conftest.py index 475dfa9..dc875ec 100644 --- a/http-api/tests/conftest.py +++ b/http-api/tests/conftest.py @@ -144,9 +144,14 @@ def dynamodb_persistence_layer(dynamodb_client): @pytest.fixture() def dynamodb_seeds(dynamodb_client): - with jsonlines.open('tests/seeds.jsonl') as lines: - for line in lines: - dynamodb_client.put_item(TableName=PYTEST_TABLE_NAME, Item=line) + with open('tests/seeds.jsonl', 'rb') as fp: + reader = jsonlines.Reader(fp) + + for line in reader.iter(type=dict, skip_invalid=True): + dynamodb_client.put_item( + TableName=PYTEST_TABLE_NAME, + Item=line, + ) @pytest.fixture diff --git a/http-api/tests/routes/orgs/__init__.py b/http-api/tests/routes/orgs/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/http-api/tests/routes/orgs/test_admins.py b/http-api/tests/routes/orgs/test_admins.py new file mode 100644 index 0000000..13393c0 --- /dev/null +++ b/http-api/tests/routes/orgs/test_admins.py @@ -0,0 +1,57 @@ +from http import HTTPMethod, HTTPStatus + +from layercake.dynamodb import DynamoDBPersistenceLayer, KeyPair + +from ...conftest import HttpApiProxy, LambdaContext + + +def test_post_admins( + mock_app, + dynamodb_seeds, + dynamodb_persistence_layer: DynamoDBPersistenceLayer, + http_api_proxy: HttpApiProxy, + lambda_context: LambdaContext, +): + r = mock_app.lambda_handler( + http_api_proxy( + raw_path='/orgs/cJtK9SsnJhKPyxESe7g3DG/admins', + method=HTTPMethod.POST, + body={ + 'id': '15ee05a3-4ceb-4b7e-9979-db75b28c9ade', + 'name': 'Sérgio R Siqueira', + 'email': 'sergio@somosbeta.com.br', + }, + ), + lambda_context, + ) + assert r['statusCode'] == HTTPStatus.CREATED + + data = dynamodb_persistence_layer.collection.query( + KeyPair('cJtK9SsnJhKPyxESe7g3DG', 'admins'), + ) + assert len(data['items']) == 2 + + +def test_delete_admins( + mock_app, + dynamodb_seeds, + dynamodb_persistence_layer: DynamoDBPersistenceLayer, + http_api_proxy: HttpApiProxy, + lambda_context: LambdaContext, +): + r = mock_app.lambda_handler( + http_api_proxy( + raw_path='/orgs/cJtK9SsnJhKPyxESe7g3DG/admins', + method=HTTPMethod.DELETE, + body={ + 'user_id': 'e170d457-bd6b-475d-b6ae-4f3427a04873', + }, + ), + lambda_context, + ) + assert r['statusCode'] == HTTPStatus.OK + + data = dynamodb_persistence_layer.collection.query( + KeyPair('cJtK9SsnJhKPyxESe7g3DG', 'admins'), + ) + assert len(data['items']) == 0 diff --git a/http-api/tests/routes/test_users.py b/http-api/tests/routes/test_users.py index 371d0f9..df83c4c 100644 --- a/http-api/tests/routes/test_users.py +++ b/http-api/tests/routes/test_users.py @@ -191,91 +191,3 @@ def test_post_user( ) assert r['statusCode'] == HTTPStatus.CREATED - - -def test_post_email( - mock_app, - dynamodb_client, - dynamodb_seeds, - dynamodb_persistence_layer: DynamoDBPersistenceLayer, - http_api_proxy: HttpApiProxy, - lambda_context: LambdaContext, -): - r = mock_app.lambda_handler( - http_api_proxy( - raw_path='/users/5OxmMjL-ujoR5IMGegQz/emails', - method=HTTPMethod.POST, - body={ - 'email': 'sergio+pytest@somosbeta.com.br', - }, - ), - lambda_context, - ) - - assert r['statusCode'] == HTTPStatus.CREATED - - user = dynamodb_persistence_layer.collection.get_item( - KeyPair('5OxmMjL-ujoR5IMGegQz', '0') - ) - assert user['emails'] == { - 'sergio@somosbeta.com.br', - 'osergiosiqueira@gmail.com', - 'sergio+pytest@somosbeta.com.br', - } - - -def test_patch_email( - mock_app, - dynamodb_client, - dynamodb_seeds, - dynamodb_persistence_layer: DynamoDBPersistenceLayer, - http_api_proxy: HttpApiProxy, - lambda_context: LambdaContext, -): - r = mock_app.lambda_handler( - http_api_proxy( - raw_path='/users/5OxmMjL-ujoR5IMGegQz/emails', - method=HTTPMethod.PATCH, - body={ - 'old_email': 'sergio@somosbeta.com.br', - 'new_email': 'osergiosiqueira@gmail.com', - }, - ), - lambda_context, - ) - - assert r['statusCode'] == HTTPStatus.OK - - user = dynamodb_persistence_layer.collection.get_item( - KeyPair('5OxmMjL-ujoR5IMGegQz', '0') - ) - assert user['email'] == 'osergiosiqueira@gmail.com' - - -def test_delete_email( - mock_app, - dynamodb_client, - dynamodb_seeds, - dynamodb_persistence_layer: DynamoDBPersistenceLayer, - http_api_proxy: HttpApiProxy, - lambda_context: LambdaContext, -): - r = mock_app.lambda_handler( - http_api_proxy( - raw_path='/users/5OxmMjL-ujoR5IMGegQz/emails', - method=HTTPMethod.DELETE, - body={ - 'email': 'osergiosiqueira@gmail.com', - }, - ), - lambda_context, - ) - - assert r['statusCode'] == HTTPStatus.OK - - user = dynamodb_persistence_layer.collection.get_item( - KeyPair('5OxmMjL-ujoR5IMGegQz', '0') - ) - assert user['emails'] == { - 'sergio@somosbeta.com.br', - } diff --git a/http-api/tests/routes/users/__init__.py b/http-api/tests/routes/users/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/http-api/tests/routes/users/test_add.py b/http-api/tests/routes/users/test_add.py new file mode 100644 index 0000000..a99abde --- /dev/null +++ b/http-api/tests/routes/users/test_add.py @@ -0,0 +1,78 @@ +import json +from http import HTTPMethod, HTTPStatus + +from layercake.dynamodb import DynamoDBPersistenceLayer, PartitionKey + +from ...conftest import HttpApiProxy, LambdaContext + + +def test_add( + mock_app, + dynamodb_seeds, + dynamodb_persistence_layer: DynamoDBPersistenceLayer, + http_api_proxy: HttpApiProxy, + lambda_context: LambdaContext, +): + org_id = 'cJtK9SsnJhKPyxESe7g3DG' + r = mock_app.lambda_handler( + http_api_proxy( + raw_path='/users', + method=HTTPMethod.POST, + body={ + 'name': 'Eddie Van Halen', + 'email': 'eddie@vanhalen.com', + 'cpf': '12974982085', + 'org': { + 'id': org_id, + 'name': 'EDUSEG', + 'cnpj': '15608435000190', + }, + }, + ), + lambda_context, + ) + user = json.loads(r['body']) + + assert r['statusCode'] == HTTPStatus.CREATED + + r = dynamodb_persistence_layer.collection.query(PartitionKey(user['id'])) + assert len(r['items']) == 2 + + r = dynamodb_persistence_layer.collection.query( + PartitionKey(f'orgmembers#{org_id}') + ) + # 2 items were added from the seed + assert len(r['items']) == 3 + + +def test_add_existing_user( + mock_app, + dynamodb_seeds, + dynamodb_persistence_layer: DynamoDBPersistenceLayer, + http_api_proxy: HttpApiProxy, + lambda_context: LambdaContext, +): + org_id = 'cJtK9SsnJhKPyxESe7g3DG' + r = mock_app.lambda_handler( + http_api_proxy( + raw_path='/users', + method=HTTPMethod.POST, + body={ + 'name': 'Sérgio Rafael Siqueira', + 'email': 'sergio@somosbeta.com.br', + 'cpf': '07879819908', + 'org': { + 'id': org_id, + 'name': 'EDUSEG', + 'cnpj': '15608435000190', + }, + }, + ), + lambda_context, + ) + + r = dynamodb_persistence_layer.collection.query( + PartitionKey(f'orgmembers#{org_id}') + ) + # 2 items were added from the seed + assert len(r['items']) == 3 diff --git a/http-api/tests/routes/users/test_emails.py b/http-api/tests/routes/users/test_emails.py new file mode 100644 index 0000000..aa4b177 --- /dev/null +++ b/http-api/tests/routes/users/test_emails.py @@ -0,0 +1,93 @@ +from http import HTTPMethod, HTTPStatus + +from layercake.dynamodb import ( + DynamoDBPersistenceLayer, + KeyPair, +) + +from ...conftest import HttpApiProxy, LambdaContext + + +def test_add_email( + mock_app, + dynamodb_seeds, + dynamodb_persistence_layer: DynamoDBPersistenceLayer, + http_api_proxy: HttpApiProxy, + lambda_context: LambdaContext, +): + r = mock_app.lambda_handler( + http_api_proxy( + raw_path='/users/5OxmMjL-ujoR5IMGegQz/emails', + method=HTTPMethod.POST, + body={ + 'email': 'sergio+pytest@somosbeta.com.br', + }, + ), + lambda_context, + ) + + assert r['statusCode'] == HTTPStatus.CREATED + + user = dynamodb_persistence_layer.collection.get_item( + KeyPair('5OxmMjL-ujoR5IMGegQz', '0') + ) + assert user['emails'] == { + 'sergio@somosbeta.com.br', + 'osergiosiqueira@gmail.com', + 'sergio+pytest@somosbeta.com.br', + } + + +def test_update_email( + mock_app, + dynamodb_seeds, + dynamodb_persistence_layer: DynamoDBPersistenceLayer, + http_api_proxy: HttpApiProxy, + lambda_context: LambdaContext, +): + r = mock_app.lambda_handler( + http_api_proxy( + raw_path='/users/5OxmMjL-ujoR5IMGegQz/emails', + method=HTTPMethod.PATCH, + body={ + 'old_email': 'sergio@somosbeta.com.br', + 'new_email': 'osergiosiqueira@gmail.com', + }, + ), + lambda_context, + ) + + assert r['statusCode'] == HTTPStatus.OK + + user = dynamodb_persistence_layer.collection.get_item( + KeyPair('5OxmMjL-ujoR5IMGegQz', '0') + ) + assert user['email'] == 'osergiosiqueira@gmail.com' + + +def test_remove_email( + mock_app, + dynamodb_seeds, + dynamodb_persistence_layer: DynamoDBPersistenceLayer, + http_api_proxy: HttpApiProxy, + lambda_context: LambdaContext, +): + r = mock_app.lambda_handler( + http_api_proxy( + raw_path='/users/5OxmMjL-ujoR5IMGegQz/emails', + method=HTTPMethod.DELETE, + body={ + 'email': 'osergiosiqueira@gmail.com', + }, + ), + lambda_context, + ) + + assert r['statusCode'] == HTTPStatus.OK + + user = dynamodb_persistence_layer.collection.get_item( + KeyPair('5OxmMjL-ujoR5IMGegQz', '0') + ) + assert user['emails'] == { + 'sergio@somosbeta.com.br', + } diff --git a/http-api/tests/routes/users/test_orgs.py b/http-api/tests/routes/users/test_orgs.py new file mode 100644 index 0000000..e69de29 diff --git a/http-api/tests/seeds.jsonl b/http-api/tests/seeds.jsonl index a76ec6a..65afa0f 100644 --- a/http-api/tests/seeds.jsonl +++ b/http-api/tests/seeds.jsonl @@ -1,14 +1,7 @@ {"id": {"S": "apikey"}, "sk": {"S": "MzI1MDQ0NTctZjEzMy00YzAwLTkzNmItNmFhNzEyY2E5ZjQw"}, "tenant": {"M": {"id": {"S": "*"}, "name": {"S": "default"}}}, "user": {"M": {"id": {"S": "5OxmMjL-ujoR5IMGegQz"}, "name": {"S": "Sérgio R Siqueira"}, "email": {"S": "sergio@somosbeta.com.br"}}}} -{"id": {"S": "5OxmMjL-ujoR5IMGegQz"}, "sk": {"S": "0"}, "update_date": {"S": "2024-02-08T16:42:33.776409-03:00"}, "create_date": {"S": "2019-03-25T00:00:00-03:00"}, "email_verified": {"BOOL": true}, "cognito:sub": {"S": "58efed8d-d276-41a8-8502-4ab8b5a6415e"}, "cpf": {"S": "07879819908"}, "email": {"S": "sergio@somosbeta.com.br"}, "name": {"S": "S\u00e9rgio Rafael de Siqueira"}, "last_login": {"S": "2024-02-08T20:53:45.818126-03:00"}, "emails": {"SS": ["sergio@somosbeta.com.br","osergiosiqueira@gmail.com"]}, "tenant:org_id": {"L": [{"S": "cJtK9SsnJhKPyxESe7g3DG"}]}} -{"id": {"S": "5OxmMjL-ujoR5IMGegQz"}, "sk": {"S": "cognito"}, "create_date": {"S": "2025-03-03T17:12:26.443507-03:00"}, "sub": {"S": "58efed8d-d276-41a8-8502-4ab8b5a6415e"}} -{"id": {"S": "5OxmMjL-ujoR5IMGegQz"}, "sk": {"S": "emails#sergio@somosbeta.com.br"}, "email_verified": {"BOOL": true}, "update_date": {"S": "2024-02-08T16:42:33.776409-03:00"}, "create_date": {"S": "2019-03-25T00:00:00-03:00"}, "email_primary": {"BOOL": true}, "mx_record_exists": {"BOOL": true}, "update_date": {"S": "2023-11-09T12:13:04.308986-03:00"}} -{"id": {"S": "5OxmMjL-ujoR5IMGegQz"}, "sk": {"S": "emails#osergiosiqueira@gmail.com"}, "email_verified": {"BOOL": true}, "update_date": {"S": "2024-02-08T16:42:33.776409-03:00"}, "create_date": {"S": "2019-03-25T00:00:00-03:00"}, "email_primary": {"BOOL": false}, "mx_record_exists": {"BOOL": true}, "update_date": {"S": "2023-11-09T12:13:04.308986-03:00"}} -{"id": {"S": "5OxmMjL-ujoR5IMGegQz"}, "sk": {"S": "acls#*"}, "create_date": {"S": "2022-06-13T15:00:24.309410-03:00"}, "roles": {"L": [{"S": "ADMIN"}]}} -{"id": {"S": "5OxmMjL-ujoR5IMGegQz"}, "sk": {"S": "acls#cJtK9SsnJhKPyxESe7g3DG"}, "create_date": {"S": "2025-03-14T10:06:34.628078-03:00"}, "roles": {"L": [{"S": "ADMIN"}]}} -{"id": {"S": "5OxmMjL-ujoR5IMGegQz"}, "sk": {"S": "orgs#cJtK9SsnJhKPyxESe7g3DG"}, "cnpj": {"S": "15608435000190"}, "create_date": {"S": "2025-03-13T16:36:50.073156-03:00"}, "name": {"S": "Beta Educação"}} {"id": {"S": "log:5OxmMjL-ujoR5IMGegQz"}, "sk": {"S": "2024-02-08T16:42:33.776409-03:00"}, "action": {"S": "OPEN_EMAIL"}} {"id": {"S": "log:5OxmMjL-ujoR5IMGegQz"}, "sk": {"S": "2019-03-25T00:00:00-03:00"}, "action": {"S": "CLICK_EMAIL"}} -{"id": {"S": "cJtK9SsnJhKPyxESe7g3DG"}, "sk": {"S": "0"}, "name": {"S": "EDUSEG"}, "cnpj": {"S": "15608435000190"}, "email": {"S": "org+15608435000190@users.noreply.betaeducacao.com.br"}} + {"id": {"S": "cJtK9SsnJhKPyxESe7g3DG"}, "sk": {"S": "metadata#payment_policy"}, "due_days": {"N": "90"}} {"id": {"S": "cJtK9SsnJhKPyxESe7g3DG"}, "sk": {"S": "metadata#billing_policy"}, "billing_day": {"N": "1"}, "payment_method": {"S": "PIX"}} {"id": {"S": "cJtK9SsnJhKPyxESe7g3DG"}, "sk": {"S": "metadata#address"}, "address1": {"S": "Av. Presidente Kennedy, 815"}, "address2": {"S": "Sala 1"}, "city": {"S": "São José"}, "state": {"S": "SC"}, "postcode": {"S": "88101001"}, "neighborhood": {"S": "Campinas"}} @@ -19,9 +12,7 @@ {"id": {"S": "43ea4475-c369-4f90-b576-135b7df5106b"}, "sk": {"S": "lock"}, "create_date": {"S": "2024-11-04T16:27:37.042051-03:00"}, "hash": {"S": "f8f7996aa99d50eb85266be5a9fca1db"}, "ttl": {"N": "1759692457"}, "ttl_date": {"S": "2025-10-05T16:27:37.042051-03:00"}} {"id": {"S": "QV4sXY3DvSTUMGJ4QqsrwJ"}, "sk": {"S": "0"}} {"id": {"S": "QV4sXY3DvSTUMGJ4QqsrwJ"}, "sk": {"S": "generated_items#43ea4475-c369-4f90-b576-135b7df5106b"}} -{"id": {"S": "email"}, "sk": {"S": "sergio@somosbeta.com.br"}} -{"id": {"S": "cpf"}, "sk": {"S": "07879819908"}} -{"id": {"S": "cpf"}, "sk": {"S": "08679004901"}} + {"id": {"S": "lock"}, "sk": {"S": "c2116a43f8f1aed659a10c83dab17ed3"}} {"id": {"S": "vacancies#cJtK9SsnJhKPyxESe7g3DG"}, "sk": {"S": "3CNrFB9dy2RLit2pdeUWy4#8c9b55ef-e988-43ee-b2da-8594850605d7"}} {"payment_method": {"S": "CREDIT_CARD"}, "status": {"S": "PAID"}, "assignee": {"M": {"name": {"S": "Alessandra Larivia"}}}, "total": {"N": "149"}, "installments": {"N": "1"}, "due_date": {"S": "2024-02-08T08:46:59.771233-03:00"}, "email": {"S": "financeiro@aquanobile.com.br"}, "name": {"S": "AQUA NOBILE SERVI\u00c7OS LTDA"}, "create_date": {"S": "2024-02-08T08:41:59.773135-03:00"}, "payment_date": {"S": "2024-02-08T08:42:10.910170-03:00"}, "phone_number": {"S": "+553130705599"}, "sk": {"S": "0"}, "cnpj": {"S": "11278500000106"}, "id": {"S": "KpZTYvu4RzgMJW3A2DF6cC"}, "update_date": {"S": "2024-02-08T08:42:10.910170-03:00"}, "tenant": {"S": "cJtK9SsnJhKPyxESe7g3DG"}} @@ -34,9 +25,39 @@ {"sk": {"S": "nfse"}, "nfse": {"S": "10384"}, "id": {"S": "KpZTYvu4RzgMJW3A2DF6cC"}, "create_date": {"S": "2024-02-08T09:05:03.879692-03:00"}} {"sk": {"S": "user"}, "user_id": {"S": "5AZXXXCWa2bU4spsxfLznx"}, "id": {"S": "KpZTYvu4RzgMJW3A2DF6cC"}, "create_date": {"S": "2024-02-08T08:42:05.190415-03:00"}} {"id": {"S": "15ee05a3-4ceb-4b7e-9979-db75b28c9ade"}, "sk": {"S": "0"}, "name": {"S": "pytest"}} +{"id": {"S": "edp8njvgQuzNkLx2ySNfAD"},"sk": {"S": "metadata#billing_policy"},"billing_day": {"N": "1"},"created_at": {"S": "2025-07-23T13:56:42.794693-03:00"},"payment_method": {"S": "MANUAL"}} + +// User +{"id": {"S": "5OxmMjL-ujoR5IMGegQz"}, "sk": {"S": "0"}, "update_date": {"S": "2024-02-08T16:42:33.776409-03:00"}, "create_date": {"S": "2019-03-25T00:00:00-03:00"}, "email_verified": {"BOOL": true}, "cognito:sub": {"S": "58efed8d-d276-41a8-8502-4ab8b5a6415e"}, "cpf": {"S": "07879819908"}, "email": {"S": "sergio@somosbeta.com.br"}, "name": {"S": "S\u00e9rgio Rafael de Siqueira"}, "last_login": {"S": "2024-02-08T20:53:45.818126-03:00"}, "emails": {"SS": ["sergio@somosbeta.com.br","osergiosiqueira@gmail.com"]}, "tenant:org_id": {"L": [{"S": "cJtK9SsnJhKPyxESe7g3DG"}]}} +{"id": {"S": "5OxmMjL-ujoR5IMGegQz"}, "sk": {"S": "cognito"}, "create_date": {"S": "2025-03-03T17:12:26.443507-03:00"}, "sub": {"S": "58efed8d-d276-41a8-8502-4ab8b5a6415e"}} +{"id": {"S": "5OxmMjL-ujoR5IMGegQz"}, "sk": {"S": "emails#sergio@somosbeta.com.br"}, "email_verified": {"BOOL": true}, "update_date": {"S": "2024-02-08T16:42:33.776409-03:00"}, "create_date": {"S": "2019-03-25T00:00:00-03:00"}, "email_primary": {"BOOL": true}, "mx_record_exists": {"BOOL": true}, "update_date": {"S": "2023-11-09T12:13:04.308986-03:00"}} +{"id": {"S": "5OxmMjL-ujoR5IMGegQz"}, "sk": {"S": "emails#osergiosiqueira@gmail.com"}, "email_verified": {"BOOL": true}, "update_date": {"S": "2024-02-08T16:42:33.776409-03:00"}, "create_date": {"S": "2019-03-25T00:00:00-03:00"}, "email_primary": {"BOOL": false}, "mx_record_exists": {"BOOL": true}, "update_date": {"S": "2023-11-09T12:13:04.308986-03:00"}} +{"id": {"S": "5OxmMjL-ujoR5IMGegQz"}, "sk": {"S": "acls#*"}, "create_date": {"S": "2022-06-13T15:00:24.309410-03:00"}, "roles": {"L": [{"S": "ADMIN"}]}} +{"id": {"S": "5OxmMjL-ujoR5IMGegQz"}, "sk": {"S": "acls#cJtK9SsnJhKPyxESe7g3DG"}, "create_date": {"S": "2025-03-14T10:06:34.628078-03:00"}, "roles": {"L": [{"S": "ADMIN"}]}} +{"id": {"S": "5OxmMjL-ujoR5IMGegQz"}, "sk": {"S": "orgs#cJtK9SsnJhKPyxESe7g3DG"}, "cnpj": {"S": "15608435000190"}, "create_date": {"S": "2025-03-13T16:36:50.073156-03:00"}, "name": {"S": "Beta Educação"}} + +// Email +{"id": {"S": "email"}, "sk": {"S": "sergio@somosbeta.com.br"}, "user_id": {"S": "5OxmMjL-ujoR5IMGegQz"}} + +// CPF +{"id": {"S": "cpf"}, "sk": {"S": "07879819908"}, "user_id": {"S": "5OxmMjL-ujoR5IMGegQz"}} +{"id": {"S": "cpf"}, "sk": {"S": "08679004901"}} + +// Custom pricing {"id": {"S": "CUSTOM_PRICING#ORG#cJtK9SsnJhKPyxESe7g3DG"}, "sk": {"S": "COURSE#281198c2-f293-4acc-b96e-e4a2d5f6b73c"}, "unit_price": {"N": "199"}} + +// Billing {"id": {"S": "BILLING#ORG#edp8njvgQuzNkLx2ySNfAD"},"sk": {"S": "START#2025-07-01#END#2025-07-31"},"created_at": {"S": "2025-07-24T15:46:43.312549-03:00"},"status": {"S": "PENDING"}} {"id": {"S": "BILLING#ORG#edp8njvgQuzNkLx2ySNfAD"},"sk": {"S": "START#2025-07-01#END#2025-07-31#ENROLLMENT#556e99cf-18b2-459c-a46d-f71a807ba551"},"author": {"M": {"id": {"S": "SMEXYk5MQkKCzknJpxqr8n"},"name": {"S": "Carolina Brand"}}},"course": {"M": {"id": {"S": "5c119d4b-573c-4d8d-a99d-63756af2f4c5"},"name": {"S": "NR-06 - Equipamento de Proteção Individual - EPI"}}},"created_at": {"S": "2025-07-24T16:42:41.673797-03:00"},"enrolled_at": {"S": "2025-07-24T15:46:37.162960-03:00"},"unit_price": {"N": "79.2"},"user": {"M": {"id": {"S": "02157895558"},"name": {"S": "ERICK ALVES DOS SANTOS"}}}} {"id": {"S": "BILLING#ORG#edp8njvgQuzNkLx2ySNfAD"},"sk": {"S": "START#2025-07-01#END#2025-07-31#ENROLLMENT#d2124d5a-caaf-4e27-9edb-8380faf15f35"},"author": {"M": {"id": {"S": "SMEXYk5MQkKCzknJpxqr8n"},"name": {"S": "Carolina Brand"}}},"course": {"M": {"id": {"S": "a810dd22-56c0-4d9b-8cd2-7e2ee9c45839"},"name": {"S": "NR-11 – Transporte, movimentação, armazenagem e manuseio de materiais"}}},"created_at": {"S": "2025-07-25T03:31:17.306858-03:00"},"enrolled_at": {"S": "2025-07-25T03:31:11.736247-03:00"},"unit_price": {"N": "87.2"},"user": {"M": {"id": {"S": "02157895558"},"name": {"S": "ERICK ALVES DOS SANTOS"}}}} {"id": {"S": "BILLING#ORG#edp8njvgQuzNkLx2ySNfAD"},"sk": {"S": "START#2025-07-01#END#2025-07-31#SCHEDULE#AUTO_CLOSE"},"created_at": {"S": "2025-07-24T15:46:43.312549-03:00"},"ttl": {"N": "1754017200"}} -{"id": {"S": "edp8njvgQuzNkLx2ySNfAD"},"sk": {"S": "metadata#billing_policy"},"billing_day": {"N": "1"},"created_at": {"S": "2025-07-23T13:56:42.794693-03:00"},"payment_method": {"S": "MANUAL"}} \ No newline at end of file + +// Org +{"id": {"S": "cJtK9SsnJhKPyxESe7g3DG"}, "sk": {"S": "0"}, "name": {"S": "EDUSEG"}, "cnpj": {"S": "15608435000190"}, "email": {"S": "org+15608435000190@users.noreply.betaeducacao.com.br"}} + +// Org admins +{"id": {"S": "cJtK9SsnJhKPyxESe7g3DG"}, "sk": {"S": "admins#e170d457-bd6b-475d-b6ae-4f3427a04873"}, "name": {"S": "Jimi Hendrix"}, "email": {"S": "jimi@hendrix.com"}} + +// Org members +{"id": {"S": "orgmembers#cJtK9SsnJhKPyxESe7g3DG"}, "sk": {"S": "15ee05a3-4ceb-4b7e-9979-db75b28c9ade"}} +{"id": {"S": "orgmembers#cJtK9SsnJhKPyxESe7g3DG"}, "sk": {"S": "e170d457-bd6b-475d-b6ae-4f3427a04873"}} \ No newline at end of file diff --git a/http-api/uv.lock b/http-api/uv.lock index 36659af..bccd5fa 100644 --- a/http-api/uv.lock +++ b/http-api/uv.lock @@ -517,7 +517,7 @@ wheels = [ [[package]] name = "layercake" -version = "0.9.8" +version = "0.9.9" source = { directory = "../layercake" } dependencies = [ { name = "arnparse" }, diff --git a/layercake/layercake/dynamodb.py b/layercake/layercake/dynamodb.py index bc2e334..75e096a 100644 --- a/layercake/layercake/dynamodb.py +++ b/layercake/layercake/dynamodb.py @@ -33,7 +33,7 @@ serializer = TypeSerializer() deserializer = TypeDeserializer() -def _serialize_to_basic_types(data: Any) -> str | dict | list: +def _serialize_to_basic_types(data: Any) -> str | dict | set: match data: case datetime(): return data.isoformat() @@ -41,8 +41,8 @@ def _serialize_to_basic_types(data: Any) -> str | dict | list: return str(data) case IPv4Address(): return str(data) - case tuple(): - return [_serialize_to_basic_types(v) for v in data] + case tuple() | list(): + return set(_serialize_to_basic_types(v) for v in data) case dict(): return {k: _serialize_to_basic_types(v) for k, v in data.items()} case _: diff --git a/layercake/pyproject.toml b/layercake/pyproject.toml index b7fa75f..787af0c 100644 --- a/layercake/pyproject.toml +++ b/layercake/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "layercake" -version = "0.9.8" +version = "0.9.9" description = "Packages shared dependencies to optimize deployment and ensure consistency across functions." readme = "README.md" authors = [ diff --git a/layercake/template.yaml b/layercake/template.yaml index ac83b8f..def7df0 100644 --- a/layercake/template.yaml +++ b/layercake/template.yaml @@ -16,7 +16,7 @@ Resources: CompatibleRuntimes: - python3.12 - python3.13 - RetentionPolicy: Retain + RetentionPolicy: Delete Metadata: BuildMethod: python3.13 BuildArchitecture: x86_64 diff --git a/streams-events/app/meili.py b/streams-events/app/meili.py index 1143e23..25bc76a 100644 --- a/streams-events/app/meili.py +++ b/streams-events/app/meili.py @@ -38,10 +38,10 @@ class Op: for op, doc in ops.items(): match op: - case DynamoDBRecordEventName.INSERT: + case ( + DynamoDBRecordEventName.INSERT | DynamoDBRecordEventName.MODIFY + ): index.add_documents(doc, serializer=JSONEncoder) - case DynamoDBRecordEventName.MODIFY: - index.update_documents(doc, serializer=JSONEncoder) case DynamoDBRecordEventName.REMOVE: index.delete_documents(doc)