Skip to content

Commit

Permalink
feat: added firestore key validators
Browse files Browse the repository at this point in the history
  • Loading branch information
dereekb committed Jun 22, 2022
1 parent 2897f90 commit 9d090db
Show file tree
Hide file tree
Showing 8 changed files with 170 additions and 12 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { asSegueRef, asSegueRefString, SegueRefOrSegueRefRouterLink } from './..
import { DbxAuthService } from '../../../service/auth.service';
import { FactoryWithRequiredInput, getValueFromGetter, isGetter, Maybe } from '@dereekb/util';
import { Injector } from '@angular/core';
import { timeoutStartWith } from '@dereekb/rxjs';

/**
* authTransitionHookFn() configuration. The values are handled as:
Expand Down Expand Up @@ -104,6 +105,8 @@ export function makeAuthTransitionHook(config: AuthTransitionHookConfig): Transi
}

const resultObs = decisionObs.pipe(
// after 10 seconds of no transition working, redirect with a false decision
timeoutStartWith(false as AuthTransitionDecision, 10 * 1000),
first(),
switchMap((decision: AuthTransitionDecision): Observable<HookResult> => {
if (typeof decision === 'boolean') {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { filterMaybe, isNot } from '@dereekb/rxjs';
import { filterMaybe, isNot, timeoutStartWith } from '@dereekb/rxjs';
import { Injectable, Optional } from '@angular/core';
import { AuthUserState, DbxAuthService, loggedOutObsFromIsLoggedIn, loggedInObsFromIsLoggedIn, AuthUserIdentifier, authUserIdentifier } from '@dereekb/dbx-core';
import { Auth, authState, User, IdTokenResult, ParsedToken, GoogleAuthProvider, signInWithPopup, AuthProvider, PopupRedirectResolver, signInAnonymously, signInWithEmailAndPassword, UserCredential, FacebookAuthProvider, GithubAuthProvider, TwitterAuthProvider, createUserWithEmailAndPassword } from '@angular/fire/auth';
Expand Down Expand Up @@ -41,14 +41,7 @@ export const DEFAULT_DBX_FIREBASE_AUTH_SERVICE_DELEGATE: DbxFirebaseAuthServiceD
export class DbxFirebaseAuthService implements DbxAuthService {
private readonly _authState$: Observable<Maybe<User>> = authState(this.firebaseAuth);

readonly currentAuthUser$: Observable<Maybe<User>> = this._authState$.pipe(
timeout({
first: 1000,
with: () => this._authState$.pipe(startWith(null))
}),
distinctUntilChanged(),
shareReplay(1)
);
readonly currentAuthUser$: Observable<Maybe<User>> = this._authState$.pipe(timeoutStartWith(null as Maybe<User>, 1000), distinctUntilChanged(), shareReplay(1));

readonly currentAuthUserInfo$: Observable<Maybe<AuthUserInfo>> = this.currentAuthUser$.pipe(map((x) => (x ? authUserInfoFromAuthUser(x) : undefined)));

Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { childFirestoreModelKeyPath } from '.';
import { childFirestoreModelKeyPath, isFirestoreModelId, isFirestoreModelKey } from '.';
import { firestoreModelKeys, firestoreModelIdentity, firestoreModelKey, firestoreModelKeyPath } from './collection';

describe('firestoreModelIdentity()', () => {
Expand Down Expand Up @@ -124,3 +124,35 @@ describe('childFirestoreModelKeyPath', () => {
});
});
});

describe('isFirestoreModelId', () => {
it('should pass firestore model ids', () => {
expect(isFirestoreModelId('a')).toBe(true);
});

it('should fail on firestore model keys', () => {
expect(isFirestoreModelId('a/b')).toBe(false);
});
});

describe('isFirestoreModelKey', () => {
it('should pass root firestore model keys', () => {
expect(isFirestoreModelKey('a/b')).toBe(true);
});

it('should pass child firestore model keys', () => {
expect(isFirestoreModelKey('a/b/c/d')).toBe(true);
});

it('should fail on firestore model ids', () => {
expect(isFirestoreModelKey('a')).toBe(false);
});

it('should fail on firestore model ids that end with a slash', () => {
expect(isFirestoreModelKey('a/')).toBe(false);
});

it('should fail on firestore model keys that point to a collection', () => {
expect(isFirestoreModelKey('a/b/c')).toBe(false);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,22 @@ export interface FirestoreModelIdentityRef<I extends FirestoreModelIdentity> {
*/
export type FirestoreModelId = string;

/**
* Firestore Model Id Regex
*
* https://stackoverflow.com/questions/52850099/what-is-the-reg-expression-for-firestore-constraints-on-document-ids
*/
export const FIRESTORE_MODEL_ID_REGEX = /^(?!\.\.?$)(?!.*__.*__)([^\s\/]{1,1500})$/;

/**
* Returns true if the input string is a FirestoreModelId.
*
* @param input
*/
export function isFirestoreModelId(input: string | FirestoreModelId): input is FirestoreModelId {
return FIRESTORE_MODEL_ID_REGEX.test(input);
}

/**
* Reference to a FirestoreModelId
*/
Expand All @@ -201,6 +217,25 @@ export interface FirestoreModelIdRef {
*/
export type FirestoreModelKey = ModelKey;

/**
* Firestore Model Key Regex that checks for pairs.
*/
export const FIRESTORE_MODEL_KEY_REGEX = /^(?:([^\s\/]+)\/([^\s\/]+))(?:\/(?:([^\s\/]+))\/(?:([^\s\/]+)))*$/;

/**
* Firestore Model Key Regex that is more strict
*/
export const FIRESTORE_MODEL_KEY_REGEX_STRICT = /^(?:(?:(?!\.\.?$)(?!.*__.*__)([^\s\/]+))\/(?:(?!\.\.?$)(?!.*__.*__)([^\s\/]+))\/?)(?:\/(?:(?!\.\.?$)(?!.*__.*__)([^\s\/]+))\/(?:(?!\.\.?$)(?!.*__.*__)([^\s\/]+)))*$/;

/**
* Returns true if the input string is a FirestoreModelKey.
*
* @param input
*/
export function isFirestoreModelKey(input: string | FirestoreModelKey): input is FirestoreModelKey {
return FIRESTORE_MODEL_KEY_REGEX.test(input);
}

/**
* A part of a FirestoreModelKey.
*/
Expand Down
1 change: 1 addition & 0 deletions packages/firebase/src/lib/common/model/model/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
export * from './model.loader';
export * from './model.param';
export * from './model.validator';
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import { Expose } from 'class-transformer';
import { IsOptional, validate } from 'class-validator';
import { FirestoreModelId, FirestoreModelKey } from '../../firestore/collection/collection';
import { IsFirestoreModelId, IsFirestoreModelKey } from './model.validator';

class TestModelClass {
@Expose()
@IsOptional()
@IsFirestoreModelId()
id!: FirestoreModelId;

@Expose()
@IsOptional()
@IsFirestoreModelKey()
key!: FirestoreModelKey;
}

describe('IsFirestoreModelKey', () => {
it('should pass valid keys', async () => {
const instance = new TestModelClass();
instance.key = 'valid/key';

const result = await validate(instance);
expect(result.length).toBe(0);
});

it('should fail on invalid keys', async () => {
const instance = new TestModelClass();
instance.key = 'invalid';

const result = await validate(instance);
expect(result.length).toBe(1);
});
});

describe('IsFirestoreModelId', () => {
it('should pass valid ids', async () => {
const instance = new TestModelClass();
instance.id = 'validid';

const result = await validate(instance);
expect(result.length).toBe(0);
});

it('should fail on invalid ids', async () => {
const instance = new TestModelClass();
instance.id = 'invalid/id';

const result = await validate(instance);
expect(result.length).toBe(1);
});
});
42 changes: 42 additions & 0 deletions packages/firebase/src/lib/common/model/model/model.validator.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { ValidatorConstraint, ValidatorConstraintInterface, ValidationArguments, ValidationOptions, registerDecorator } from 'class-validator';
import { isFirestoreModelId, isFirestoreModelKey } from '../../firestore/collection/collection';

/**
* isFirestoreModelKey validator
*/
export function IsFirestoreModelKey(validationOptions?: ValidationOptions) {
return function (object: Object, propertyName: string) {
registerDecorator({
name: 'isFirestoreModelKey',
target: object.constructor,
propertyName: propertyName,
options: validationOptions,
validator: {
validate: isFirestoreModelKey,
defaultMessage(args: ValidationArguments) {
return `"${args.value}" is not a FirestoreModelKey.`;
}
}
});
};
}

/**
* isFirestoreModelId validator
*/
export function IsFirestoreModelId(validationOptions?: ValidationOptions) {
return function (object: Object, propertyName: string) {
registerDecorator({
name: 'isFirestoreModelId',
target: object.constructor,
propertyName: propertyName,
options: validationOptions,
validator: {
validate: isFirestoreModelId,
defaultMessage(args: ValidationArguments) {
return `"${args.value}" is not a FirestoreModelId.`;
}
}
});
};
}
4 changes: 2 additions & 2 deletions packages/rxjs/src/lib/rxjs/value.ts
Original file line number Diff line number Diff line change
Expand Up @@ -113,9 +113,9 @@ export function switchMapMaybeObs<T = unknown>(): OperatorFunction<Maybe<Observa
/**
* Used to pass a default value incase an observable has not yet started emititng values.
*/
export function timeoutStartWith<T>(defaultValue: GetterOrValue<T>): MonoTypeOperatorFunction<T> {
export function timeoutStartWith<T>(defaultValue: GetterOrValue<T>, first = 0): MonoTypeOperatorFunction<T> {
return (source: Observable<T>) => {
return source.pipe(timeout({ first: 0, with: () => source.pipe(startWith(getValueFromGetter(defaultValue))) }));
return source.pipe(timeout({ first, with: () => source.pipe(startWith(getValueFromGetter(defaultValue))) }));
};
}

Expand Down

0 comments on commit 9d090db

Please sign in to comment.